Skip to content
Open
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
5 changes: 5 additions & 0 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { hideBin } from 'yargs/helpers';
import { VERSION } from './src/lib/version.js';

const WIZARD_VERSION = VERSION;

Check warning on line 9 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value

const NODE_VERSION_RANGE = '>=18.17.0';

Expand Down Expand Up @@ -340,7 +340,7 @@
});
},
(argv) => {
const options = { ...argv };

Check warning on line 343 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`
const mcpFeatures = options.features
?.split(',')
.map((s) => s.trim())
Expand Down Expand Up @@ -420,7 +420,7 @@
await removeMCPServerFromClientsStep({
local: options.local,
});
}

Check warning on line 423 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`
})();
},
)
Expand Down Expand Up @@ -466,7 +466,7 @@
const installDir = (options.installDir as string) || process.cwd();

const { startTUI } = await import('./src/ui/tui/start-tui.js');
const { buildSession } = await import('./src/lib/wizard-session.js');

Check warning on line 469 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`
const { TaskStreamPush } = await import('./src/lib/task-stream/index.js');
const { FileDestination } = await import(
'./src/lib/task-stream/destinations/file.js'
Expand Down Expand Up @@ -496,6 +496,8 @@
session.workflowLabel = config.flowKey;
if (options.skillId) {
session.skillId = options.skillId as string;
} else if (config.skillId) {
session.skillId = config.skillId;
}

tui.store.session = session;
Expand Down Expand Up @@ -574,7 +576,7 @@
*
* Validates flags, builds a `ci:true` session, runs `preRun` (or the
* workflow's `onReady` hooks by default), executes `runAgent`, and
* routes any failure through `wizardAbort`. `wizardAbort` owns all

Check warning on line 579 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Forbidden non-null assertion
* exits — never add a raw `process.exit` here.
*/
function runWizardCI(
Expand Down Expand Up @@ -619,7 +621,7 @@
: path.join(process.cwd(), options.installDir as string);

const session = buildSession({
debug: options.debug as boolean | undefined,

Check warning on line 624 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type

Check warning on line 624 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `Flow`

Check warning on line 624 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe argument of type `any` assigned to a parameter of type `string`
forceInstall: options.forceInstall as boolean | undefined,
installDir,
ci: true,
Expand All @@ -632,9 +634,12 @@
projectId: options.projectId as string | undefined,
benchmark: options.benchmark as boolean | undefined,
yaraReport: options.yaraReport as boolean | undefined,
...env,

Check warning on line 637 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type

Check warning on line 637 in bin.ts

View workflow job for this annotation

GitHub Actions / Lint

Unsafe assignment of an `any` value
});
session.workflowLabel = config.flowKey;
if (config.skillId) {
session.skillId = config.skillId;
}
const runDef = typeof config.run === 'object' ? config.run : null;

getUI().intro('Welcome to the PostHog setup wizard');
Expand Down
19 changes: 19 additions & 0 deletions src/lib/agent/agent-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ export const AgentSignals = {
WIZARD_REMARK: '[WIZARD-REMARK]',
/** Signal prefix for benchmark logging */
BENCHMARK: '[BENCHMARK]',
/**
* Signal emitted when the agent has created a PostHog dashboard for the
* user. Format: `[DASHBOARD_URL] <full https url>`. The URL is captured
* onto `session.dashboardUrl` and surfaced by workflows in their outro.
*/
DASHBOARD_URL: '[DASHBOARD_URL]',
} as const;

export type AgentSignal = (typeof AgentSignals)[keyof typeof AgentSignals];
Expand Down Expand Up @@ -1212,6 +1218,19 @@ function handleSDKMessage(
getUI().pushStatus(statusText);
spinner.message(statusText);
}

// Check for [DASHBOARD_URL] markers
const dashboardRegex = new RegExp(
`${AgentSignals.DASHBOARD_URL.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&',
)}\\s*(\\S+)`,
'm',
);
const dashboardMatch = block.text.match(dashboardRegex);
if (dashboardMatch) {
getUI().setDashboardUrl(dashboardMatch[1].trim());
}
}

// Intercept TodoWrite tool_use blocks for task progression
Expand Down
12 changes: 12 additions & 0 deletions src/lib/wizard-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,18 @@ export interface OutroData {
continueUrl?: string;
/** Report file the agent wrote (e.g. "posthog-setup-report.md") */
reportFile?: string;
/** PostHog dashboard URL the workflow created on the user's behalf. */
dashboardUrl?: string;
}

/**
* PostHog dashboard URL emitted by the agent during a workflow run.
* Populated via the `[DASHBOARD_URL]` text marker in agent assistant messages
* — see `handleSDKMessage` in `agent/agent-interface.ts`. Read by workflows
* (e.g. events-audit) inside `buildOutroData` to surface a dashboard link
* the agent actually created.
*/

export interface WizardSession {
// From CLI args
debug: boolean;
Expand Down Expand Up @@ -158,6 +168,7 @@ export interface WizardSession {
user: string;
} | null;
outroData: OutroData | null;
dashboardUrl: string | null;

// Additional features queue (drained via stop hook after main integration)
additionalFeatureQueue: AdditionalFeature[];
Expand Down Expand Up @@ -230,6 +241,7 @@ export function buildSession(args: {
settingsConflicts: null,
portConflictProcess: null,
outroData: null,
dashboardUrl: null,
additionalFeatureQueue: [],
workflowLabel: null,
skillId: null,
Expand Down
1 change: 1 addition & 0 deletions src/lib/workflows/agent-skill/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export function createSkillWorkflow(
command: opts.command,
description: opts.description,
flowKey: opts.flowKey,
skillId: opts.skillId,
steps: AGENT_SKILL_STEPS,
run: {
skillId: opts.skillId,
Expand Down
77 changes: 77 additions & 0 deletions src/lib/workflows/events-audit/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { WorkflowConfig } from '../workflow-step.js';
import type { WorkflowRun } from '../../agent/agent-runner.js';
import type { WizardSession } from '../../wizard-session.js';
import { OutroKind } from '../../wizard-session.js';
import { SPINNER_MESSAGE } from '../../framework-config.js';
import { isUsingTypeScript } from '../../../utils/setup-utils.js';
import { getCloudUrlFromRegion } from '../../../utils/urls.js';
import { EVENTS_AUDIT_WORKFLOW } from './steps.js';

export const SETUP_REPORT_FILE = 'posthog-events-audit-report.md';

const DOCS_URL = 'https://posthog.com/docs/product-analytics/best-practices';

export const eventsAuditConfig: WorkflowConfig = {
command: 'events-audit',
description: 'Audit PostHog event tracking in this project',
flowKey: 'events-audit',
skillId: 'events-audit',
steps: EVENTS_AUDIT_WORKFLOW,

run: (session: WizardSession): Promise<WorkflowRun> => {
const typeScriptDetected = isUsingTypeScript({
installDir: session.installDir,
});
session.typescript = typeScriptDetected;

return Promise.resolve({
skillId: 'events-audit',
integrationLabel: 'events-audit',
spinnerMessage: SPINNER_MESSAGE,
successMessage:
'Events audit complete! You can view the report at ./posthog-events-audit-report.md',
estimatedDurationMinutes: 5,
reportFile: SETUP_REPORT_FILE,
docsUrl: DOCS_URL,
errorMessage: 'Events audit failed',
additionalFeatureQueue: session.additionalFeatureQueue,

customPrompt: (ctx) =>
`Audit PostHog event capture in this project. Do not modify any project files — produce a read-only report only.

Project context:
- PostHog Project ID: ${ctx.projectId}
- TypeScript: ${typeScriptDetected ? 'Yes' : 'No'}
- PostHog public token: ${ctx.projectApiKey}
- PostHog Host: ${ctx.host}
`,

buildOutroData: (sess, _credentials, cloudRegion) => {
const cloudUrl = cloudRegion
? getCloudUrlFromRegion(cloudRegion)
: undefined;
const continueUrl =
sess.signup && cloudUrl
? `${cloudUrl}/products?source=wizard`
: undefined;
// The agent emits `[DASHBOARD_URL] <url>` once it creates the
// dashboard; the SDK-message interceptor stores it on the session.
// Fall back to the dashboards index if nothing was emitted.
const dashboardUrl =
sess.dashboardUrl ?? (cloudUrl ? `${cloudUrl}/dashboard` : undefined);

return {
kind: OutroKind.Success as const,
message: 'Your events audit was successful',
reportFile: SETUP_REPORT_FILE,
changes: [],
docsUrl: DOCS_URL,
continueUrl,
dashboardUrl,
};
},
});
},
};

export { EVENTS_AUDIT_WORKFLOW } from './steps.js';
115 changes: 115 additions & 0 deletions src/lib/workflows/events-audit/steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* Events-audit workflow.
*
* Mirrors the posthog-integration step list, except:
* - The initial framework detection step is omitted — the events-audit
* skill handles detection at agent run time.
* - The intro step uses the audit intro screen (no framework selection
* logic) instead of the integration intro.
*/

import type { Workflow } from '../workflow-step.js';
import type { WizardSession } from '../../wizard-session.js';
import { RunPhase } from '../../wizard-session.js';
import {
evaluateWizardReadiness,
WizardReadiness,
SIGNUP_WIZARD_READINESS_CONFIG,
getBlockingServiceKeys,
} from '../../health-checks/readiness.js';

function needsSetup(session: WizardSession): boolean {
const config = session.frameworkConfig;
if (!config?.metadata.setup?.questions) return false;

return config.metadata.setup.questions.some(
(q: { key: string }) => !(q.key in session.frameworkContext),
);
}

function healthCheckReady(session: WizardSession): boolean {
if (!session.readinessResult) return false;

if (session.signup) {
const hardBlocking = getBlockingServiceKeys(
session.readinessResult.health,
SIGNUP_WIZARD_READINESS_CONFIG,
);
const defaultBlocking = getBlockingServiceKeys(
session.readinessResult.health,
);
if (hardBlocking.length === 0 && defaultBlocking.length === 0) return true;
return session.outageDismissed;
}

if (session.readinessResult.decision === WizardReadiness.No) {
return session.outageDismissed;
}
return true;
}

export const EVENTS_AUDIT_WORKFLOW: Workflow = [
{
id: 'intro',
label: 'Welcome',
screen: 'audit-intro',
gate: (session) => session.setupConfirmed,
},
{
id: 'health-check',
label: 'Health check',
screen: 'health-check',
gate: healthCheckReady,
onInit: (ctx) => {
evaluateWizardReadiness()
.then((readiness) => {
ctx.setReadinessResult(readiness);
})
.catch(() => {
ctx.setReadinessResult({
decision: WizardReadiness.Yes,
health: {} as never,
reasons: [],
});
});
},
},
{
id: 'setup',
label: 'Setup',
screen: 'setup',
show: needsSetup,
isComplete: (session) => !needsSetup(session),
},
{
id: 'auth',
label: 'Authentication',
screen: 'auth',
isComplete: (session) => session.credentials !== null,
},
{
id: 'run',
label: 'Events audit',
screen: 'run',
isComplete: (session) =>
session.runPhase === RunPhase.Completed ||
session.runPhase === RunPhase.Error,
},
{
id: 'mcp',
label: 'MCP servers',
screen: 'mcp',
isComplete: (session) => session.mcpComplete,
},
{
id: 'outro',
label: 'Done',
screen: 'outro',
isComplete: (session) => session.outroDismissed,
},
{
id: 'keep-skills',
label: 'Keep Skills',
screen: 'keep-skills',
},
];
2 changes: 2 additions & 0 deletions src/lib/workflows/workflow-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import type { WorkflowConfig } from './workflow-step.js';
import { posthogIntegrationConfig } from './posthog-integration/index.js';
import { revenueAnalyticsConfig } from './revenue-analytics/index.js';
import { auditConfig } from './audit/index.js';
import { eventsAuditConfig } from './events-audit/index.js';
import { posthogDoctorConfig } from './posthog-doctor/index.js';

export const WORKFLOW_REGISTRY: WorkflowConfig[] = [
posthogIntegrationConfig,
revenueAnalyticsConfig,
auditConfig,
eventsAuditConfig,
posthogDoctorConfig,
];

Expand Down
7 changes: 7 additions & 0 deletions src/lib/workflows/workflow-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ export interface WorkflowConfig {
description: string;
/** Unique flow key — matches the Flow enum value */
flowKey: string;
/**
* Context-mill skill ID this workflow installs and runs. When present,
* bin.ts seeds `session.skillId` with this value before the TUI renders
* so intro screens can resolve skill metadata without waiting for the
* agent run.
*/
skillId?: string;
/** The ordered step list */
steps: Workflow;
/** Agent run config. Static object or async function for dynamic config. */
Expand Down
4 changes: 4 additions & 0 deletions src/ui/logging-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ export class LoggingUI implements WizardUI {
// No-op in CI mode
}

setDashboardUrl(_url: string): void {
// No-op in CI mode
}

setFrameworkContext(_key: string, _value: unknown): void {
// No-op in CI mode
}
Expand Down
1 change: 1 addition & 0 deletions src/ui/tui/flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export enum Flow {
PostHogIntegration = 'posthog-integration',
RevenueAnalyticsSetup = 'revenue-analytics-setup',
Audit = 'audit',
EventsAudit = 'events-audit',
PosthogDoctor = 'posthog-doctor',
AgentSkill = 'agent-skill',
McpAdd = 'mcp-add',
Expand Down
4 changes: 4 additions & 0 deletions src/ui/tui/ink-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ export class InkUI implements WizardUI {
this.store.setEventPlan(events);
}

setDashboardUrl(url: string): void {
this.store.setDashboardUrl(url);
}

setFrameworkContext(key: string, value: unknown): void {
this.store.setFrameworkContext(key, value);
}
Expand Down
9 changes: 9 additions & 0 deletions src/ui/tui/screens/OutroScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ export const OutroScreen = ({ store }: OutroScreenProps) => {
</Box>
)}

{outroData.dashboardUrl && (
<Box marginTop={1}>
<Text>
We've also made you a dashboard:{' '}
<Text color="cyan">{outroData.dashboardUrl}</Text>
</Text>
</Box>
)}

{outroData.docsUrl && (
<Box marginTop={1}>
<Text>
Expand Down
Loading
Loading