@zitadel/cli 0.1.0-alpha.9 → 1.0.0-alpha.20

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 (66) hide show
  1. package/README.md +250 -62
  2. package/SKILLS.md +221 -37
  3. package/dist/branding-cygSBPkV.mjs +79 -0
  4. package/dist/branding-cygSBPkV.mjs.map +1 -0
  5. package/dist/claim-state-DlfD6Res.mjs +66 -0
  6. package/dist/claim-state-DlfD6Res.mjs.map +1 -0
  7. package/dist/commands/apply.mjs +27 -8
  8. package/dist/commands/apply.mjs.map +1 -1
  9. package/dist/commands/branding/eject.mjs +104 -0
  10. package/dist/commands/branding/eject.mjs.map +1 -0
  11. package/dist/commands/claim.mjs +402 -0
  12. package/dist/commands/claim.mjs.map +1 -0
  13. package/dist/commands/doctor.mjs +594 -41
  14. package/dist/commands/doctor.mjs.map +1 -1
  15. package/dist/commands/eject.mjs +18 -6
  16. package/dist/commands/eject.mjs.map +1 -1
  17. package/dist/commands/logs.mjs +2 -2
  18. package/dist/commands/plan.mjs +19 -6
  19. package/dist/commands/plan.mjs.map +1 -1
  20. package/dist/commands/reset.mjs +2 -2
  21. package/dist/commands/schemas/list.mjs +146 -0
  22. package/dist/commands/schemas/list.mjs.map +1 -0
  23. package/dist/commands/setup.mjs +629 -214
  24. package/dist/commands/setup.mjs.map +1 -1
  25. package/dist/commands/start.mjs +4 -4
  26. package/dist/commands/start.mjs.map +1 -1
  27. package/dist/commands/status.mjs +52 -13
  28. package/dist/commands/status.mjs.map +1 -1
  29. package/dist/commands/stop.mjs +3 -3
  30. package/dist/commands/stop.mjs.map +1 -1
  31. package/dist/designs-Ckz18Dpo.mjs +38 -0
  32. package/dist/designs-Ckz18Dpo.mjs.map +1 -0
  33. package/dist/{docker-CnGQK3ZK.mjs → docker-DcRGTOJa.mjs} +5 -4
  34. package/dist/docker-DcRGTOJa.mjs.map +1 -0
  35. package/dist/environment-rjRVkJjW.mjs +17 -0
  36. package/dist/environment-rjRVkJjW.mjs.map +1 -0
  37. package/dist/journey-guidance-BGrOX_gT.mjs +40 -0
  38. package/dist/journey-guidance-BGrOX_gT.mjs.map +1 -0
  39. package/dist/oclif-CanO3zdt.mjs +1769 -0
  40. package/dist/oclif-CanO3zdt.mjs.map +1 -0
  41. package/dist/{orca-U142Wrau.mjs → orca-BzQIQqzt.mjs} +1663 -303
  42. package/dist/orca-BzQIQqzt.mjs.map +1 -0
  43. package/dist/package-manager-DFdLcDx1.mjs +194 -0
  44. package/dist/package-manager-DFdLcDx1.mjs.map +1 -0
  45. package/dist/{ports-B09RjuHx.mjs → ports-BM20XIZb.mjs} +25 -20
  46. package/dist/ports-BM20XIZb.mjs.map +1 -0
  47. package/dist/{processes-Cw8TO1SY.mjs → processes-DKHPkU8O.mjs} +1 -1
  48. package/dist/{processes-Cw8TO1SY.mjs.map → processes-DKHPkU8O.mjs.map} +1 -1
  49. package/dist/{project-Cd0L3PtM.mjs → project-CHhot85s.mjs} +57 -4
  50. package/dist/project-CHhot85s.mjs.map +1 -0
  51. package/dist/sync-BBlACKzT.mjs +1633 -0
  52. package/dist/sync-BBlACKzT.mjs.map +1 -0
  53. package/dist/user-schema-DTuOsdKE.mjs +91 -0
  54. package/dist/user-schema-DTuOsdKE.mjs.map +1 -0
  55. package/oclif.manifest.json +406 -5
  56. package/package.json +10 -5
  57. package/dist/docker-CnGQK3ZK.mjs.map +0 -1
  58. package/dist/docker-guidance-ypN3IM3o.mjs +0 -21
  59. package/dist/docker-guidance-ypN3IM3o.mjs.map +0 -1
  60. package/dist/oclif-B7lBzh3R.mjs +0 -808
  61. package/dist/oclif-B7lBzh3R.mjs.map +0 -1
  62. package/dist/orca-U142Wrau.mjs.map +0 -1
  63. package/dist/ports-B09RjuHx.mjs.map +0 -1
  64. package/dist/project-Cd0L3PtM.mjs.map +0 -1
  65. package/dist/sync-BojoQm2P.mjs +0 -733
  66. package/dist/sync-BojoQm2P.mjs.map +0 -1
@@ -1,15 +1,18 @@
1
- import { C as isObject, E as ZitadelError, a as DEFAULT_LOCAL_SERVER_PORT, b as publicCliCommand, c as checkLocalServerHealth, l as defaultLocalServerImageForCliVersion, m as readRuntimeMetadata, p as localServerUrl, s as assertLocalStateWritable, t as BaseCommand } from "../oclif-B7lBzh3R.mjs";
2
- import { i as issuerFromPort, t as createOrca } from "../orca-U142Wrau.mjs";
3
- import { a as readZitadelConfig, i as readRendererId, o as readZitadelSecret, r as readDevelopmentIssuer, t as hasZitadelConfig } from "../project-Cd0L3PtM.mjs";
4
- import { n as listenersForPort } from "../ports-B09RjuHx.mjs";
5
- import { d as assertServerPackageAvailable, o as imageAvailable, r as dockerAvailable } from "../docker-CnGQK3ZK.mjs";
6
- import { n as dockerUnavailableMessage, t as dockerRuntimeGuidance } from "../docker-guidance-ypN3IM3o.mjs";
7
- import { t as discoverManagedRuntimeProcesses } from "../processes-Cw8TO1SY.mjs";
1
+ import { E as ZitadelError, S as isObject, a as DEFAULT_LOCAL_SERVER_PORT, c as checkLocalServerHealth, h as readRuntimeMetadata, j as publicCliCommand, l as defaultLocalServerImageForCliVersion, m as localServerUrl, s as assertLocalStateWritable, t as BaseCommand } from "../oclif-CanO3zdt.mjs";
2
+ import { n as claimCommand, r as claimState, t as claimAction } from "../claim-state-DlfD6Res.mjs";
3
+ import { s as issuerFromPort, t as createOrca } from "../orca-BzQIQqzt.mjs";
4
+ import { a as readProjectServer, c as readZitadelConfig, i as readPreset, l as readZitadelSecret, o as readRendererId, r as readDevelopmentIssuer, s as readUseCase, t as hasZitadelConfig } from "../project-CHhot85s.mjs";
5
+ import { i as updateScaffold, t as SCHEMAS_DIR } from "../user-schema-DTuOsdKE.mjs";
6
+ import { c as writeScaffoldManifest, n as detectPackageManager, o as hashScaffoldFile, s as readScaffoldManifest, t as addExactCommandFor } from "../package-manager-DFdLcDx1.mjs";
7
+ import { d as assertServerPackageAvailable, o as imageAvailable, r as dockerAvailable } from "../docker-DcRGTOJa.mjs";
8
+ import { n as dockerRuntimeGuidance, r as dockerUnavailableMessage, t as listenersForPort } from "../ports-BM20XIZb.mjs";
9
+ import { t as discoverManagedRuntimeProcesses } from "../processes-DKHPkU8O.mjs";
10
+ import { chmod, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
11
+ import { join } from "node:path";
8
12
  import { Flags } from "@oclif/core";
9
13
  import consola from "consola";
10
- import { join } from "node:path";
11
- import { chmod, readFile, stat, writeFile } from "node:fs/promises";
12
- import "@zitadel/api/generated/endpoints/zitadelNextGen.zod";
14
+ import semver from "semver";
15
+ import { schemaConfigSchema } from "@zitadel/config/schemas";
13
16
  //#region src/commands/doctor/checks/types.ts
14
17
  /**
15
18
  * Base class for checks: subclasses declare `name`, `path`, and a success
@@ -35,7 +38,11 @@ var AbstractSanityCheck = class {
35
38
  name: this.name,
36
39
  status: "fail",
37
40
  message: error instanceof Error ? error.message : String(error),
38
- path: this.path
41
+ path: this.path,
42
+ ...error instanceof ZitadelError ? {
43
+ code: error.code,
44
+ ...error.hint === void 0 ? {} : { hint: error.hint }
45
+ } : {}
39
46
  };
40
47
  }
41
48
  }
@@ -147,46 +154,47 @@ var FrameworkCheck = class extends AbstractSanityCheck {
147
154
  //#endregion
148
155
  //#region src/commands/doctor/patch-context.ts
149
156
  /**
150
- * Reconstructs a {@link PatchContext} from the on-disk project (config, secret)
151
- * plus fresh framework detection, so a patcher repair can rebuild its plan.
152
- * Used by the dependency check's `fix`, which reclaims the framework-specific
153
- * SDK package via `patcher.repair`. The user schema and flow definition are
154
- * server-owned and no longer scaffolded locally, so nothing here reads them.
157
+ * Reconstructs a {@link PatchContext} from the on-disk project (config,
158
+ * secret, scaffold manifest) plus fresh framework detection, so a patcher
159
+ * repair can rebuild its plan. Used by the doctor checks' `fix` paths.
160
+ * `preset`/`useCase` come back from `zitadel.json` and
161
+ * `scaffoldedFramework` from the scaffold manifest, so the rebuilt plan
162
+ * matches what setup originally emitted (a fresh-scaffolded app's home page
163
+ * is part of the plan again, for example). Schema and flow config are
164
+ * user-editable sync resources, not patcher inputs, so nothing here reads
165
+ * them.
155
166
  */
156
167
  async function loadPatchContext(cwd, orca, cliVersion) {
157
168
  const config = await readZitadelConfig(cwd);
158
169
  const secret = await readZitadelSecret(cwd);
159
170
  const framework = await orca.detect(cwd);
171
+ const scaffold = await readScaffoldManifest(cwd);
160
172
  return {
161
173
  framework,
162
174
  rendererId: readRendererId(config),
163
175
  issuer: await resolveIssuer(cwd, config, framework),
164
176
  server: typeof config.server === "string" ? config.server : "",
165
177
  cliVersion,
178
+ scaffoldedFramework: scaffold?.scaffolded_framework,
179
+ posture: scaffold?.posture ?? "page",
180
+ preset: readPreset(config),
181
+ useCase: readUseCase(config),
166
182
  project: {
167
183
  id: secret.project_id,
168
- projectSecret: secret.project_secret,
169
- previewSecret: secret.preview_secret,
170
- previewOrigins: secret.preview_origins,
171
- createdAt: secret.created_at
184
+ project_secret: secret.project_secret,
185
+ preview_secret: secret.preview_secret,
186
+ preview_origins: secret.preview_origins,
187
+ created_at: secret.created_at
172
188
  }
173
189
  };
174
190
  }
175
191
  async function resolveIssuer(cwd, config, facts) {
176
192
  const fromConfig = readDevelopmentIssuer(config);
177
193
  if (fromConfig && fromConfig.length > 0) return fromConfig;
178
- const state = await readState(cwd);
179
- if (typeof state?.dev_port === "number") return issuerFromPort(state.dev_port);
194
+ const scaffold = await readScaffoldManifest(cwd);
195
+ if (typeof scaffold?.dev_port === "number") return issuerFromPort(scaffold.dev_port);
180
196
  return facts.url;
181
197
  }
182
- async function readState(cwd) {
183
- try {
184
- const contents = await readFile(join(cwd, ".zitadel/state.json"), "utf8");
185
- return JSON.parse(contents);
186
- } catch {
187
- return;
188
- }
189
- }
190
198
  //#endregion
191
199
  //#region src/commands/doctor/checks/dependency.ts
192
200
  /**
@@ -204,23 +212,391 @@ var DependencyCheck = class extends AbstractSanityCheck {
204
212
  if (![...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})].some((name) => name.startsWith("@zitadel"))) throw new Error("no @zitadel* dependency found in package.json");
205
213
  }
206
214
  /**
207
- * Repairs by reclaiming the patcher's managed artifacts: rebuilds the
215
+ * Repairs by re-applying the patcher's managed artifacts: rebuilds the
208
216
  * `PatchContext` from disk and calls `patcher.repair`, which re-adds the
209
217
  * SDK dependency via its `add-dep` op. The exact package name is framework
210
218
  * + renderer specific and known only to the patcher (which deliberately
211
219
  * hides its file-op plan behind the family-neutral `Patcher` interface),
212
- * so going through `repair` is the only sanctioned path.
220
+ * so going through `repair` is the only sanctioned path. Missing-only:
221
+ * the repair restores absent managed files as a side effect but never
222
+ * overwrites an existing one, edited or not.
213
223
  */
214
224
  async fix(ctx) {
215
225
  const patchCtx = await loadPatchContext(ctx.cwd, ctx.orca, ctx.cliVersion);
216
226
  await ctx.orca.patcherFor(patchCtx.framework.id).repair(patchCtx, {
217
227
  cwd: ctx.cwd,
218
228
  dryRun: ctx.dryRun,
219
- force: true
229
+ force: false,
230
+ missingOnly: true
220
231
  });
221
232
  }
222
233
  };
223
234
  //#endregion
235
+ //#region src/commands/doctor/checks/dependency-version.ts
236
+ /**
237
+ * Verifies that exactly-pinned `@zitadel/*` dependencies match this CLI's own
238
+ * version. The packages publish as one lockstep train, and the scaffold pins
239
+ * the SDK at the CLI's version — but the CLI floats (`npx @zitadel/cli@alpha`)
240
+ * while the app's pin stays put, so a newer CLI can restore files or write
241
+ * guidance that references SDK entry points the older pinned SDK does not
242
+ * ship (e.g. a subpath export added a train later). A mismatch therefore
243
+ * *warns* with the exact remedy instead of leaving the skew to surface as a
244
+ * module-not-found in the app.
245
+ *
246
+ * Only exact semver pins are compared: ranges, dist-tags, and `file:`/
247
+ * `workspace:` specifiers express a deliberate resolution choice the check
248
+ * cannot second-guess.
249
+ *
250
+ * Implements {@link SanityCheck} directly: the abstract base's verify-throws
251
+ * contract can only express pass/fail, and a version skew is advisory — the
252
+ * app keeps working on its pinned train until the user upgrades.
253
+ */
254
+ var DependencyVersionCheck = class {
255
+ name = "dependency-version";
256
+ path = "package.json";
257
+ async run(ctx) {
258
+ const base = {
259
+ name: this.name,
260
+ path: this.path
261
+ };
262
+ if (semver.valid(ctx.cliVersion) === null) return {
263
+ ...base,
264
+ status: "pass",
265
+ message: `CLI version ${ctx.cliVersion} is not an exact release; skipping the pin comparison`
266
+ };
267
+ let pkg;
268
+ try {
269
+ pkg = JSON.parse(await readFile(join(ctx.cwd, "package.json"), "utf8"));
270
+ } catch {
271
+ return {
272
+ ...base,
273
+ status: "pass",
274
+ message: "no package.json to compare pins against"
275
+ };
276
+ }
277
+ const exact = Object.entries({
278
+ ...pkg.dependencies ?? {},
279
+ ...pkg.devDependencies ?? {}
280
+ }).filter(([name]) => name.startsWith("@zitadel/")).filter(([, version]) => semver.valid(version.trim()) !== null);
281
+ const mismatched = exact.filter(([, version]) => !semver.eq(version.trim(), ctx.cliVersion));
282
+ if (mismatched.length > 0) {
283
+ const remedy = addExactCommandFor(await detectPackageManager(ctx.cwd), mismatched.map(([name]) => `${name}@${ctx.cliVersion}`)).display;
284
+ return {
285
+ ...base,
286
+ status: "warn",
287
+ message: `${mismatched.map(([name, version]) => `${name}@${version}`).join(", ")} does not match this CLI's version (${ctx.cliVersion}); scaffolded files and guidance target the CLI's train — update with \`${remedy}\``,
288
+ details: {
289
+ mismatched: mismatched.map(([name, version]) => ({
290
+ name,
291
+ declared: version,
292
+ expected: ctx.cliVersion
293
+ })),
294
+ remedy_command: remedy
295
+ }
296
+ };
297
+ }
298
+ return {
299
+ ...base,
300
+ status: "pass",
301
+ message: exact.length > 0 ? `Zitadel package pins match the CLI version (${exact.length} compared)` : "no exactly-pinned Zitadel packages to compare"
302
+ };
303
+ }
304
+ /**
305
+ * No auto-fix: repair's dependency op is deliberately additive-only (it
306
+ * never rewrites a version the user declared), so the repair ships as the
307
+ * package-manager-aware install command in `details.remedy_command` — the
308
+ * doctor command surfaces it in the envelope's `next_commands` — with the
309
+ * same command quoted in the warn message for human readers.
310
+ */
311
+ async fix(_ctx) {}
312
+ };
313
+ //#endregion
314
+ //#region src/commands/doctor/checks/managed-files.ts
315
+ /**
316
+ * Verifies the scaffolded app files still exist: the ones `zitadel setup`
317
+ * wrote per the scaffold manifest in `.zitadel/state.json`, or — for apps
318
+ * scaffolded before the manifest existed — the patcher's current template
319
+ * expectations minus conditionally-written files. A missing *infrastructure*
320
+ * file (request boundary, provider, type declarations) breaks the auth
321
+ * integration and fails the check; a missing *presentation* page only warns.
322
+ * Edited and user-adopted files pass — they are the intended customization
323
+ * paths — and are labelled in `details` for visibility.
324
+ *
325
+ * Implements {@link SanityCheck} directly rather than via the abstract base:
326
+ * the base's `verify`-throws contract can only express pass/fail, and this
327
+ * check needs the `warn` outcome.
328
+ */
329
+ var ManagedFilesCheck = class {
330
+ name = "managed-files";
331
+ async run(ctx) {
332
+ const manifest = await readScaffoldManifest(ctx.cwd);
333
+ const evaluated = manifest ? await evaluateManifest(ctx.cwd, manifest) : await evaluateTemplates(ctx);
334
+ if (!evaluated) return {
335
+ name: this.name,
336
+ status: "warn",
337
+ message: "Could not enumerate scaffolded app files (framework or renderer unresolved); skipping the file check"
338
+ };
339
+ const { mode, rows } = evaluated;
340
+ const wiring = await probeConfigWiring(ctx);
341
+ const actions = await resolveArtifacts(ctx);
342
+ const boundaries = actions ? await detectBoundaryConflicts(actions, ctx.cwd, manifest) : [];
343
+ const missing = rows.filter((row) => row.state === "missing");
344
+ const missingInfrastructure = missing.filter((row) => row.class === "infrastructure");
345
+ const detachedInfrastructure = wiring.filter((s) => s.state === "detached" && s.wiring === "infrastructure");
346
+ const detachedConvenience = wiring.filter((s) => s.state === "detached" && s.wiring === "convenience");
347
+ const unverifiable = wiring.filter((s) => s.state === "unknown");
348
+ const conflicts = boundaries.filter((b) => b.disposition === "conflict");
349
+ const removable = boundaries.filter((b) => b.disposition === "removable");
350
+ const details = {
351
+ mode,
352
+ files: rows,
353
+ ...wiring.length > 0 ? { config_wiring: wiring } : {},
354
+ ...boundaries.length > 0 ? { boundary_conflicts: boundaries } : {}
355
+ };
356
+ const failures = [];
357
+ if (missingInfrastructure.length > 0) failures.push(`missing scaffolded infrastructure file(s): ${listPaths(missingInfrastructure)}`);
358
+ if (detachedInfrastructure.length > 0) failures.push(`detached managed config wiring: ${detachedInfrastructure.map((s) => s.path).join(", ")}`);
359
+ for (const conflict of conflicts) failures.push(`conflicting request boundaries: ${conflict.retired} is user-modified while the current templates use ${conflict.current} — migrate your changes and remove one of them manually`);
360
+ if (failures.length > 0) return {
361
+ name: this.name,
362
+ status: "fail",
363
+ message: failures.join("; "),
364
+ details
365
+ };
366
+ const warnings = [];
367
+ if (missing.length > 0) warnings.push(`missing scaffolded page(s): ${listPaths(missing)}`);
368
+ if (detachedConvenience.length > 0) warnings.push(`unapplied managed config edit(s): ${detachedConvenience.map((s) => s.path).join(", ")}`);
369
+ if (unverifiable.length > 0) warnings.push(`unverifiable managed config wiring (manual review required): ${unverifiable.map((s) => s.reason ? `${s.path} (${s.reason})` : s.path).join(", ")}`);
370
+ for (const leftover of removable) warnings.push(`retired boundary ${leftover.retired} left over from a template migration (doctor --fix removes it and installs ${leftover.current})`);
371
+ if (warnings.length > 0) return {
372
+ name: this.name,
373
+ status: "warn",
374
+ message: warnings.join("; "),
375
+ details
376
+ };
377
+ return {
378
+ name: this.name,
379
+ status: "pass",
380
+ message: `Scaffolded app files are present (${rows.length} tracked${wiring.length > 0 ? `, ${wiring.length} wiring${wiring.length === 1 ? "" : "s"} verified` : ""}${mode === "template" ? ", from templates" : ""})`,
381
+ details
382
+ };
383
+ }
384
+ /**
385
+ * Restores missing managed files via the patcher's missing-only repair —
386
+ * existing files, edited or adopted, are never overwritten — then refreshes
387
+ * the manifest hashes of the files the repair brought back so they classify
388
+ * as pristine again. The restored bytes come from the *current* templates,
389
+ * which may differ from what the original CLI version wrote; the re-hash
390
+ * records that honestly. On a pre-manifest app (template mode) a successful
391
+ * repair also materializes the manifest from the marker-bearing files now
392
+ * on disk, completing the migration ADR 042 promises — adopted files stay
393
+ * the user's and are not recorded.
394
+ */
395
+ async fix(ctx) {
396
+ const manifest = await readScaffoldManifest(ctx.cwd);
397
+ const missingBefore = manifest ? await missingManifestPaths(ctx.cwd, manifest) : [];
398
+ const patchCtx = await loadPatchContext(ctx.cwd, ctx.orca, ctx.cliVersion);
399
+ const patcher = ctx.orca.patcherFor(patchCtx.framework.id);
400
+ const actions = patcher.artifacts({
401
+ framework: patchCtx.framework,
402
+ rendererId: patchCtx.rendererId
403
+ });
404
+ const boundaries = await detectBoundaryConflicts(actions, ctx.cwd, manifest);
405
+ const excludePaths = boundaries.filter((b) => b.disposition === "conflict").map((b) => b.current);
406
+ const removed = [];
407
+ if (!ctx.dryRun) {
408
+ for (const boundary of boundaries) if (boundary.disposition === "removable") {
409
+ await rm(join(ctx.cwd, boundary.retired), { force: true });
410
+ removed.push(boundary.retired);
411
+ }
412
+ }
413
+ await patcher.repair(patchCtx, {
414
+ cwd: ctx.cwd,
415
+ dryRun: ctx.dryRun,
416
+ force: false,
417
+ missingOnly: true,
418
+ ...excludePaths.length > 0 ? { excludePaths } : {}
419
+ });
420
+ if (ctx.dryRun) return;
421
+ if (!manifest) {
422
+ await materializeManifest(ctx, patchCtx);
423
+ return;
424
+ }
425
+ if (missingBefore.length === 0 && removed.length === 0) return;
426
+ const current = new Set(actions.markedFiles);
427
+ const conditional = new Set(actions.conditionalFiles ?? []);
428
+ const files = {};
429
+ for (const [path, entry] of Object.entries(manifest.files)) {
430
+ const contents = await readIfExists(join(ctx.cwd, path));
431
+ if (contents === void 0 && !current.has(path)) continue;
432
+ files[path] = contents !== void 0 && missingBefore.includes(path) ? {
433
+ ...entry,
434
+ hash: hashScaffoldFile(contents)
435
+ } : entry;
436
+ }
437
+ for (const path of actions.markedFiles) {
438
+ if (files[path] || conditional.has(path)) continue;
439
+ const contents = await readIfExists(join(ctx.cwd, path));
440
+ if (contents !== void 0 && contents.includes("// zitadel-cli: managed-file v1")) files[path] = {
441
+ hash: hashScaffoldFile(contents),
442
+ class: actions.fileClasses?.[path] ?? "presentation"
443
+ };
444
+ }
445
+ await updateScaffold(ctx.cwd, {
446
+ ...manifest,
447
+ files
448
+ });
449
+ }
450
+ };
451
+ async function detectBoundaryConflicts(actions, cwd, manifest) {
452
+ const conflicts = [];
453
+ for (const [current, retiredPaths] of Object.entries(actions.retiredAlternates ?? {})) for (const retired of retiredPaths) {
454
+ const contents = await readIfExists(join(cwd, retired));
455
+ if (contents === void 0) continue;
456
+ const entry = manifest?.files[retired];
457
+ const pristine = entry !== void 0 && contents.includes("// zitadel-cli: managed-file v1") && hashScaffoldFile(contents) === entry.hash;
458
+ conflicts.push({
459
+ current,
460
+ retired,
461
+ disposition: pristine ? "removable" : "conflict"
462
+ });
463
+ }
464
+ return conflicts;
465
+ }
466
+ /**
467
+ * Probe the patcher's managed config wirings (dev proxy merges, route
468
+ * registrations). Needs the full patch context (project secret et al) to
469
+ * rebuild the plan; when that fails — missing secret, undetectable
470
+ * framework — the probe is skipped silently and the responsible check
471
+ * reports the real problem.
472
+ */
473
+ async function probeConfigWiring(ctx) {
474
+ try {
475
+ const patchCtx = await loadPatchContext(ctx.cwd, ctx.orca, ctx.cliVersion);
476
+ return await ctx.orca.patcherFor(patchCtx.framework.id).verify(patchCtx, { cwd: ctx.cwd });
477
+ } catch {
478
+ return [];
479
+ }
480
+ }
481
+ /**
482
+ * Migration path for apps scaffolded before the manifest existed: after a
483
+ * template-mode repair, record the marker-bearing managed files now on disk
484
+ * so the next doctor run gets exact manifest-mode verification instead of
485
+ * template guessing (and its Next-major / template-growth misclassification
486
+ * risk). Marker-less (adopted) files and conditionally-scaffolded ones are
487
+ * deliberately absent.
488
+ *
489
+ * The manifest is only finalized when every current *infrastructure* file is
490
+ * present and recordable (marker-bearing): materializing during an
491
+ * unresolved boundary conflict — or over an adopted boundary — would write a
492
+ * manifest that tracks no request boundary at all, and deleting the boundary
493
+ * later would go undetected. Until then the app stays on the template
494
+ * fallback, whose presence checks keep enforcing the boundary. Best-effort:
495
+ * an unreadable state file leaves the app in template mode, same as before.
496
+ */
497
+ async function materializeManifest(ctx, patchCtx) {
498
+ try {
499
+ const actions = ctx.orca.patcherFor(patchCtx.framework.id).artifacts({
500
+ framework: patchCtx.framework,
501
+ rendererId: patchCtx.rendererId
502
+ });
503
+ const conditional = new Set(actions.conditionalFiles ?? []);
504
+ const marked = [];
505
+ for (const path of actions.markedFiles) {
506
+ if (conditional.has(path)) continue;
507
+ const contents = await readIfExists(join(ctx.cwd, path));
508
+ if (contents !== void 0 && contents.includes("// zitadel-cli: managed-file v1")) marked.push(path);
509
+ }
510
+ if (!actions.markedFiles.filter((path) => actions.fileClasses?.[path] === "infrastructure" && !conditional.has(path)).every((path) => marked.includes(path))) return;
511
+ await writeScaffoldManifest({
512
+ cwd: ctx.cwd,
513
+ actions,
514
+ written: marked
515
+ });
516
+ } catch {}
517
+ }
518
+ async function evaluateManifest(cwd, manifest) {
519
+ const rows = [];
520
+ for (const [path, entry] of Object.entries(manifest.files)) {
521
+ const contents = await readIfExists(join(cwd, path));
522
+ rows.push({
523
+ path,
524
+ class: entry.class,
525
+ state: stateFor(contents, entry.hash)
526
+ });
527
+ }
528
+ return {
529
+ mode: "manifest",
530
+ rows
531
+ };
532
+ }
533
+ /**
534
+ * Fallback for apps scaffolded before the manifest existed: derive the
535
+ * expected set from the patcher's current templates. Conditionally-written
536
+ * files (e.g. the framework home page) are excluded — without a manifest
537
+ * there is no way to know whether setup ever wrote them. Presence-only:
538
+ * no recorded hash means `pristine` vs `edited` cannot be told apart.
539
+ */
540
+ async function evaluateTemplates(ctx) {
541
+ const actions = await resolveArtifacts(ctx);
542
+ if (!actions) return;
543
+ const conditional = new Set(actions.conditionalFiles ?? []);
544
+ const rows = [];
545
+ for (const path of actions.markedFiles) {
546
+ if (conditional.has(path)) continue;
547
+ const contents = await readIfExists(join(ctx.cwd, path));
548
+ rows.push({
549
+ path,
550
+ class: actions.fileClasses?.[path] ?? "presentation",
551
+ state: stateFor(contents, void 0)
552
+ });
553
+ }
554
+ return {
555
+ mode: "template",
556
+ rows
557
+ };
558
+ }
559
+ /**
560
+ * Resolves the patcher artifacts the same way `eject` does, tolerating every
561
+ * failure with `undefined`: a broken framework detection, an unreadable
562
+ * config, or an unresolvable renderer are reported by their own checks —
563
+ * this one only degrades to "cannot enumerate".
564
+ */
565
+ async function resolveArtifacts(ctx) {
566
+ const framework = await ctx.orca.tryDetect(ctx.cwd);
567
+ if (!framework) return;
568
+ try {
569
+ const config = await readZitadelConfig(ctx.cwd).catch(() => ({}));
570
+ return ctx.orca.patcherFor(framework.id).artifacts({
571
+ framework,
572
+ rendererId: readRendererId(config)
573
+ });
574
+ } catch {
575
+ return;
576
+ }
577
+ }
578
+ function stateFor(contents, hash) {
579
+ if (contents === void 0) return "missing";
580
+ if (!contents.includes("// zitadel-cli: managed-file v1")) return "adopted";
581
+ if (hash === void 0) return "managed";
582
+ return hashScaffoldFile(contents) === hash ? "pristine" : "edited";
583
+ }
584
+ async function missingManifestPaths(cwd, manifest) {
585
+ const missing = [];
586
+ for (const path of Object.keys(manifest.files)) if (await readIfExists(join(cwd, path)) === void 0) missing.push(path);
587
+ return missing;
588
+ }
589
+ async function readIfExists(path) {
590
+ try {
591
+ return await readFile(path, "utf8");
592
+ } catch {
593
+ return;
594
+ }
595
+ }
596
+ function listPaths(rows) {
597
+ return rows.map((row) => row.path).join(", ");
598
+ }
599
+ //#endregion
224
600
  //#region src/commands/doctor/checks/project-match.ts
225
601
  /** Verifies `.zitadel/secret`'s project_id matches `zitadel.json`'s project. */
226
602
  var ProjectMatchCheck = class extends AbstractSanityCheck {
@@ -235,6 +611,137 @@ var ProjectMatchCheck = class extends AbstractSanityCheck {
235
611
  }
236
612
  };
237
613
  //#endregion
614
+ //#region src/lib/json-dir.ts
615
+ /**
616
+ * Read every `*.json` file under `absDir` and return each body as a plain
617
+ * object. Files are read in lexical filename order so the result is
618
+ * deterministic for downstream hashing and diffs. Each file is parsed by
619
+ * `JSON.parse`; this function does **not** apply any schema validation —
620
+ * unknown keys, forward-compatible fields, and `${VAR}` placeholders survive
621
+ * intact. Callers that need a typed shape pass the result through their
622
+ * resource's Zod schema.
623
+ *
624
+ * Returns an empty array when the directory is missing. Throws `E_VALIDATION`
625
+ * `ZitadelError` on invalid JSON or when a file's root value is not an object.
626
+ *
627
+ * @param absDir - Absolute path to the directory to scan.
628
+ */
629
+ async function readJsonDir(absDir) {
630
+ let entries;
631
+ try {
632
+ entries = await readdir(absDir);
633
+ } catch (error) {
634
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return [];
635
+ throw error;
636
+ }
637
+ const result = [];
638
+ for (const entry of entries.filter((name) => name.endsWith(".json")).sort()) {
639
+ const abs = join(absDir, entry);
640
+ let raw;
641
+ try {
642
+ raw = JSON.parse(await readFile(abs, "utf8"));
643
+ } catch {
644
+ throw new ZitadelError("E_VALIDATION", `${entry} is not valid JSON`);
645
+ }
646
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new ZitadelError("E_VALIDATION", `${entry} must contain a JSON object at the root`);
647
+ result.push(raw);
648
+ }
649
+ return result;
650
+ }
651
+ //#endregion
652
+ //#region src/commands/doctor/checks/schema.ts
653
+ /**
654
+ * Verifies local schema config files round-trip through the canonical
655
+ * `schemaConfigSchema` from `@zitadel/config/schemas` — the same Zod the
656
+ * sync engine validates with, so doctor and `apply` can never disagree
657
+ * about a file. The union enforces the `kind` discriminator
658
+ * (`user-schema` / `schema-url`) and the `metaSchema`/`x-auth-methods`/etc.
659
+ * fields the platform requires.
660
+ */
661
+ var SchemaCheck = class extends AbstractSanityCheck {
662
+ name = "schema";
663
+ path = SCHEMAS_DIR;
664
+ summary = "User schemas are valid Zitadel schema bodies";
665
+ /**
666
+ * An empty `.zitadel/schemas/` is a warning, not a failure: projects
667
+ * created before editable config existed were seeded server-side and have
668
+ * no local schema files, and there is no safe auto-fix that could invent
669
+ * them. Only invalid files fail the check.
670
+ */
671
+ async run(ctx) {
672
+ if ((await readJsonDir(join(ctx.cwd, ".zitadel/schemas"))).length === 0) return {
673
+ name: this.name,
674
+ status: "warn",
675
+ message: `No schema files found in ${SCHEMAS_DIR} — this project predates editable schema config or setup was interrupted. New projects scaffold editable defaults via \`zitadel setup\`.`,
676
+ path: this.path
677
+ };
678
+ return super.run(ctx);
679
+ }
680
+ async verify(ctx) {
681
+ const schemas = await readJsonDir(join(ctx.cwd, SCHEMAS_DIR));
682
+ for (const schema of schemas) {
683
+ const result = schemaConfigSchema.safeParse(schema);
684
+ if (!result.success) throw new Error(result.error.issues.map((issue) => `${issue.path.join(".") || "/"} ${issue.message}`).join("; "));
685
+ }
686
+ }
687
+ };
688
+ //#endregion
689
+ //#region src/commands/doctor/checks/claim.ts
690
+ /**
691
+ * Reports whether the project is attached to a team, reading `claimed_at` and
692
+ * `team_id` from `.zitadel/secret`.
693
+ *
694
+ * Always advisory, never a failure. An unattached project works exactly like an
695
+ * attached one — same issuer, same users, same applications — so the only thing
696
+ * missing is durability, which is the developer's call and not a defect. That
697
+ * matters mechanically too: `doctor` turns any `fail` into a thrown
698
+ * `E_VALIDATION`, so failing here would break every scripted `zitadel doctor`
699
+ * against a project nobody had claimed yet.
700
+ *
701
+ * Implements {@link SanityCheck} directly rather than via the abstract base,
702
+ * for the same reason `ManagedFilesCheck` does: the base's `verify`-throws
703
+ * contract can only express pass/fail, and this check is only ever warn or
704
+ * pass.
705
+ */
706
+ var ClaimCheck = class {
707
+ name = "claim";
708
+ path = ".zitadel/secret";
709
+ async run(ctx) {
710
+ let state;
711
+ try {
712
+ state = claimState({
713
+ secret: await readZitadelSecret(ctx.cwd),
714
+ server: readProjectServer(await readZitadelConfig(ctx.cwd))
715
+ });
716
+ } catch {
717
+ return {
718
+ name: this.name,
719
+ status: "pass",
720
+ message: "Skipped: could not read the project files that record the owning team",
721
+ path: this.path
722
+ };
723
+ }
724
+ if (state.kind === "detached") return {
725
+ name: this.name,
726
+ status: "warn",
727
+ message: "This project is temporary until you attach it to a team. Run `zitadel claim` to make it permanent; nothing about the project changes.",
728
+ path: this.path
729
+ };
730
+ return {
731
+ name: this.name,
732
+ status: "pass",
733
+ message: state.kind === "attached" ? `Project is attached to team ${state.team_id}` : "Project is not on a server where teams apply",
734
+ path: this.path
735
+ };
736
+ }
737
+ /**
738
+ * Deliberately a no-op. `doctor --fix` calls `fix` on every non-passing
739
+ * check, but claiming requires a human to sign in through a browser, so there
740
+ * is nothing safe to automate here.
741
+ */
742
+ async fix(_ctx) {}
743
+ };
744
+ //#endregion
238
745
  //#region src/commands/doctor/checks/index.ts
239
746
  /** Every diagnostic the `doctor` command runs, in display order. */
240
747
  const SANITY_CHECKS = [
@@ -244,8 +751,12 @@ const SANITY_CHECKS = [
244
751
  new GitignoreCheck(),
245
752
  new EnvExampleCheck(),
246
753
  new FrameworkCheck(),
754
+ new SchemaCheck(),
247
755
  new DependencyCheck(),
248
- new ProjectMatchCheck()
756
+ new DependencyVersionCheck(),
757
+ new ManagedFilesCheck(),
758
+ new ProjectMatchCheck(),
759
+ new ClaimCheck()
249
760
  ];
250
761
  //#endregion
251
762
  //#region src/commands/doctor/index.ts
@@ -262,8 +773,9 @@ const LOCAL_RUNTIME_CHECK_NAMES = new Set([
262
773
  *
263
774
  * Runs every registered {@link SANITY_CHECKS} entry and emits the aggregate
264
775
  * result; if any check fails it throws `E_VALIDATION` carrying the full check
265
- * details. With `--fix`, each failing check first attempts its own repair (a
266
- * no-op for checks with no safe automatic remedy), then the battery re-runs.
776
+ * details. With `--fix`, each check that did not pass failed or warned —
777
+ * first attempts its own repair (a no-op for checks with no safe automatic
778
+ * remedy), then the battery re-runs.
267
779
  *
268
780
  * The `--fix` loop is best-effort: a repair that throws (e.g. a missing
269
781
  * prerequisite file the check itself would also flag) is logged at debug
@@ -273,7 +785,7 @@ const LOCAL_RUNTIME_CHECK_NAMES = new Set([
273
785
  var Doctor = class Doctor extends BaseCommand {
274
786
  static description = "Verify local runtime and project state.";
275
787
  static flags = {
276
- fix: Flags.boolean({ description: "Re-apply missing managed files." }),
788
+ fix: Flags.boolean({ description: "Repair missing files and stale managed wiring." }),
277
789
  image: Flags.string({ description: "Container image to check." }),
278
790
  port: Flags.integer({
279
791
  description: "Local HTTP port.",
@@ -311,7 +823,7 @@ var Doctor = class Doctor extends BaseCommand {
311
823
  if (hasConfig && flags.fix) {
312
824
  const before = await Promise.all(SANITY_CHECKS.map((check) => check.run(ctx)));
313
825
  for (const [index, check] of SANITY_CHECKS.entries()) {
314
- if (before[index]?.status !== "fail") continue;
826
+ if (before[index]?.status === "pass") continue;
315
827
  try {
316
828
  await check.fix(ctx);
317
829
  } catch (error) {
@@ -324,6 +836,13 @@ var Doctor = class Doctor extends BaseCommand {
324
836
  const failed = checks.filter((check) => check.status === "fail");
325
837
  const warnings = checks.filter((check) => check.status === "warn");
326
838
  const warningAdvice = advisoryForWarnings(warnings, this.meta.cliVersion);
839
+ this.recordTelemetry({
840
+ runtime: runtimeBackend,
841
+ checks_total: checks.length,
842
+ checks_failed: failed.length,
843
+ checks_warn: warnings.length,
844
+ failed_checks: failed.length > 0 ? failed.map((check) => check.name).join(",") : void 0
845
+ });
327
846
  const data = {
328
847
  title: failed.length > 0 ? "Zitadel doctor found issues." : warnings.length > 0 ? "Zitadel doctor passed with warnings." : "Zitadel doctor passed.",
329
848
  ok: failed.length === 0,
@@ -339,7 +858,7 @@ var Doctor = class Doctor extends BaseCommand {
339
858
  };
340
859
  if (failed.length > 0) {
341
860
  const advice = failureAdvice(failed, image, port, this.meta.cliVersion);
342
- throw new ZitadelError(failed.some((check) => check.name === "port") ? "E_PORT_IN_USE" : "E_VALIDATION", "Zitadel doctor found issues", {
861
+ throw new ZitadelError(failed.some((check) => check.name === "port") ? "E_PORT_IN_USE" : failed.find((check) => check.code !== void 0)?.code ?? "E_VALIDATION", "Zitadel doctor found issues", {
343
862
  hint: advice.hint,
344
863
  nextCommands: advice.nextCommands,
345
864
  details: data
@@ -375,15 +894,27 @@ function failureAdvice(failed, image, port, cliVersion) {
375
894
  };
376
895
  if (failedNames.has("port")) {
377
896
  const fallbackPort = port === 8080 ? port + 1 : DEFAULT_LOCAL_SERVER_PORT;
897
+ const stopCommand = publicCliCommand("stop --all", cliVersion);
898
+ const retryCommand = publicCliCommand("doctor", cliVersion);
899
+ const alternatePortCommand = publicCliCommand(`doctor --port ${String(fallbackPort)}`, cliVersion);
378
900
  return {
379
- hint: `Port ${String(port)} is already in use. Stop the process using it, or choose another port for local Zitadel.`,
380
- nextCommands: [publicCliCommand(`doctor --port ${String(fallbackPort)}`, cliVersion)]
901
+ hint: `Port ${String(port)} is already in use. Stop the process using it, run \`${stopCommand}\` for CLI-managed local runtimes, then rerun \`${retryCommand}\`; or choose another port with \`${alternatePortCommand}\`.`,
902
+ nextCommands: [
903
+ stopCommand,
904
+ retryCommand,
905
+ alternatePortCommand
906
+ ]
381
907
  };
382
908
  }
383
909
  if (failedNames.has("runtime")) return {
384
910
  hint: "Existing local runtime metadata was found, but the local Zitadel server is not healthy. Start it again or reset stale local data.",
385
911
  nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand("reset --force", cliVersion)]
386
912
  };
913
+ const typed = failed.find((check) => check.code !== void 0 && check.hint !== void 0);
914
+ if (typed?.hint !== void 0) return {
915
+ hint: typed.hint,
916
+ nextCommands: [publicCliCommand("doctor", cliVersion)]
917
+ };
387
918
  if (failed.some((check) => !LOCAL_RUNTIME_CHECK_NAMES.has(check.name))) return {
388
919
  hint: `Run \`${publicCliCommand("doctor --fix", cliVersion)}\` to re-apply missing managed files.`,
389
920
  nextCommands: [publicCliCommand("doctor --fix", cliVersion)]
@@ -401,10 +932,19 @@ function advisoryForWarnings(warnings, cliVersion) {
401
932
  nextActions.push(...advice.nextActions);
402
933
  nextCommands.push(...advice.nextCommands);
403
934
  }
935
+ if (warnings.some((check) => check.name === "claim")) {
936
+ nextActions.push(claimAction(cliVersion));
937
+ nextCommands.push(claimCommand(cliVersion));
938
+ }
404
939
  if (hasManagedRuntimeProcesses(warnings.find((check) => check.name === "managed-runtime-processes"))) {
405
940
  nextActions.push("Review other host-wide CLI-managed local Zitadel runtimes before starting a new one.");
406
941
  nextCommands.push(publicCliCommand("stop --all", cliVersion));
407
942
  }
943
+ const dependencyRemedy = remedyCommandOf(warnings.find((check) => check.name === "dependency-version"));
944
+ if (dependencyRemedy !== void 0) {
945
+ nextActions.push("Align the exactly-pinned @zitadel/* dependencies with the CLI version; scaffolded files and guidance target the CLI's train.");
946
+ nextCommands.push(dependencyRemedy);
947
+ }
408
948
  if (nextActions.length === 0 && nextCommands.length === 0) return;
409
949
  return {
410
950
  nextActions: unique(nextActions),
@@ -520,6 +1060,19 @@ async function checkRuntime(runtime, runtimeBackend) {
520
1060
  function formatListeners(listeners) {
521
1061
  return listeners.map((listener) => [listener.command ?? "unknown", listener.pid ? `pid ${String(listener.pid)}` : void 0].filter(Boolean).join(" ")).join(", ");
522
1062
  }
1063
+ /**
1064
+ * A warning may carry its own repair as `details.remedy_command` (today: the
1065
+ * dependency-version check's package-manager-aware exact-pin install).
1066
+ * Surfacing that string keeps the structured advisory identical to the
1067
+ * command quoted in the warning's prose.
1068
+ */
1069
+ function remedyCommandOf(check) {
1070
+ if (check?.status !== "warn") return;
1071
+ const details = check.details;
1072
+ if (typeof details !== "object" || details === null) return;
1073
+ const remedy = details.remedy_command;
1074
+ return typeof remedy === "string" && remedy.length > 0 ? remedy : void 0;
1075
+ }
523
1076
  function hasManagedRuntimeProcesses(check) {
524
1077
  if (!check || check.status !== "warn") return false;
525
1078
  const details = check.details;