Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { doctorCommand } from './src/commands/doctor';
import { migrateCommand } from './src/commands/migrate';
import { eventsAuditCommand } from './src/commands/events-audit';
import { revenueCommand } from './src/commands/revenue';
import { slackCommand } from './src/commands/slack';
import { uploadSourcemapsCommand } from './src/commands/upload-sourcemaps';
import { skillCommand } from './src/commands/skill';
import { recoverOrphanedSettingsBackups } from './src/lib/agent/claude-settings';
Expand Down Expand Up @@ -67,6 +68,7 @@ Wizard.use(basicIntegrationCommand)
.use(migrateCommand)
.use(eventsAuditCommand)
.use(revenueCommand)
.use(slackCommand)
.use(uploadSourcemapsCommand)
.use(skillCommand)
.init();
43 changes: 43 additions & 0 deletions src/commands/slack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Arguments } from 'yargs';
import { getUI, setUI } from '@ui';
import { LoggingUI } from '@ui/logging-ui';
import { Program } from '@lib/programs/program-registry';
import { VERSION } from '@lib/version';
import type { Command } from './command';

export const slackCommand: Command = {
name: 'slack',
description: 'Connect PostHog to your Slack',
handler: runSlackConnect,
// Mirrors the mcp command family shape: `wizard slack` and
// `wizard slack add` run the same connect flow.
children: [
{
name: 'add',
description: 'Connect PostHog to your Slack',
handler: runSlackConnect,
},
],
};

function runSlackConnect(argv: Arguments): void {
void (async () => {
const debug = argv.debug as boolean | undefined;

try {
const { startTUI } = await import('@ui/tui/start-tui');
const { buildSession } = await import('@lib/wizard-session');
const tui = startTUI(VERSION, Program.SlackConnect);
tui.store.session = buildSession({ debug });
} catch (err) {
// TUI unavailable — connecting Slack has no headless fallback.
setUI(new LoggingUI());
getUI().log.error(
`Connecting Slack requires an interactive terminal. ${
err instanceof Error ? err.message : String(err)
}`,
);
process.exit(1);
}
})();
}
26 changes: 13 additions & 13 deletions src/lib/oauth/program-scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
* surface (feature flags, experiments, surveys, replays, errors, web
* analytics, LLM analytics, cohorts, persons) plus read/write on
* annotations; `AgentSkill` adds feature-flag read/write; the default
* `PostHogIntegration` run adds `integration:read` for the
* Connect-Slack step. Persistence writes (dashboard:write,
* `PostHogIntegration` run and the standalone `slack` flow add
* `integration:read` for the Connect-Slack step. Persistence writes (dashboard:write,
* insight:write, notebook:write, query:read) come for free from the
* base set, so the tutorial's "save as insight / pin to dashboard /
* add to notebook" follow-ups keep working.
Expand Down Expand Up @@ -120,18 +120,17 @@ export const AGENT_SKILL_SCOPE_ADDITIONS = [
] as const;

/**
* Extra scope the default wizard run needs on top of `WIZARD_OAUTH_SCOPES`.
* Extra scope the Connect-Slack step needs on top of `WIZARD_OAUTH_SCOPES`.
*
* The Connect-Slack step at the end of the run polls
* `/api/projects/:id/integrations/` (`fetchSlackConnected`) to render the
* already-connected variant and to flip live once the user completes the
* Slack OAuth step in the browser. Without `integration:read` the first
* poll 403s, the screen stops polling, and an already-connected project
* is nagged with the connect nudge.
* The step polls `/api/projects/:id/integrations/` (`fetchSlackConnected`)
* to render the already-connected variant and to flip live once the user
* completes the Slack OAuth step in the browser. Without `integration:read`
* the first poll 403s, the screen stops polling, and an already-connected
* project is nagged with the connect nudge. Used by the default integration
* run (the step ends the run) and by the standalone `wizard slack` flow
* (the step is the whole program).
*/
export const POSTHOG_INTEGRATION_SCOPE_ADDITIONS = [
'integration:read',
] as const;
export const CONNECT_SLACK_SCOPE_ADDITIONS = ['integration:read'] as const;

/**
* Per-program scope additions, layered on top of `WIZARD_OAUTH_SCOPES`.
Expand All @@ -151,7 +150,8 @@ const PROGRAM_SCOPE_ADDITIONS: Partial<Record<ProgramId, readonly string[]>> = {
// ever changes, this line will fail to type-check.
'mcp-tutorial': MCP_TUTORIAL_SCOPE_ADDITIONS,
'agent-skill': AGENT_SKILL_SCOPE_ADDITIONS,
'posthog-integration': POSTHOG_INTEGRATION_SCOPE_ADDITIONS,
'posthog-integration': CONNECT_SLACK_SCOPE_ADDITIONS,
slack: CONNECT_SLACK_SCOPE_ADDITIONS,
};

/**
Expand Down
3 changes: 3 additions & 0 deletions src/lib/programs/program-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
mcpRemoveConfig,
mcpTutorialConfig,
} from './mcp/index.js';
import { slackConnectConfig } from './slack/index.js';

// Generic skill program — invoked when the wizard runs an arbitrary
// context-mill skill chosen at runtime (session.skillId) rather than a
Expand All @@ -53,6 +54,7 @@ export const PROGRAM_REGISTRY = [
mcpAddConfig,
mcpRemoveConfig,
mcpTutorialConfig,
slackConnectConfig,
] as const satisfies readonly ProgramConfig[];

/**
Expand All @@ -74,6 +76,7 @@ export const Program = {
McpAdd: mcpAddConfig.id,
McpRemove: mcpRemoveConfig.id,
McpTutorial: mcpTutorialConfig.id,
SlackConnect: slackConnectConfig.id,
} as const;

/** Compile-time union of every registered program id. */
Expand Down
60 changes: 60 additions & 0 deletions src/lib/programs/slack/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Slack connect program — TUI-only flow invoked by `wizard slack`. One
* step: the same Connect Slack screen the MCP flows end on. OAuth kicks
* off from `onInit` while the screen renders the auth-wait state; the
* connected poll starts once credentials land.
*/

import type { ProgramConfig } from '@lib/programs/program-step';
import { getOrAskForProjectData } from '@utils/setup-utils';
import { getUI } from '@ui';
import { analytics } from '@utils/analytics';

export const slackConnectConfig: ProgramConfig = {
id: 'slack',
description: 'Connect PostHog to your Slack',
steps: [
{
id: 'slack-connect',
label: 'Connect Slack',
screenId: 'slack-connect',
isComplete: (s) => s.slackStepDismissed,
onInit: loginForSlackConnect,
},
],
};

/** OAuth for the standalone flow. The screen shows the auth-wait state
* (and the login URL) until the credentials land in the store. */
function loginForSlackConnect(): void {
void (async () => {
try {
const data = await getOrAskForProjectData({
signup: false,
ci: false,
apiKey: undefined,
projectId: undefined,
programId: 'slack',
});
const ui = getUI();
ui.setCredentials({
accessToken: data.accessToken,
projectApiKey: data.projectApiKey,
host: data.host,
projectId: data.projectId,
});
ui.setRoleAtOrganization(data.roleAtOrganization);
ui.setApiUser(data.user);
ui.setLoginUrl(null);
} catch (err) {
analytics.captureException(
err instanceof Error ? err : new Error(String(err)),
{ step: 'slack_connect_login' },
);
getUI().log.error(
`Login failed. ${err instanceof Error ? err.message : String(err)}`,
);
process.exit(1);
}
})();
}
Loading
Loading