Skip to content

Commit 3bf2558

Browse files
feat: CI mode for wizard (#216)
Co-authored-by: Vincent (Wen Yu) Ge <29069505+gewenyu99@users.noreply.github.com>
1 parent dcab7d8 commit 3bf2558

File tree

12 files changed

+338
-51
lines changed

12 files changed

+338
-51
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,48 @@ The following CLI arguments are available:
4848
| `--integration` | Integration to set up | string | | "nextjs", "astro", "react", "svelte", "react-native" | |
4949
| `--force-install` | Force install packages even if peer dependency checks fail | boolean | `false` | | `POSTHOG_WIZARD_FORCE_INSTALL` |
5050
| `--install-dir` | Directory to install PostHog in | string | | | `POSTHOG_WIZARD_INSTALL_DIR` |
51+
| `--ci` | Enable CI mode for non-interactive execution | boolean | `false` | | `POSTHOG_WIZARD_CI` |
52+
| `--api-key` | PostHog personal API key (phx_xxx) for authentication | string | | | `POSTHOG_WIZARD_API_KEY` |
5153

5254
> Note: A large amount of the scaffolding for this came from the amazing Sentry
5355
> wizard, which you can find [here](https://github.com/getsentry/sentry-wizard)
5456
> 💖
5557
58+
# CI Mode
59+
60+
Run the wizard non-interactive executions with `--ci`:
61+
62+
```bash
63+
npx @posthog/wizard --ci --region us --api-key $POSTHOG_PERSONAL_API_KEY --install-dir .
64+
```
65+
66+
When running in CI mode (`--ci`):
67+
68+
- Bypasses OAuth login flow (uses personal API key directly)
69+
- Auto-selects defaults for all prompts
70+
- Skips MCP server installation
71+
- Auto-continues on git warnings (uncommitted/untracked files)
72+
- Auto-consents to AI usage
73+
74+
The CLI args override environment variables in CI mode.
75+
76+
### Required Flags for CI Mode
77+
78+
- `--region`: Cloud region (`us` or `eu`)
79+
- `--api-key`: Personal API key (`phx_xxx`) from your [PostHog settings](https://app.posthog.com/settings/user-api-keys)
80+
- `--install-dir`: Directory to install PostHog in (e.g., `.` for current directory)
81+
82+
### Required API Key Scopes
83+
84+
When creating your personal API key, ensure it has the following scopes enabled:
85+
86+
- `user:read` - Required to fetch user information
87+
- `project:read` - Required to fetch project details and API token
88+
- `introspection` - Required for API introspection
89+
- `llm_gateway:read` - Required for LLM gateway access
90+
- `dashboard:write` - Required to create dashboards
91+
- `insight:write` - Required to create insights
92+
5693
# Steal this code
5794

5895
While the wizard works great on its own, we also find the approach used by this

bin.ts

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,6 @@ import { runWizard } from './src/run';
2323
import { isNonInteractiveEnvironment } from './src/utils/environment';
2424
import clack from './src/utils/clack';
2525

26-
if (isNonInteractiveEnvironment()) {
27-
clack.intro(chalk.inverse(`PostHog Wizard`));
28-
29-
clack.log.error(
30-
'This installer requires an interactive terminal (TTY) to run.\n' +
31-
'It appears you are running in a non-interactive environment.\n' +
32-
'Please run the wizard in an interactive terminal.',
33-
);
34-
process.exit(1);
35-
}
36-
3726
if (process.env.NODE_ENV === 'test') {
3827
void (async () => {
3928
try {
@@ -79,6 +68,17 @@ yargs(hideBin(process.argv))
7968
'Use local MCP server at http://localhost:8787/mcp\nenv: POSTHOG_WIZARD_LOCAL_MCP',
8069
type: 'boolean',
8170
},
71+
ci: {
72+
default: false,
73+
describe:
74+
'Enable CI mode for non-interactive execution\nenv: POSTHOG_WIZARD_CI',
75+
type: 'boolean',
76+
},
77+
'api-key': {
78+
describe:
79+
'PostHog personal API key (phx_xxx) for authentication\nenv: POSTHOG_WIZARD_API_KEY',
80+
type: 'string',
81+
},
8282
})
8383
.command(
8484
['$0'],
@@ -105,6 +105,42 @@ yargs(hideBin(process.argv))
105105
},
106106
(argv) => {
107107
const options = { ...argv };
108+
109+
// CI mode validation and TTY check
110+
if (options.ci) {
111+
// Validate required CI flags
112+
if (!options.region) {
113+
clack.intro(chalk.inverse(`PostHog Wizard`));
114+
clack.log.error('CI mode requires --region (us or eu)');
115+
process.exit(1);
116+
}
117+
if (!options.apiKey) {
118+
clack.intro(chalk.inverse(`PostHog Wizard`));
119+
clack.log.error(
120+
'CI mode requires --api-key (personal API key phx_xxx)',
121+
);
122+
process.exit(1);
123+
}
124+
if (!options.installDir) {
125+
clack.intro(chalk.inverse(`PostHog Wizard`));
126+
clack.log.error(
127+
'CI mode requires --install-dir (directory to install PostHog in)',
128+
);
129+
process.exit(1);
130+
}
131+
} else if (isNonInteractiveEnvironment()) {
132+
// Original TTY error for non-CI mode
133+
clack.intro(chalk.inverse(`PostHog Wizard`));
134+
clack.log.error(
135+
'This installer requires an interactive terminal (TTY) to run.\n' +
136+
'It appears you are running in a non-interactive environment.\n' +
137+
'Please run the wizard in an interactive terminal.\n\n' +
138+
'For CI/CD environments, use --ci mode:\n' +
139+
' npx @posthog/wizard --ci --region us --api-key phx_xxx',
140+
);
141+
process.exit(1);
142+
}
143+
108144
void runWizard(options as unknown as WizardOptions);
109145
},
110146
)

src/__tests__/cli.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ describe('CLI argument parsing', () => {
2323
process.env = { ...originalEnv };
2424
delete process.env.POSTHOG_WIZARD_REGION;
2525
delete process.env.POSTHOG_WIZARD_DEFAULT;
26+
delete process.env.POSTHOG_WIZARD_CI;
27+
delete process.env.POSTHOG_WIZARD_API_KEY;
28+
delete process.env.POSTHOG_WIZARD_INSTALL_DIR;
2629

2730
// Mock process.exit to prevent test runner from exiting
2831
process.exit = jest.fn() as any;
@@ -187,4 +190,113 @@ describe('CLI argument parsing', () => {
187190
expect(args.debug).toBe(true);
188191
});
189192
});
193+
194+
describe('--ci flag', () => {
195+
test('defaults to false when not specified', async () => {
196+
await runCLI([]);
197+
198+
const args = getLastCallArgs(mockRunWizard);
199+
expect(args.ci).toBe(false);
200+
});
201+
202+
test('can be set to true', async () => {
203+
await runCLI([
204+
'--ci',
205+
'--region',
206+
'us',
207+
'--api-key',
208+
'phx_test',
209+
'--install-dir',
210+
'/tmp/test',
211+
]);
212+
213+
const args = getLastCallArgs(mockRunWizard);
214+
expect(args.ci).toBe(true);
215+
});
216+
217+
test('requires --region when --ci is set', async () => {
218+
await runCLI([
219+
'--ci',
220+
'--api-key',
221+
'phx_test',
222+
'--install-dir',
223+
'/tmp/test',
224+
]);
225+
226+
expect(process.exit).toHaveBeenCalledWith(1);
227+
});
228+
229+
test('requires --api-key when --ci is set', async () => {
230+
await runCLI(['--ci', '--region', 'us', '--install-dir', '/tmp/test']);
231+
232+
expect(process.exit).toHaveBeenCalledWith(1);
233+
});
234+
235+
test('requires --install-dir when --ci is set', async () => {
236+
await runCLI(['--ci', '--region', 'us', '--api-key', 'phx_test']);
237+
238+
expect(process.exit).toHaveBeenCalledWith(1);
239+
});
240+
241+
test('passes --api-key to runWizard', async () => {
242+
await runCLI([
243+
'--ci',
244+
'--region',
245+
'us',
246+
'--api-key',
247+
'phx_test_key',
248+
'--install-dir',
249+
'/tmp/test',
250+
]);
251+
252+
const args = getLastCallArgs(mockRunWizard);
253+
expect(args.apiKey).toBe('phx_test_key');
254+
});
255+
});
256+
257+
describe('CI environment variables', () => {
258+
test('respects POSTHOG_WIZARD_CI', async () => {
259+
process.env.POSTHOG_WIZARD_CI = 'true';
260+
process.env.POSTHOG_WIZARD_REGION = 'us';
261+
process.env.POSTHOG_WIZARD_API_KEY = 'phx_env_key';
262+
process.env.POSTHOG_WIZARD_INSTALL_DIR = '/tmp/test';
263+
264+
await runCLI([]);
265+
266+
const args = getLastCallArgs(mockRunWizard);
267+
expect(args.ci).toBe(true);
268+
});
269+
270+
test('respects POSTHOG_WIZARD_API_KEY', async () => {
271+
process.env.POSTHOG_WIZARD_CI = 'true';
272+
process.env.POSTHOG_WIZARD_REGION = 'eu';
273+
process.env.POSTHOG_WIZARD_API_KEY = 'phx_env_key';
274+
process.env.POSTHOG_WIZARD_INSTALL_DIR = '/tmp/test';
275+
276+
await runCLI([]);
277+
278+
const args = getLastCallArgs(mockRunWizard);
279+
expect(args.apiKey).toBe('phx_env_key');
280+
});
281+
282+
test('CLI args override CI environment variables', async () => {
283+
process.env.POSTHOG_WIZARD_CI = 'true';
284+
process.env.POSTHOG_WIZARD_REGION = 'us';
285+
process.env.POSTHOG_WIZARD_API_KEY = 'phx_env_key';
286+
process.env.POSTHOG_WIZARD_INSTALL_DIR = '/tmp/test';
287+
288+
await runCLI([
289+
'--region',
290+
'eu',
291+
'--api-key',
292+
'phx_cli_key',
293+
'--install-dir',
294+
'/other/path',
295+
]);
296+
297+
const args = getLastCallArgs(mockRunWizard);
298+
expect(args.region).toBe('eu');
299+
expect(args.apiKey).toBe('phx_cli_key');
300+
});
301+
});
190302
});

src/astro/astro-wizard.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ export async function runAstroWizard(options: WizardOptions): Promise<void> {
109109
await addMCPServerToClientsStep({
110110
cloudRegion,
111111
integration: Integration.astro,
112+
ci: options.ci,
112113
});
113114

114115
const outroMessage = getOutroMessage({

src/lib/agent-runner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ ${chalk.cyan(config.metadata.docsUrl)}`;
201201
await addMCPServerToClientsStep({
202202
cloudRegion,
203203
integration: config.metadata.integration,
204+
ci: options.ci,
204205
});
205206

206207
// Build outro message

src/react-native/react-native-wizard.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,10 @@ export async function runReactNativeWizard(
162162
await addMCPServerToClientsStep({
163163
cloudRegion,
164164
integration: Integration.reactNative,
165+
ci: options.ci,
165166
});
166167

167-
const packageManagerForOutro = await getPackageManager({
168-
installDir: options.installDir,
169-
});
168+
const packageManagerForOutro = await getPackageManager(options);
170169

171170
const outroMessage = getOutroMessage({
172171
options,

src/react/react-wizard.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export async function runReactWizard(options: WizardOptions): Promise<void> {
156156
await addMCPServerToClientsStep({
157157
cloudRegion,
158158
integration: Integration.react,
159+
ci: options.ci,
159160
});
160161

161162
const outroMessage = getOutroMessage({

src/run.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type Args = {
2828
default?: boolean;
2929
signup?: boolean;
3030
localMcp?: boolean;
31+
ci?: boolean;
32+
apiKey?: string;
3133
};
3234

3335
export async function runWizard(argv: Args) {
@@ -55,10 +57,16 @@ export async function runWizard(argv: Args) {
5557
default: finalArgs.default ?? false,
5658
signup: finalArgs.signup ?? false,
5759
localMcp: finalArgs.localMcp ?? false,
60+
ci: finalArgs.ci ?? false,
61+
apiKey: finalArgs.apiKey,
5862
};
5963

6064
clack.intro(`Welcome to the PostHog setup wizard ✨`);
6165

66+
if (wizardOptions.ci) {
67+
clack.log.info(chalk.dim('Running in CI mode'));
68+
}
69+
6270
const integration =
6371
finalArgs.integration ?? (await getIntegrationForSetup(wizardOptions));
6472

src/steps/add-mcp-server-to-clients/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,20 @@ export const addMCPServerToClientsStep = async ({
4949
cloudRegion,
5050
askPermission = true,
5151
local = false,
52+
ci = false,
5253
}: {
5354
integration?: Integration;
5455
cloudRegion?: CloudRegion;
5556
askPermission?: boolean;
5657
local?: boolean;
58+
ci?: boolean;
5759
}): Promise<string[]> => {
60+
// CI mode: skip MCP installation entirely (default to No)
61+
if (ci) {
62+
clack.log.info('Skipping MCP installation (CI mode)');
63+
return [];
64+
}
65+
5866
const region = cloudRegion ?? (await askForCloudRegion());
5967

6068
const hasPermission = askPermission

src/svelte/svelte-wizard.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export async function runSvelteWizard(options: WizardOptions): Promise<void> {
151151
await addMCPServerToClientsStep({
152152
cloudRegion,
153153
integration: Integration.svelte,
154+
ci: options.ci,
154155
});
155156

156157
const outroMessage = getOutroMessage({

0 commit comments

Comments
 (0)