@zitadel/cli 0.1.0-alpha.0 → 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +227 -18
  2. package/SKILLS.md +104 -11
  3. package/dist/commands/apply.mjs +4 -4
  4. package/dist/commands/apply.mjs.map +1 -1
  5. package/dist/commands/doctor.mjs +291 -17
  6. package/dist/commands/doctor.mjs.map +1 -1
  7. package/dist/commands/eject.mjs +14 -6
  8. package/dist/commands/eject.mjs.map +1 -1
  9. package/dist/commands/logs.mjs +58 -0
  10. package/dist/commands/logs.mjs.map +1 -0
  11. package/dist/commands/plan.mjs +4 -4
  12. package/dist/commands/plan.mjs.map +1 -1
  13. package/dist/commands/reset.mjs +79 -0
  14. package/dist/commands/reset.mjs.map +1 -0
  15. package/dist/commands/setup.mjs +267 -105
  16. package/dist/commands/setup.mjs.map +1 -1
  17. package/dist/commands/start.mjs +288 -0
  18. package/dist/commands/start.mjs.map +1 -0
  19. package/dist/commands/status.mjs +92 -27
  20. package/dist/commands/status.mjs.map +1 -1
  21. package/dist/commands/stop.mjs +105 -0
  22. package/dist/commands/stop.mjs.map +1 -0
  23. package/dist/docker-CnGQK3ZK.mjs +432 -0
  24. package/dist/docker-CnGQK3ZK.mjs.map +1 -0
  25. package/dist/docker-guidance-ypN3IM3o.mjs +21 -0
  26. package/dist/docker-guidance-ypN3IM3o.mjs.map +1 -0
  27. package/dist/{project-C3pSfbao.mjs → oclif-B7lBzh3R.mjs} +339 -119
  28. package/dist/oclif-B7lBzh3R.mjs.map +1 -0
  29. package/dist/orca-BoTFU8SI.mjs +2581 -0
  30. package/dist/orca-BoTFU8SI.mjs.map +1 -0
  31. package/dist/ports-B09RjuHx.mjs +111 -0
  32. package/dist/ports-B09RjuHx.mjs.map +1 -0
  33. package/dist/processes-Cw8TO1SY.mjs +120 -0
  34. package/dist/processes-Cw8TO1SY.mjs.map +1 -0
  35. package/dist/project-Cd0L3PtM.mjs +87 -0
  36. package/dist/project-Cd0L3PtM.mjs.map +1 -0
  37. package/dist/{sync-Cuyh-X1J.mjs → sync-BojoQm2P.mjs} +4 -4
  38. package/dist/{sync-Cuyh-X1J.mjs.map → sync-BojoQm2P.mjs.map} +1 -1
  39. package/oclif.manifest.json +399 -7
  40. package/package.json +8 -41
  41. package/dist/orca-COsUnVoz.mjs +0 -1006
  42. package/dist/orca-COsUnVoz.mjs.map +0 -1
  43. package/dist/project-C3pSfbao.mjs.map +0 -1
@@ -1,13 +1,180 @@
1
- import { l as DEFAULT_SERVER, n as hasZitadelSecret, p as ZitadelError, s as BaseCommand, t as hasZitadelConfig } from "../project-C3pSfbao.mjs";
2
- import { n as RENDERER_IDS, r as issuerFromPort, t as createOrca } from "../orca-COsUnVoz.mjs";
3
- import { Flags } from "@oclif/core";
1
+ import { D as toZitadelError, E as ZitadelError, b as publicCliCommand, n as DEFAULT_SERVER, t as BaseCommand } from "../oclif-B7lBzh3R.mjs";
2
+ import { i as issuerFromPort, n as inspectScaffoldTarget, r as RENDERER_IDS, t as createOrca } from "../orca-BoTFU8SI.mjs";
3
+ import { n as hasZitadelSecret, t as hasZitadelConfig } from "../project-Cd0L3PtM.mjs";
4
+ import { t as listListeningPorts } from "../ports-B09RjuHx.mjs";
4
5
  import { cancel, confirm, intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
6
+ import { Flags } from "@oclif/core";
7
+ import { createZitadelClient } from "@zitadel/api/client";
5
8
  import { consola as consola$1 } from "consola";
6
- import { readFile, stat } from "node:fs/promises";
7
9
  import { basename, join } from "node:path";
8
- import { execFile } from "node:child_process";
9
- import { createZitadelClient } from "@zitadel/api/client";
10
+ import { readFile, stat } from "node:fs/promises";
11
+ import { spawn } from "node:child_process";
10
12
  import pc from "picocolors";
13
+ //#region src/lib/package-manager.ts
14
+ async function detectPackageManager(cwd) {
15
+ const declared = await packageManagerFromManifest(cwd);
16
+ if (declared) return declared;
17
+ if (await exists(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
18
+ if (await exists(join(cwd, "yarn.lock"))) return "yarn";
19
+ if (await exists(join(cwd, "bun.lock"))) return "bun";
20
+ if (await exists(join(cwd, "bun.lockb"))) return "bun";
21
+ if (await exists(join(cwd, "package-lock.json"))) return "npm";
22
+ return "npm";
23
+ }
24
+ function installCommandFor(packageManager) {
25
+ return command(packageManager, ["install"]);
26
+ }
27
+ function devCommandFor(packageManager) {
28
+ switch (packageManager) {
29
+ case "npm": return command("npm", ["run", "dev"]);
30
+ case "pnpm": return command("pnpm", ["dev"]);
31
+ case "yarn": return command("yarn", ["dev"]);
32
+ case "bun": return command("bun", ["run", "dev"]);
33
+ }
34
+ }
35
+ async function runPackageCommand(packageCommand, options) {
36
+ await new Promise((resolve, reject) => {
37
+ const child = spawn(packageCommand.command, packageCommand.args, {
38
+ cwd: options.cwd,
39
+ env: options.env ?? process.env,
40
+ stdio: options.redirectStdoutToStderr ? [
41
+ "ignore",
42
+ "pipe",
43
+ "pipe"
44
+ ] : "inherit"
45
+ });
46
+ if (options.redirectStdoutToStderr) {
47
+ child.stdout?.on("data", (chunk) => process.stderr.write(chunk));
48
+ child.stderr?.on("data", (chunk) => process.stderr.write(chunk));
49
+ }
50
+ child.on("error", reject);
51
+ child.on("close", (code, signal) => {
52
+ if (code === 0) {
53
+ resolve();
54
+ return;
55
+ }
56
+ const detail = signal ? `signal ${signal}` : `exit ${String(code ?? 1)}`;
57
+ const error = /* @__PURE__ */ new Error(`${packageCommand.display} failed with ${detail}`);
58
+ Object.assign(error, {
59
+ code: code ?? 1,
60
+ signal
61
+ });
62
+ reject(error);
63
+ });
64
+ });
65
+ }
66
+ function command(packageManager, args) {
67
+ return {
68
+ command: packageManager,
69
+ args,
70
+ display: [packageManager, ...args].join(" ")
71
+ };
72
+ }
73
+ async function packageManagerFromManifest(cwd) {
74
+ try {
75
+ const raw = await readFile(join(cwd, "package.json"), "utf8");
76
+ const parsed = JSON.parse(raw);
77
+ return typeof parsed.packageManager === "string" ? packageManagerFromString(parsed.packageManager) : void 0;
78
+ } catch {
79
+ return;
80
+ }
81
+ }
82
+ function packageManagerFromString(value) {
83
+ const name = value.split("@")[0];
84
+ return name === "npm" || name === "pnpm" || name === "yarn" || name === "bun" ? name : void 0;
85
+ }
86
+ async function exists(path) {
87
+ try {
88
+ await stat(path);
89
+ return true;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ //#endregion
95
+ //#region src/commands/setup/install.ts
96
+ async function installDependenciesForSetup(input) {
97
+ const packageManager = await detectPackageManager(input.cwd);
98
+ const installCommand = installCommandFor(packageManager);
99
+ const devCommand = devCommandFor(packageManager);
100
+ if (!(input.scaffoldedFramework || input.depsAdded.length > 0)) return outcome({
101
+ install: {
102
+ status: "not-needed",
103
+ package_manager: packageManager,
104
+ command: installCommand.display,
105
+ reason: "no-dependency-changes"
106
+ },
107
+ devCommand: devCommand.display,
108
+ issuer: input.issuer,
109
+ includeInstallCommand: false
110
+ });
111
+ if (input.dryRun || input.skipInstall) return outcome({
112
+ install: {
113
+ status: "skipped",
114
+ package_manager: packageManager,
115
+ command: installCommand.display,
116
+ reason: input.dryRun ? "dry-run" : "skip-install"
117
+ },
118
+ devCommand: devCommand.display,
119
+ issuer: input.issuer,
120
+ includeInstallCommand: true
121
+ });
122
+ consola$1.start(`Installing dependencies with ${installCommand.display}`);
123
+ try {
124
+ await (input.run ?? runPackageCommand)(installCommand, {
125
+ cwd: input.cwd,
126
+ env: input.env,
127
+ redirectStdoutToStderr: input.json
128
+ });
129
+ } catch (error) {
130
+ throw installFailed(error, installCommand, devCommand, input.cwd);
131
+ }
132
+ consola$1.success("Installed dependencies");
133
+ return outcome({
134
+ install: {
135
+ status: "completed",
136
+ package_manager: packageManager,
137
+ command: installCommand.display
138
+ },
139
+ devCommand: devCommand.display,
140
+ issuer: input.issuer,
141
+ includeInstallCommand: false
142
+ });
143
+ }
144
+ function outcome(input) {
145
+ const startAction = `Start your project: ${input.devCommand} (then open ${input.issuer})`;
146
+ const verifyAction = "Verify auth in the browser: register a user, log out, log in again with the same user, and confirm /profile shows Signed in.";
147
+ return {
148
+ install: input.install,
149
+ devCommand: input.devCommand,
150
+ nextActions: input.includeInstallCommand ? [
151
+ `Install dependencies: ${input.install.command}`,
152
+ startAction,
153
+ verifyAction
154
+ ] : [startAction, verifyAction],
155
+ nextCommands: input.includeInstallCommand ? [input.install.command, input.devCommand] : [input.devCommand]
156
+ };
157
+ }
158
+ function installFailed(error, installCommand, devCommand, cwd) {
159
+ return new ZitadelError("E_VALIDATION", `Dependency install failed: ${installCommand.display}`, {
160
+ hint: `Run ${installCommand.display} in ${cwd}, then start the app with ${devCommand.display}.`,
161
+ nextCommands: [installCommand.display],
162
+ details: {
163
+ command: installCommand.display,
164
+ cwd,
165
+ original: errorShape(error)
166
+ }
167
+ });
168
+ }
169
+ function errorShape(error) {
170
+ if (error instanceof Error) return {
171
+ name: error.name,
172
+ message: error.message,
173
+ code: error.code
174
+ };
175
+ return { message: String(error) };
176
+ }
177
+ //#endregion
11
178
  //#region src/commands/setup/prompts/cancel.ts
12
179
  /**
13
180
  * Converts a clack cancellation (Ctrl-C) into a thrown `E_VALIDATION` rather
@@ -27,7 +194,8 @@ function bail(value) {
27
194
  * becomes the issuer URL (`http://localhost:<port>`) via `issuerFromPort`.
28
195
  */
29
196
  var DevPortPrompt = class {
30
- async ask(answers, _ctx) {
197
+ async ask(answers, ctx) {
198
+ if (ctx.devPortFromFlag) return answers;
31
199
  const value = await text({
32
200
  message: "Dev server port",
33
201
  placeholder: String(answers.devPort),
@@ -63,79 +231,6 @@ var FrameworkConfirmPrompt = class {
63
231
  }
64
232
  };
65
233
  //#endregion
66
- //#region src/lib/prober/ports.ts
67
- /**
68
- * Enumerate TCP ports currently in LISTEN state on the loopback interface
69
- * (`127.0.0.1`, `::1`, or the wildcard `*`). Spawns `lsof -iTCP -sTCP:LISTEN
70
- * -P -n -F n` and parses its machine-readable output. Returns the unique,
71
- * numerically-sorted list of ports.
72
- *
73
- * Never throws. Returns `[]` whenever lsof is unavailable (e.g. Windows,
74
- * unusual PATH), the spawn errors, exits non-zero, or the call exceeds
75
- * `timeoutMs` (default 1000ms). The caller treats an empty list the same as
76
- * "no listeners worth probing."
77
- */
78
- async function listListeningPorts(opts) {
79
- const timeoutMs = opts?.timeoutMs ?? 1e3;
80
- try {
81
- return parseLsofPorts(await runLsof(timeoutMs));
82
- } catch {
83
- return [];
84
- }
85
- }
86
- /**
87
- * Spawn `lsof` with the canned argv and resolve to its stdout. Hand-rolled
88
- * rather than `util.promisify(execFile)` because the latter resolves with a
89
- * `{stdout, stderr}` object via its custom-promisify symbol — a heavier shape
90
- * to mock and worse to read at the call site, where we only ever want stdout.
91
- */
92
- function runLsof(timeoutMs) {
93
- return new Promise((resolve, reject) => {
94
- execFile("lsof", [
95
- "-iTCP",
96
- "-sTCP:LISTEN",
97
- "-P",
98
- "-n",
99
- "-F",
100
- "n"
101
- ], {
102
- timeout: timeoutMs,
103
- encoding: "utf8"
104
- }, (err, stdout) => {
105
- if (err) {
106
- reject(err);
107
- return;
108
- }
109
- resolve(stdout);
110
- });
111
- });
112
- }
113
- /**
114
- * Parse the `n` records emitted by `lsof -F n`. Each record is a single line
115
- * `n<address>` where `<address>` ends in `:<port>` (e.g. `n*:8080`,
116
- * `n127.0.0.1:3000`, `n[::1]:5050`). Only loopback/wildcard hosts are kept;
117
- * external interface bindings are ignored.
118
- */
119
- function parseLsofPorts(stdout) {
120
- const ports = /* @__PURE__ */ new Set();
121
- for (const line of stdout.split(/\r?\n/)) {
122
- if (!line.startsWith("n")) continue;
123
- const address = line.slice(1);
124
- const colon = address.lastIndexOf(":");
125
- if (colon < 0) continue;
126
- const host = address.slice(0, colon);
127
- const portStr = address.slice(colon + 1);
128
- if (!isLoopback(host)) continue;
129
- const port = Number.parseInt(portStr, 10);
130
- if (Number.isFinite(port) && port > 0 && port < 65536) ports.add(port);
131
- }
132
- return [...ports].sort((a, b) => a - b);
133
- }
134
- /** Recognised loopback host strings as emitted by `lsof -F n`. */
135
- function isLoopback(host) {
136
- return host === "*" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
137
- }
138
- //#endregion
139
234
  //#region src/lib/prober/http.ts
140
235
  /**
141
236
  * Fetch `url` with a per-call timeout and pass the resulting `Response` to
@@ -345,9 +440,8 @@ const url = (s) => pc.cyan(s);
345
440
  const id = (s) => pc.yellow(s);
346
441
  /**
347
442
  * Reads the project root to identify the framework version, TS presence,
348
- * and which package manager the user runs. Returns the worst-case
349
- * (`"unknown"` PM, no version) on any error so summary rendering can
350
- * always proceed.
443
+ * and which package manager the user runs. Returns safe defaults (npm
444
+ * fallback, no version) on any error so summary rendering can always proceed.
351
445
  */
352
446
  async function detectProjectFacts(cwd, frameworkId) {
353
447
  const facts = {
@@ -370,16 +464,9 @@ function formatFrameworkLine(facts) {
370
464
  const pretty = prettyFramework(facts.framework);
371
465
  segments.push(facts.frameworkVersion ? `${pretty} ${facts.frameworkVersion}` : pretty);
372
466
  if (facts.typescript) segments.push("TypeScript");
373
- if (facts.packageManager !== "unknown") segments.push(facts.packageManager);
467
+ segments.push(facts.packageManager);
374
468
  return segments.join(` ${pc.dim("·")} `);
375
469
  }
376
- async function detectPackageManager(cwd) {
377
- if (await fileExists(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
378
- if (await fileExists(join(cwd, "yarn.lock"))) return "yarn";
379
- if (await fileExists(join(cwd, "bun.lockb"))) return "bun";
380
- if (await fileExists(join(cwd, "package-lock.json"))) return "npm";
381
- return "unknown";
382
- }
383
470
  async function fileExists(p) {
384
471
  try {
385
472
  await stat(p);
@@ -434,7 +521,7 @@ const FRAMEWORK_OPTIONS = createOrca().availableFrameworks().map((framework) =>
434
521
  */
435
522
  var Setup = class Setup extends BaseCommand {
436
523
  static description = "Create a Zitadel project and scaffold local auth.";
437
- static examples = ["<%= config.bin %> setup --framework next"];
524
+ static examples = ["<%= config.bin %> setup --framework next", "<%= config.bin %> setup --framework react --dev-port 3000"];
438
525
  static flags = {
439
526
  framework: Flags.string({
440
527
  description: "Framework to target.",
@@ -443,11 +530,17 @@ var Setup = class Setup extends BaseCommand {
443
530
  renderer: Flags.string({
444
531
  description: "Renderer (default: react).",
445
532
  options: [...RENDERER_IDS]
446
- })
533
+ }),
534
+ "dev-port": Flags.integer({ description: "Dev-server port; also the issuer origin registered with Zitadel. Defaults to the detected port. Use distinct ports to run several scaffolded apps side by side." }),
535
+ "skip-install": Flags.boolean({ description: "Do not install dependencies after setup updates package.json." })
447
536
  };
448
537
  async run() {
449
538
  const { flags } = await this.parse(Setup);
450
- await this.toMeta(flags);
539
+ try {
540
+ await this.toMeta(flags);
541
+ } catch (error) {
542
+ throw localSetupHint(error, flags.framework, this.config.version);
543
+ }
451
544
  const { cwd, nonInteractive, dryRun, force } = this.meta;
452
545
  if (await hasZitadelConfig(cwd)) return this.emit({
453
546
  status: "skipped",
@@ -462,13 +555,24 @@ var Setup = class Setup extends BaseCommand {
462
555
  framework = await orca.detect(cwd, flags.framework);
463
556
  consola$1.success(`Detected ${framework.id}${framework.devPort ? ` (dev port ${framework.devPort})` : ""}`);
464
557
  } catch (error) {
465
- if (error instanceof ZitadelError && error.code === "E_FRAMEWORK_NOT_DETECTED" && await orca.isEmpty(cwd)) {
466
- consola$1.info("Empty directory scaffolding a fresh project");
558
+ if (error instanceof ZitadelError && error.code === "E_FRAMEWORK_NOT_DETECTED") {
559
+ const target = await inspectScaffoldTarget(cwd);
560
+ if (!target.scaffoldable) throw frameworkDetectionWithScaffoldTarget(error, cwd, target);
561
+ consola$1.info("Fresh app directory — scaffolding a fresh project");
467
562
  framework = await orca.scaffold(cwd, await resolveScaffoldFramework(flags.framework, nonInteractive, orca));
468
563
  scaffoldedFramework = true;
469
564
  consola$1.success(`Scaffolded ${framework.id} skeleton`);
470
565
  } else throw error;
471
566
  }
567
+ if (flags["dev-port"] !== void 0) {
568
+ const devPort = flags["dev-port"];
569
+ if (!Number.isInteger(devPort) || devPort < 1 || devPort > 65535) throw new ZitadelError("E_VALIDATION", `--dev-port must be an integer in 1..65535, got ${devPort}`);
570
+ framework = {
571
+ ...framework,
572
+ devPort,
573
+ url: issuerFromPort(devPort)
574
+ };
575
+ }
472
576
  let answers = {
473
577
  server: this.meta.source,
474
578
  devPort: framework.devPort
@@ -477,22 +581,30 @@ var Setup = class Setup extends BaseCommand {
477
581
  intro("Zitadel setup");
478
582
  const promptCtx = {
479
583
  framework,
480
- serverFlag: this.meta.serverFlag
584
+ serverFlag: this.meta.serverFlag,
585
+ devPortFromFlag: flags["dev-port"] !== void 0
481
586
  };
482
587
  for (const prompt of SETUP_PROMPTS) answers = await prompt.ask(answers, promptCtx);
483
588
  outro("Configuration captured");
484
589
  }
485
590
  const issuer = issuerFromPort(answers.devPort);
591
+ framework = {
592
+ ...framework,
593
+ devPort: answers.devPort,
594
+ url: issuer
595
+ };
486
596
  consola$1.start(`Creating project on ${answers.server}${dryRun ? " (dry run)" : ""}`);
487
597
  const unauthClient = createZitadelClient({ baseUrl: answers.server });
488
- const project = dryRun ? dryRunProject() : await unauthClient.createProject({ previewOrigins: [] });
598
+ const project = dryRun ? dryRunProject(issuer) : await createProjectWithLocalHint(unauthClient, answers.server, this.meta.cliVersion, issuer, framework.id);
489
599
  consola$1.success(`Created project ${project.id}`);
490
600
  const ctx = {
491
601
  framework,
492
602
  rendererId: flags.renderer ?? "react",
493
603
  project,
494
604
  issuer,
495
- server: answers.server
605
+ server: answers.server,
606
+ cliVersion: this.meta.cliVersion,
607
+ scaffoldedFramework
496
608
  };
497
609
  consola$1.start(`Patching project files${dryRun ? " (dry run)" : ""}`);
498
610
  const result = await orca.patcherFor(framework.id).patch(ctx, {
@@ -506,6 +618,16 @@ var Setup = class Setup extends BaseCommand {
506
618
  }
507
619
  for (const file of result.filesSkipped) consola$1.info(`Left ${relativeDisplay(cwd, file)} unchanged (already matches target)`);
508
620
  consola$1.success(`Patched ${result.filesWritten.length} file${result.filesWritten.length === 1 ? "" : "s"}` + (result.filesSkipped.length > 0 ? ` (${result.filesSkipped.length} unchanged)` : ""));
621
+ const installOutcome = await installDependenciesForSetup({
622
+ cwd,
623
+ depsAdded: result.depsAdded,
624
+ dryRun,
625
+ env: this.meta.env,
626
+ issuer,
627
+ json: this.jsonEnabled(),
628
+ scaffoldedFramework,
629
+ skipInstall: Boolean(flags["skip-install"])
630
+ });
509
631
  const writtenRel = result.filesWritten.map((file) => relativeDisplay(cwd, file));
510
632
  if (!this.jsonEnabled()) {
511
633
  const sections = buildSummary({
@@ -521,7 +643,7 @@ var Setup = class Setup extends BaseCommand {
521
643
  message: [
522
644
  renderSummary(sections),
523
645
  "",
524
- `Open your app on ${url(`${issuer}/login`)} and register your first user.`
646
+ installOutcome.nextActions.join("\n")
525
647
  ].join("\n"),
526
648
  style: {
527
649
  padding: 1,
@@ -543,12 +665,23 @@ var Setup = class Setup extends BaseCommand {
543
665
  server: answers.server,
544
666
  files_written: result.filesWritten.map((file) => relativeDisplay(cwd, file)),
545
667
  files_skipped: result.filesSkipped.map((file) => relativeDisplay(cwd, file)),
546
- next_actions: [`Start your project: npm install && npm run dev (then open ${issuer}/login)`],
547
- next_commands: ["npm install", "npm run dev"]
668
+ install: installOutcome.install,
669
+ next_actions: installOutcome.nextActions,
670
+ next_commands: installOutcome.nextCommands
548
671
  }
549
672
  });
550
673
  }
551
674
  };
675
+ function frameworkDetectionWithScaffoldTarget(error, cwd, target) {
676
+ return new ZitadelError(error.code, "Could not detect a supported app framework, and this directory is not a fresh scaffold target", {
677
+ hint: `${target.reason ?? "Directory is not empty."} Run setup from an empty directory to scaffold a new app, or run setup from an existing supported app project.`,
678
+ details: {
679
+ cwd,
680
+ entries: target.entries,
681
+ reason: target.reason
682
+ }
683
+ });
684
+ }
552
685
  /**
553
686
  * Resolves which framework to scaffold into an empty directory: the explicit
554
687
  * `--framework`, else PickFrameworkPrompt, else a hard error in non-interactive
@@ -556,19 +689,44 @@ var Setup = class Setup extends BaseCommand {
556
689
  */
557
690
  async function resolveScaffoldFramework(framework, nonInteractive, orca) {
558
691
  if (framework) return framework;
559
- if (nonInteractive) throw new ZitadelError("E_FRAMEWORK_NOT_DETECTED", "Empty directory — pass --framework", { hint: "Example: --framework next" });
692
+ if (nonInteractive) throw new ZitadelError("E_FRAMEWORK_NOT_DETECTED", "Empty directory — pass --framework", { hint: "Run without --json/--non-interactive to choose from a prompt, or pass --framework for scripted setup." });
560
693
  return new PickFrameworkPrompt().ask(orca.availableFrameworks());
561
694
  }
562
695
  /** A deterministic stand-in project for `--dry-run`, so no remote call is made. */
563
- function dryRunProject() {
696
+ function dryRunProject(issuer) {
564
697
  return {
565
698
  id: "dry-run-0000",
566
699
  projectSecret: "sk_proj_dry_run_full",
567
700
  previewSecret: "sk_proj_dry_run_preview",
568
- previewOrigins: [],
701
+ previewOrigins: [issuer],
569
702
  createdAt: "2026-04-21T14:03:11.000Z"
570
703
  };
571
704
  }
705
+ async function createProjectWithLocalHint(client, server, cliVersion, issuer, framework) {
706
+ try {
707
+ return await client.createProject({ previewOrigins: [issuer] });
708
+ } catch (error) {
709
+ const normalized = toZitadelError(error);
710
+ throw new ZitadelError(normalized.code, normalized.message, {
711
+ hint: `${normalized.hint ? `${normalized.hint} ` : ""}If you meant to use a local Zitadel server, start it first and retry setup with --framework ${framework} --server local.`,
712
+ nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand(`setup --framework ${framework} --server local`, cliVersion)],
713
+ details: {
714
+ server,
715
+ original: normalized.details
716
+ }
717
+ });
718
+ }
719
+ }
720
+ function localSetupHint(error, framework, cliVersion) {
721
+ const normalized = toZitadelError(error);
722
+ if (normalized.code !== "E_LOCAL_SERVER_NOT_RUNNING") return error;
723
+ const setupCommand = framework ? `setup --framework ${framework} --server local` : "setup --server local";
724
+ return new ZitadelError(normalized.code, normalized.message, {
725
+ hint: `${normalized.hint ? `${normalized.hint} ` : ""}Start local Zitadel first, then rerun setup. After setup succeeds, follow its next_commands to start the app and verify registration, logout, and login in the browser.`,
726
+ nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand(setupCommand, cliVersion)],
727
+ details: normalized.details
728
+ });
729
+ }
572
730
  /** Renders an absolute path relative to `cwd` for human-readable output. */
573
731
  function relativeDisplay(cwd, path) {
574
732
  return path.startsWith(cwd) ? path.slice(cwd.length + 1) : path;
@@ -620,10 +778,12 @@ const SENTENCE_BY_PATH = {
620
778
  ".env.example": { subject: "the .env example template" },
621
779
  ".env.local": { subject: "the local development environment variables" },
622
780
  ".zitadel/state.json": { subject: "the empty sync state file" },
781
+ "app/page.tsx": { subject: "the auth home page" },
623
782
  "app/login/page.tsx": { subject: "the login page" },
624
783
  "app/register/page.tsx": { subject: "the registration page" },
625
784
  "app/profile/page.tsx": { subject: "the profile page" },
626
785
  "middleware.ts": { subject: "the Next.js middleware" },
786
+ "proxy.ts": { subject: "the Next.js proxy" },
627
787
  "custom-elements.d.ts": { subject: "the web-component type declarations" },
628
788
  "package.json": { subject: "package.json with the SDK dependency" }
629
789
  };
@@ -647,9 +807,11 @@ function buildSummary(opts) {
647
807
  secondary: path(fileNameOf(packageJsonHit))
648
808
  });
649
809
  for (const [label, suffix] of [
810
+ ["Home page", "app/page.tsx"],
650
811
  ["Login page", "app/login/page.tsx"],
651
812
  ["Register page", "app/register/page.tsx"],
652
813
  ["Profile page", "app/profile/page.tsx"],
814
+ ["Request proxy", "proxy.ts"],
653
815
  ["Middleware", "middleware.ts"],
654
816
  ["Env vars", ".env.local"]
655
817
  ]) {