Skip to content

Commit 7afca9f

Browse files
MattBroclaudeedwinyjlim
authored
feat: headless provision subcommand + --ci --signup for agents (#415)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Edwin Lim <edwin@posthog.com>
1 parent cbf6b04 commit 7afca9f

4 files changed

Lines changed: 560 additions & 15 deletions

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,46 @@ npx @posthog/wizard revenue
4848
Requires PostHog and Stripe SDKs already installed. Supports `--ci` with the
4949
same flags as the main wizard.
5050

51+
## Headless signup + install (agents / CI)
52+
53+
For a fully non-interactive first-run (no existing PostHog account, no TTY,
54+
no browser), combine `--ci --signup --email`. The wizard provisions a new
55+
account, uses the returned personal API key to run the normal CI install,
56+
and wires PostHog into the project at `--install-dir`:
57+
58+
```bash
59+
npx @posthog/wizard --ci --signup \
60+
--email you@example.com \
61+
--install-dir .
62+
```
63+
64+
Optional flags: `--name "Your Name"`, `--region eu` (default `us`),
65+
`--integration nextjs` (else auto-detected).
66+
67+
### Provision only
68+
69+
If you just want credentials — for tests, pre-flight checks, or wiring up
70+
PostHog yourself — use the `provision` subcommand, which emits a structured
71+
`ProvisioningResult` and does nothing else:
72+
73+
```bash
74+
# Human-readable (when stdout is a TTY)
75+
npx @posthog/wizard provision --email user@example.com --region us
76+
77+
# Machine-readable — auto when stdout is piped, or force with --json
78+
npx @posthog/wizard provision --email user@example.com --region eu --json
79+
```
80+
81+
Success prints the full `ProvisioningResult` (`projectApiKey`, `host`,
82+
`projectId`, `accountId`, `accessToken`, `refreshToken`, and
83+
`personalApiKey` if present). Failure exits 1; in `--json` mode the error
84+
is emitted to stderr as `{"error":"...","code":"..."}`, with `code` set to
85+
`email_exists` when the address is already registered.
86+
87+
> ⚠️ **Output contains live credentials.** Pipe it into a secrets store —
88+
> do not let it be captured by shared CI logs. Mask the step output or
89+
> redirect stdout to a file your job reads and discards.
90+
5191
# Options
5292

5393
The following CLI arguments are available:

bin.ts

Lines changed: 149 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ const cli = yargs(hideBin(process.argv))
185185
'Run a specific context-mill skill by ID\nenv: POSTHOG_WIZARD_SKILL',
186186
type: 'string',
187187
},
188+
name: {
189+
describe:
190+
'Name for account creation with --ci --signup\nenv: POSTHOG_WIZARD_NAME',
191+
type: 'string',
192+
},
188193
});
189194
},
190195
(argv) => {
@@ -193,25 +198,81 @@ const cli = yargs(hideBin(process.argv))
193198
// CI mode validation and TTY check
194199
if (options.ci) {
195200
if (!options.region) options.region = 'us';
196-
if (!options.apiKey) {
201+
if (!options.installDir) {
197202
setUI(new LoggingUI());
198203
getUI().intro('PostHog Wizard');
199204
getUI().log.error(
200-
'CI mode requires --api-key (personal API key phx_xxx)',
205+
'CI mode requires --install-dir (directory to install in)',
201206
);
202207
process.exit(1);
203208
return;
204209
}
205-
if (!options.installDir) {
210+
if (!options.apiKey && !options.signup) {
206211
setUI(new LoggingUI());
207212
getUI().intro('PostHog Wizard');
208213
getUI().log.error(
209-
'CI mode requires --install-dir (directory to install in)',
214+
'CI mode requires --api-key (personal API key phx_xxx). ' +
215+
'To create a new account instead, use --signup --email you@example.com.',
216+
);
217+
process.exit(1);
218+
return;
219+
}
220+
if (!options.apiKey && options.signup && !options.email) {
221+
setUI(new LoggingUI());
222+
getUI().intro('PostHog Wizard');
223+
getUI().log.error(
224+
'CI --signup requires --email to create a new account.',
210225
);
211226
process.exit(1);
212227
return;
213228
}
214229
void (async () => {
230+
// If --signup but no existing key, provision a new account first and
231+
// use its personal API key for the rest of the CI install.
232+
if (!options.apiKey && options.signup) {
233+
setUI(new LoggingUI());
234+
getUI().intro('PostHog Wizard');
235+
try {
236+
const { provisionNewAccount } = await import(
237+
'./src/utils/provisioning.js'
238+
);
239+
const signupRegion = (options.region as string).toUpperCase() as
240+
| 'US'
241+
| 'EU';
242+
getUI().log.info(
243+
`Provisioning new PostHog account for ${String(
244+
options.email,
245+
)} in ${signupRegion}...`,
246+
);
247+
const result = await provisionNewAccount(
248+
options.email as string,
249+
options.name ?? '',
250+
signupRegion,
251+
);
252+
if (!result.personalApiKey) {
253+
getUI().log.error(
254+
'Provisioning succeeded but no personal API key was returned — cannot continue install.',
255+
);
256+
process.exit(1);
257+
return;
258+
}
259+
getUI().log.success('Account ready.');
260+
getUI().log.info(` Project API Key: ${result.projectApiKey}`);
261+
getUI().log.info(` Personal API Key: ${result.personalApiKey}`);
262+
getUI().log.info(` Host: ${result.host}`);
263+
options.apiKey = result.personalApiKey;
264+
if (options.projectId == null) {
265+
options.projectId = result.projectId;
266+
}
267+
} catch (error) {
268+
const msg =
269+
error instanceof Error ? error.message : String(error);
270+
getUI().log.error(`Provisioning failed: ${msg}`);
271+
process.exit(1);
272+
return;
273+
}
274+
}
275+
215276
const { posthogIntegrationConfig } = await import(
216277
'./src/lib/workflows/posthog-integration/index.js'
217278
);
@@ -428,6 +489,90 @@ const cli = yargs(hideBin(process.argv))
428489
.help();
429490
});
430491

492+
cli.command(
493+
'provision',
494+
'Create a new PostHog account (headless, no TUI)',
495+
(yargs) => {
496+
return yargs
497+
.options({
498+
email: {
499+
describe: 'Email address for the new account',
500+
type: 'string' as const,
501+
demandOption: true,
502+
},
503+
region: {
504+
describe: 'Cloud region (us or eu)',
505+
choices: ['us', 'eu'] as const,
506+
default: 'us',
507+
},
508+
name: {
509+
describe: 'Name for the new account',
510+
type: 'string' as const,
511+
default: '',
512+
},
513+
json: {
514+
describe:
515+
'Emit JSON result to stdout (defaults to true when stdout is not a TTY)',
516+
type: 'boolean' as const,
517+
},
518+
})
519+
.example('wizard provision --email matt+test@posthog.com --region us', '')
520+
.example(
521+
'wizard provision --email user@example.com --region eu --json',
522+
'',
523+
);
524+
},
525+
(argv) => {
526+
const email = argv.email;
527+
const region = argv.region.toUpperCase() as 'US' | 'EU';
528+
const name = argv.name ?? '';
529+
const jsonMode =
530+
argv.json === undefined ? !process.stdout.isTTY : argv.json;
531+
532+
if (!jsonMode) {
533+
setUI(new LoggingUI());
534+
}
535+
536+
void (async () => {
537+
try {
538+
const { provisionNewAccount } = await import(
539+
'./src/utils/provisioning.js'
540+
);
541+
if (!jsonMode) {
542+
getUI().log.info(`Provisioning account for ${email} in ${region}...`);
543+
}
544+
const result = await provisionNewAccount(email, name, region);
545+
if (jsonMode) {
546+
process.stdout.write(`${JSON.stringify(result)}\n`);
547+
} else {
548+
getUI().log.success('Account provisioned successfully:');
549+
getUI().log.info(` API Key: ${result.projectApiKey}`);
550+
getUI().log.info(` Host: ${result.host}`);
551+
getUI().log.info(` Project ID: ${result.projectId}`);
552+
getUI().log.info(` Account ID: ${result.accountId}`);
553+
getUI().log.info(` Access Token: ${result.accessToken}`);
554+
getUI().log.info(` Refresh Token: ${result.refreshToken}`);
555+
if (result.personalApiKey) {
556+
getUI().log.info(` Personal API Key: ${result.personalApiKey}`);
557+
}
558+
}
559+
process.exit(0);
560+
} catch (error) {
561+
const msg = error instanceof Error ? error.message : String(error);
562+
const code = msg.includes('already associated')
563+
? 'email_exists'
564+
: 'provisioning_failed';
565+
if (jsonMode) {
566+
process.stderr.write(`${JSON.stringify({ error: msg, code })}\n`);
567+
} else {
568+
getUI().log.error(`Provisioning failed: ${msg}`);
569+
}
570+
process.exit(1);
571+
}
572+
})();
573+
},
574+
);
575+
431576
// ── Skill-based workflow subcommands (derived from registry) ─────────
432577
for (const wfConfig of getSubcommandWorkflows()) {
433578
cli.command(

0 commit comments

Comments
 (0)