@zitadel/cli 0.1.0-alpha.16 → 0.1.0-alpha.18

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 (62) hide show
  1. package/README.md +145 -107
  2. package/SKILLS.md +98 -13
  3. package/dist/branding-BXj028-W.mjs +79 -0
  4. package/dist/branding-BXj028-W.mjs.map +1 -0
  5. package/dist/claim-state-BTGInAaN.mjs +66 -0
  6. package/dist/claim-state-BTGInAaN.mjs.map +1 -0
  7. package/dist/commands/apply.mjs +12 -7
  8. package/dist/commands/apply.mjs.map +1 -1
  9. package/dist/commands/branding/eject.mjs +125 -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 +477 -38
  14. package/dist/commands/doctor.mjs.map +1 -1
  15. package/dist/commands/eject.mjs +3 -3
  16. package/dist/commands/logs.mjs +2 -2
  17. package/dist/commands/plan.mjs +7 -5
  18. package/dist/commands/plan.mjs.map +1 -1
  19. package/dist/commands/reset.mjs +2 -2
  20. package/dist/commands/schemas/list.mjs +14 -10
  21. package/dist/commands/schemas/list.mjs.map +1 -1
  22. package/dist/commands/setup.mjs +331 -120
  23. package/dist/commands/setup.mjs.map +1 -1
  24. package/dist/commands/start.mjs +3 -4
  25. package/dist/commands/start.mjs.map +1 -1
  26. package/dist/commands/status.mjs +21 -13
  27. package/dist/commands/status.mjs.map +1 -1
  28. package/dist/commands/stop.mjs +3 -3
  29. package/dist/commands/stop.mjs.map +1 -1
  30. package/dist/{docker-BbX0yOpg.mjs → docker-Ciz9N6uI.mjs} +2 -2
  31. package/dist/{docker-BbX0yOpg.mjs.map → docker-Ciz9N6uI.mjs.map} +1 -1
  32. package/dist/{environment-ABj1IWZt.mjs → environment-CuHKxtIO.mjs} +1 -1
  33. package/dist/{environment-ABj1IWZt.mjs.map → environment-CuHKxtIO.mjs.map} +1 -1
  34. package/dist/{journey-guidance-E_3_UwGt.mjs → journey-guidance-CtQUfWar.mjs} +14 -3
  35. package/dist/{journey-guidance-E_3_UwGt.mjs.map → journey-guidance-CtQUfWar.mjs.map} +1 -1
  36. package/dist/{oclif-SjHLB_XK.mjs → oclif-CL6-M13m.mjs} +220 -43
  37. package/dist/oclif-CL6-M13m.mjs.map +1 -0
  38. package/dist/{orca-_TmrJQMw.mjs → orca-BWB4zOdw.mjs} +749 -192
  39. package/dist/orca-BWB4zOdw.mjs.map +1 -0
  40. package/dist/{ports-XujDtA47.mjs → ports-Cu2Y7aXQ.mjs} +25 -20
  41. package/dist/ports-Cu2Y7aXQ.mjs.map +1 -0
  42. package/dist/{processes-p2PPbKGG.mjs → processes-Bn2WngOi.mjs} +1 -1
  43. package/dist/{processes-p2PPbKGG.mjs.map → processes-Bn2WngOi.mjs.map} +1 -1
  44. package/dist/{project-DXc7q4wN.mjs → project-yejJSfdJ.mjs} +57 -4
  45. package/dist/project-yejJSfdJ.mjs.map +1 -0
  46. package/dist/scaffold-manifest-CKNFLX9T.mjs +81 -0
  47. package/dist/scaffold-manifest-CKNFLX9T.mjs.map +1 -0
  48. package/dist/{sync-Bu_iLIbb.mjs → sync-Cx2_4rXW.mjs} +178 -52
  49. package/dist/sync-Cx2_4rXW.mjs.map +1 -0
  50. package/dist/user-schema-W1OsXdpc.mjs +91 -0
  51. package/dist/user-schema-W1OsXdpc.mjs.map +1 -0
  52. package/oclif.manifest.json +227 -5
  53. package/package.json +7 -5
  54. package/dist/docker-guidance-3UZESTfb.mjs +0 -21
  55. package/dist/docker-guidance-3UZESTfb.mjs.map +0 -1
  56. package/dist/oclif-SjHLB_XK.mjs.map +0 -1
  57. package/dist/orca-_TmrJQMw.mjs.map +0 -1
  58. package/dist/ports-XujDtA47.mjs.map +0 -1
  59. package/dist/project-DXc7q4wN.mjs.map +0 -1
  60. package/dist/sync-Bu_iLIbb.mjs.map +0 -1
  61. package/dist/user-schema-DDz5-lX5.mjs +0 -13
  62. package/dist/user-schema-DDz5-lX5.mjs.map +0 -1
@@ -1,15 +1,17 @@
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-SjHLB_XK.mjs";
2
- import { o as issuerFromPort, t as createOrca } from "../orca-_TmrJQMw.mjs";
3
- import { a as readZitadelConfig, i as readRendererId, o as readZitadelSecret, r as readDevelopmentIssuer, t as hasZitadelConfig } from "../project-DXc7q4wN.mjs";
4
- import { t as SCHEMAS_DIR } from "../user-schema-DDz5-lX5.mjs";
5
- import { n as listenersForPort } from "../ports-XujDtA47.mjs";
6
- import { d as assertServerPackageAvailable, o as imageAvailable, r as dockerAvailable } from "../docker-BbX0yOpg.mjs";
7
- import { n as dockerUnavailableMessage, t as dockerRuntimeGuidance } from "../docker-guidance-3UZESTfb.mjs";
8
- import { t as discoverManagedRuntimeProcesses } from "../processes-p2PPbKGG.mjs";
9
- import { chmod, readFile, readdir, stat, writeFile } from "node:fs/promises";
1
+ import { E as ZitadelError, S as isObject, a as DEFAULT_LOCAL_SERVER_PORT, c as checkLocalServerHealth, h as readRuntimeMetadata, k as publicCliCommand, l as defaultLocalServerImageForCliVersion, m as localServerUrl, s as assertLocalStateWritable, t as BaseCommand } from "../oclif-CL6-M13m.mjs";
2
+ import { n as claimCommand, r as claimState, t as claimAction } from "../claim-state-BTGInAaN.mjs";
3
+ import { s as issuerFromPort, t as createOrca } from "../orca-BWB4zOdw.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-yejJSfdJ.mjs";
5
+ import { i as updateScaffold, t as SCHEMAS_DIR } from "../user-schema-W1OsXdpc.mjs";
6
+ import { n as readScaffoldManifest, r as writeScaffoldManifest, t as hashScaffoldFile } from "../scaffold-manifest-CKNFLX9T.mjs";
7
+ import { d as assertServerPackageAvailable, o as imageAvailable, r as dockerAvailable } from "../docker-Ciz9N6uI.mjs";
8
+ import { n as dockerRuntimeGuidance, r as dockerUnavailableMessage, t as listenersForPort } from "../ports-Cu2Y7aXQ.mjs";
9
+ import { t as discoverManagedRuntimeProcesses } from "../processes-Bn2WngOi.mjs";
10
+ import { chmod, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
10
11
  import { join } from "node:path";
11
12
  import { Flags } from "@oclif/core";
12
13
  import consola from "consola";
14
+ import semver from "semver";
13
15
  import { schemaConfigSchema } from "@zitadel/config/schemas";
14
16
  //#region src/commands/doctor/checks/types.ts
15
17
  /**
@@ -36,7 +38,11 @@ var AbstractSanityCheck = class {
36
38
  name: this.name,
37
39
  status: "fail",
38
40
  message: error instanceof Error ? error.message : String(error),
39
- path: this.path
41
+ path: this.path,
42
+ ...error instanceof ZitadelError ? {
43
+ code: error.code,
44
+ ...error.hint === void 0 ? {} : { hint: error.hint }
45
+ } : {}
40
46
  };
41
47
  }
42
48
  }
@@ -148,46 +154,47 @@ var FrameworkCheck = class extends AbstractSanityCheck {
148
154
  //#endregion
149
155
  //#region src/commands/doctor/patch-context.ts
150
156
  /**
151
- * Reconstructs a {@link PatchContext} from the on-disk project (config, secret)
152
- * plus fresh framework detection, so a patcher repair can rebuild its plan.
153
- * Used by the dependency check's `fix`, which reclaims the framework-specific
154
- * SDK package via `patcher.repair`. Schema and flow config are user-editable
155
- * sync resources, not patcher inputs, 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.
156
166
  */
157
167
  async function loadPatchContext(cwd, orca, cliVersion) {
158
168
  const config = await readZitadelConfig(cwd);
159
169
  const secret = await readZitadelSecret(cwd);
160
170
  const framework = await orca.detect(cwd);
171
+ const scaffold = await readScaffoldManifest(cwd);
161
172
  return {
162
173
  framework,
163
174
  rendererId: readRendererId(config),
164
175
  issuer: await resolveIssuer(cwd, config, framework),
165
176
  server: typeof config.server === "string" ? config.server : "",
166
177
  cliVersion,
178
+ scaffoldedFramework: scaffold?.scaffolded_framework,
179
+ posture: scaffold?.posture ?? "page",
180
+ preset: readPreset(config),
181
+ useCase: readUseCase(config),
167
182
  project: {
168
183
  id: secret.project_id,
169
- projectSecret: secret.project_secret,
170
- previewSecret: secret.preview_secret,
171
- previewOrigins: secret.preview_origins,
172
- 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
173
188
  }
174
189
  };
175
190
  }
176
191
  async function resolveIssuer(cwd, config, facts) {
177
192
  const fromConfig = readDevelopmentIssuer(config);
178
193
  if (fromConfig && fromConfig.length > 0) return fromConfig;
179
- const state = await readState(cwd);
180
- 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);
181
196
  return facts.url;
182
197
  }
183
- async function readState(cwd) {
184
- try {
185
- const contents = await readFile(join(cwd, ".zitadel/state.json"), "utf8");
186
- return JSON.parse(contents);
187
- } catch {
188
- return;
189
- }
190
- }
191
198
  //#endregion
192
199
  //#region src/commands/doctor/checks/dependency.ts
193
200
  /**
@@ -205,22 +212,385 @@ var DependencyCheck = class extends AbstractSanityCheck {
205
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");
206
213
  }
207
214
  /**
208
- * Repairs by reclaiming the patcher's managed artifacts: rebuilds the
215
+ * Repairs by re-applying the patcher's managed artifacts: rebuilds the
209
216
  * `PatchContext` from disk and calls `patcher.repair`, which re-adds the
210
217
  * SDK dependency via its `add-dep` op. The exact package name is framework
211
218
  * + renderer specific and known only to the patcher (which deliberately
212
219
  * hides its file-op plan behind the family-neutral `Patcher` interface),
213
- * 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.
214
223
  */
215
224
  async fix(ctx) {
216
225
  const patchCtx = await loadPatchContext(ctx.cwd, ctx.orca, ctx.cliVersion);
217
226
  await ctx.orca.patcherFor(patchCtx.framework.id).repair(patchCtx, {
218
227
  cwd: ctx.cwd,
219
228
  dryRun: ctx.dryRun,
220
- force: true
229
+ force: false,
230
+ missingOnly: true
231
+ });
232
+ }
233
+ };
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 = mismatched.map(([name]) => `${name}@${ctx.cliVersion}`).join(" ");
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 e.g. \`npm install ${remedy}\``,
288
+ details: { mismatched: mismatched.map(([name, version]) => ({
289
+ name,
290
+ declared: version,
291
+ expected: ctx.cliVersion
292
+ })) }
293
+ };
294
+ }
295
+ return {
296
+ ...base,
297
+ status: "pass",
298
+ message: exact.length > 0 ? `Zitadel package pins match the CLI version (${exact.length} compared)` : "no exactly-pinned Zitadel packages to compare"
299
+ };
300
+ }
301
+ /**
302
+ * No auto-fix: repair's dependency op is deliberately additive-only (it
303
+ * never rewrites a version the user declared), so the warn message carries
304
+ * the exact install command instead.
305
+ */
306
+ async fix(_ctx) {}
307
+ };
308
+ //#endregion
309
+ //#region src/commands/doctor/checks/managed-files.ts
310
+ /**
311
+ * Verifies the scaffolded app files still exist: the ones `zitadel setup`
312
+ * wrote per the scaffold manifest in `.zitadel/state.json`, or — for apps
313
+ * scaffolded before the manifest existed — the patcher's current template
314
+ * expectations minus conditionally-written files. A missing *infrastructure*
315
+ * file (request boundary, provider, type declarations) breaks the auth
316
+ * integration and fails the check; a missing *presentation* page only warns.
317
+ * Edited and user-adopted files pass — they are the intended customization
318
+ * paths — and are labelled in `details` for visibility.
319
+ *
320
+ * Implements {@link SanityCheck} directly rather than via the abstract base:
321
+ * the base's `verify`-throws contract can only express pass/fail, and this
322
+ * check needs the `warn` outcome.
323
+ */
324
+ var ManagedFilesCheck = class {
325
+ name = "managed-files";
326
+ async run(ctx) {
327
+ const manifest = await readScaffoldManifest(ctx.cwd);
328
+ const evaluated = manifest ? await evaluateManifest(ctx.cwd, manifest) : await evaluateTemplates(ctx);
329
+ if (!evaluated) return {
330
+ name: this.name,
331
+ status: "warn",
332
+ message: "Could not enumerate scaffolded app files (framework or renderer unresolved); skipping the file check"
333
+ };
334
+ const { mode, rows } = evaluated;
335
+ const wiring = await probeConfigWiring(ctx);
336
+ const actions = await resolveArtifacts(ctx);
337
+ const boundaries = actions ? await detectBoundaryConflicts(actions, ctx.cwd, manifest) : [];
338
+ const missing = rows.filter((row) => row.state === "missing");
339
+ const missingInfrastructure = missing.filter((row) => row.class === "infrastructure");
340
+ const detachedInfrastructure = wiring.filter((s) => s.state === "detached" && s.wiring === "infrastructure");
341
+ const detachedConvenience = wiring.filter((s) => s.state === "detached" && s.wiring === "convenience");
342
+ const unverifiable = wiring.filter((s) => s.state === "unknown");
343
+ const conflicts = boundaries.filter((b) => b.disposition === "conflict");
344
+ const removable = boundaries.filter((b) => b.disposition === "removable");
345
+ const details = {
346
+ mode,
347
+ files: rows,
348
+ ...wiring.length > 0 ? { config_wiring: wiring } : {},
349
+ ...boundaries.length > 0 ? { boundary_conflicts: boundaries } : {}
350
+ };
351
+ const failures = [];
352
+ if (missingInfrastructure.length > 0) failures.push(`missing scaffolded infrastructure file(s): ${listPaths(missingInfrastructure)}`);
353
+ if (detachedInfrastructure.length > 0) failures.push(`detached managed config wiring: ${detachedInfrastructure.map((s) => s.path).join(", ")}`);
354
+ 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`);
355
+ if (failures.length > 0) return {
356
+ name: this.name,
357
+ status: "fail",
358
+ message: failures.join("; "),
359
+ details
360
+ };
361
+ const warnings = [];
362
+ if (missing.length > 0) warnings.push(`missing scaffolded page(s): ${listPaths(missing)}`);
363
+ if (detachedConvenience.length > 0) warnings.push(`unapplied managed config edit(s): ${detachedConvenience.map((s) => s.path).join(", ")}`);
364
+ 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(", ")}`);
365
+ 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})`);
366
+ if (warnings.length > 0) return {
367
+ name: this.name,
368
+ status: "warn",
369
+ message: warnings.join("; "),
370
+ details
371
+ };
372
+ return {
373
+ name: this.name,
374
+ status: "pass",
375
+ message: `Scaffolded app files are present (${rows.length} tracked${wiring.length > 0 ? `, ${wiring.length} wiring${wiring.length === 1 ? "" : "s"} verified` : ""}${mode === "template" ? ", from templates" : ""})`,
376
+ details
377
+ };
378
+ }
379
+ /**
380
+ * Restores missing managed files via the patcher's missing-only repair —
381
+ * existing files, edited or adopted, are never overwritten — then refreshes
382
+ * the manifest hashes of the files the repair brought back so they classify
383
+ * as pristine again. The restored bytes come from the *current* templates,
384
+ * which may differ from what the original CLI version wrote; the re-hash
385
+ * records that honestly. On a pre-manifest app (template mode) a successful
386
+ * repair also materializes the manifest from the marker-bearing files now
387
+ * on disk, completing the migration ADR 042 promises — adopted files stay
388
+ * the user's and are not recorded.
389
+ */
390
+ async fix(ctx) {
391
+ const manifest = await readScaffoldManifest(ctx.cwd);
392
+ const missingBefore = manifest ? await missingManifestPaths(ctx.cwd, manifest) : [];
393
+ const patchCtx = await loadPatchContext(ctx.cwd, ctx.orca, ctx.cliVersion);
394
+ const patcher = ctx.orca.patcherFor(patchCtx.framework.id);
395
+ const actions = patcher.artifacts({
396
+ framework: patchCtx.framework,
397
+ rendererId: patchCtx.rendererId
398
+ });
399
+ const boundaries = await detectBoundaryConflicts(actions, ctx.cwd, manifest);
400
+ const excludePaths = boundaries.filter((b) => b.disposition === "conflict").map((b) => b.current);
401
+ const removed = [];
402
+ if (!ctx.dryRun) {
403
+ for (const boundary of boundaries) if (boundary.disposition === "removable") {
404
+ await rm(join(ctx.cwd, boundary.retired), { force: true });
405
+ removed.push(boundary.retired);
406
+ }
407
+ }
408
+ await patcher.repair(patchCtx, {
409
+ cwd: ctx.cwd,
410
+ dryRun: ctx.dryRun,
411
+ force: false,
412
+ missingOnly: true,
413
+ ...excludePaths.length > 0 ? { excludePaths } : {}
414
+ });
415
+ if (ctx.dryRun) return;
416
+ if (!manifest) {
417
+ await materializeManifest(ctx, patchCtx);
418
+ return;
419
+ }
420
+ if (missingBefore.length === 0 && removed.length === 0) return;
421
+ const current = new Set(actions.markedFiles);
422
+ const conditional = new Set(actions.conditionalFiles ?? []);
423
+ const files = {};
424
+ for (const [path, entry] of Object.entries(manifest.files)) {
425
+ const contents = await readIfExists(join(ctx.cwd, path));
426
+ if (contents === void 0 && !current.has(path)) continue;
427
+ files[path] = contents !== void 0 && missingBefore.includes(path) ? {
428
+ ...entry,
429
+ hash: hashScaffoldFile(contents)
430
+ } : entry;
431
+ }
432
+ for (const path of actions.markedFiles) {
433
+ if (files[path] || conditional.has(path)) continue;
434
+ const contents = await readIfExists(join(ctx.cwd, path));
435
+ if (contents !== void 0 && contents.includes("// zitadel-cli: managed-file v1")) files[path] = {
436
+ hash: hashScaffoldFile(contents),
437
+ class: actions.fileClasses?.[path] ?? "presentation"
438
+ };
439
+ }
440
+ await updateScaffold(ctx.cwd, {
441
+ ...manifest,
442
+ files
221
443
  });
222
444
  }
223
445
  };
446
+ async function detectBoundaryConflicts(actions, cwd, manifest) {
447
+ const conflicts = [];
448
+ for (const [current, retiredPaths] of Object.entries(actions.retiredAlternates ?? {})) for (const retired of retiredPaths) {
449
+ const contents = await readIfExists(join(cwd, retired));
450
+ if (contents === void 0) continue;
451
+ const entry = manifest?.files[retired];
452
+ const pristine = entry !== void 0 && contents.includes("// zitadel-cli: managed-file v1") && hashScaffoldFile(contents) === entry.hash;
453
+ conflicts.push({
454
+ current,
455
+ retired,
456
+ disposition: pristine ? "removable" : "conflict"
457
+ });
458
+ }
459
+ return conflicts;
460
+ }
461
+ /**
462
+ * Probe the patcher's managed config wirings (dev proxy merges, route
463
+ * registrations). Needs the full patch context (project secret et al) to
464
+ * rebuild the plan; when that fails — missing secret, undetectable
465
+ * framework — the probe is skipped silently and the responsible check
466
+ * reports the real problem.
467
+ */
468
+ async function probeConfigWiring(ctx) {
469
+ try {
470
+ const patchCtx = await loadPatchContext(ctx.cwd, ctx.orca, ctx.cliVersion);
471
+ return await ctx.orca.patcherFor(patchCtx.framework.id).verify(patchCtx, { cwd: ctx.cwd });
472
+ } catch {
473
+ return [];
474
+ }
475
+ }
476
+ /**
477
+ * Migration path for apps scaffolded before the manifest existed: after a
478
+ * template-mode repair, record the marker-bearing managed files now on disk
479
+ * so the next doctor run gets exact manifest-mode verification instead of
480
+ * template guessing (and its Next-major / template-growth misclassification
481
+ * risk). Marker-less (adopted) files and conditionally-scaffolded ones are
482
+ * deliberately absent.
483
+ *
484
+ * The manifest is only finalized when every current *infrastructure* file is
485
+ * present and recordable (marker-bearing): materializing during an
486
+ * unresolved boundary conflict — or over an adopted boundary — would write a
487
+ * manifest that tracks no request boundary at all, and deleting the boundary
488
+ * later would go undetected. Until then the app stays on the template
489
+ * fallback, whose presence checks keep enforcing the boundary. Best-effort:
490
+ * an unreadable state file leaves the app in template mode, same as before.
491
+ */
492
+ async function materializeManifest(ctx, patchCtx) {
493
+ try {
494
+ const actions = ctx.orca.patcherFor(patchCtx.framework.id).artifacts({
495
+ framework: patchCtx.framework,
496
+ rendererId: patchCtx.rendererId
497
+ });
498
+ const conditional = new Set(actions.conditionalFiles ?? []);
499
+ const marked = [];
500
+ for (const path of actions.markedFiles) {
501
+ if (conditional.has(path)) continue;
502
+ const contents = await readIfExists(join(ctx.cwd, path));
503
+ if (contents !== void 0 && contents.includes("// zitadel-cli: managed-file v1")) marked.push(path);
504
+ }
505
+ if (!actions.markedFiles.filter((path) => actions.fileClasses?.[path] === "infrastructure" && !conditional.has(path)).every((path) => marked.includes(path))) return;
506
+ await writeScaffoldManifest({
507
+ cwd: ctx.cwd,
508
+ actions,
509
+ written: marked
510
+ });
511
+ } catch {}
512
+ }
513
+ async function evaluateManifest(cwd, manifest) {
514
+ const rows = [];
515
+ for (const [path, entry] of Object.entries(manifest.files)) {
516
+ const contents = await readIfExists(join(cwd, path));
517
+ rows.push({
518
+ path,
519
+ class: entry.class,
520
+ state: stateFor(contents, entry.hash)
521
+ });
522
+ }
523
+ return {
524
+ mode: "manifest",
525
+ rows
526
+ };
527
+ }
528
+ /**
529
+ * Fallback for apps scaffolded before the manifest existed: derive the
530
+ * expected set from the patcher's current templates. Conditionally-written
531
+ * files (e.g. the framework home page) are excluded — without a manifest
532
+ * there is no way to know whether setup ever wrote them. Presence-only:
533
+ * no recorded hash means `pristine` vs `edited` cannot be told apart.
534
+ */
535
+ async function evaluateTemplates(ctx) {
536
+ const actions = await resolveArtifacts(ctx);
537
+ if (!actions) return;
538
+ const conditional = new Set(actions.conditionalFiles ?? []);
539
+ const rows = [];
540
+ for (const path of actions.markedFiles) {
541
+ if (conditional.has(path)) continue;
542
+ const contents = await readIfExists(join(ctx.cwd, path));
543
+ rows.push({
544
+ path,
545
+ class: actions.fileClasses?.[path] ?? "presentation",
546
+ state: stateFor(contents, void 0)
547
+ });
548
+ }
549
+ return {
550
+ mode: "template",
551
+ rows
552
+ };
553
+ }
554
+ /**
555
+ * Resolves the patcher artifacts the same way `eject` does, tolerating every
556
+ * failure with `undefined`: a broken framework detection, an unreadable
557
+ * config, or an unresolvable renderer are reported by their own checks —
558
+ * this one only degrades to "cannot enumerate".
559
+ */
560
+ async function resolveArtifacts(ctx) {
561
+ const framework = await ctx.orca.tryDetect(ctx.cwd);
562
+ if (!framework) return;
563
+ try {
564
+ const config = await readZitadelConfig(ctx.cwd).catch(() => ({}));
565
+ return ctx.orca.patcherFor(framework.id).artifacts({
566
+ framework,
567
+ rendererId: readRendererId(config)
568
+ });
569
+ } catch {
570
+ return;
571
+ }
572
+ }
573
+ function stateFor(contents, hash) {
574
+ if (contents === void 0) return "missing";
575
+ if (!contents.includes("// zitadel-cli: managed-file v1")) return "adopted";
576
+ if (hash === void 0) return "managed";
577
+ return hashScaffoldFile(contents) === hash ? "pristine" : "edited";
578
+ }
579
+ async function missingManifestPaths(cwd, manifest) {
580
+ const missing = [];
581
+ for (const path of Object.keys(manifest.files)) if (await readIfExists(join(cwd, path)) === void 0) missing.push(path);
582
+ return missing;
583
+ }
584
+ async function readIfExists(path) {
585
+ try {
586
+ return await readFile(path, "utf8");
587
+ } catch {
588
+ return;
589
+ }
590
+ }
591
+ function listPaths(rows) {
592
+ return rows.map((row) => row.path).join(", ");
593
+ }
224
594
  //#endregion
225
595
  //#region src/commands/doctor/checks/project-match.ts
226
596
  /** Verifies `.zitadel/secret`'s project_id matches `zitadel.json`'s project. */
@@ -311,6 +681,62 @@ var SchemaCheck = class extends AbstractSanityCheck {
311
681
  }
312
682
  };
313
683
  //#endregion
684
+ //#region src/commands/doctor/checks/claim.ts
685
+ /**
686
+ * Reports whether the project is attached to a team, reading `claimed_at` and
687
+ * `team_id` from `.zitadel/secret`.
688
+ *
689
+ * Always advisory, never a failure. An unattached project works exactly like an
690
+ * attached one — same issuer, same users, same applications — so the only thing
691
+ * missing is durability, which is the developer's call and not a defect. That
692
+ * matters mechanically too: `doctor` turns any `fail` into a thrown
693
+ * `E_VALIDATION`, so failing here would break every scripted `zitadel doctor`
694
+ * against a project nobody had claimed yet.
695
+ *
696
+ * Implements {@link SanityCheck} directly rather than via the abstract base,
697
+ * for the same reason `ManagedFilesCheck` does: the base's `verify`-throws
698
+ * contract can only express pass/fail, and this check is only ever warn or
699
+ * pass.
700
+ */
701
+ var ClaimCheck = class {
702
+ name = "claim";
703
+ path = ".zitadel/secret";
704
+ async run(ctx) {
705
+ let state;
706
+ try {
707
+ state = claimState({
708
+ secret: await readZitadelSecret(ctx.cwd),
709
+ server: readProjectServer(await readZitadelConfig(ctx.cwd))
710
+ });
711
+ } catch {
712
+ return {
713
+ name: this.name,
714
+ status: "pass",
715
+ message: "Skipped: could not read the project files that record the owning team",
716
+ path: this.path
717
+ };
718
+ }
719
+ if (state.kind === "detached") return {
720
+ name: this.name,
721
+ status: "warn",
722
+ message: "This project is temporary until you attach it to a team. Run `zitadel claim` to make it permanent; nothing about the project changes.",
723
+ path: this.path
724
+ };
725
+ return {
726
+ name: this.name,
727
+ status: "pass",
728
+ message: state.kind === "attached" ? `Project is attached to team ${state.team_id}` : "Project is not on a server where teams apply",
729
+ path: this.path
730
+ };
731
+ }
732
+ /**
733
+ * Deliberately a no-op. `doctor --fix` calls `fix` on every non-passing
734
+ * check, but claiming requires a human to sign in through a browser, so there
735
+ * is nothing safe to automate here.
736
+ */
737
+ async fix(_ctx) {}
738
+ };
739
+ //#endregion
314
740
  //#region src/commands/doctor/checks/index.ts
315
741
  /** Every diagnostic the `doctor` command runs, in display order. */
316
742
  const SANITY_CHECKS = [
@@ -322,7 +748,10 @@ const SANITY_CHECKS = [
322
748
  new FrameworkCheck(),
323
749
  new SchemaCheck(),
324
750
  new DependencyCheck(),
325
- new ProjectMatchCheck()
751
+ new DependencyVersionCheck(),
752
+ new ManagedFilesCheck(),
753
+ new ProjectMatchCheck(),
754
+ new ClaimCheck()
326
755
  ];
327
756
  //#endregion
328
757
  //#region src/commands/doctor/index.ts
@@ -339,8 +768,9 @@ const LOCAL_RUNTIME_CHECK_NAMES = new Set([
339
768
  *
340
769
  * Runs every registered {@link SANITY_CHECKS} entry and emits the aggregate
341
770
  * result; if any check fails it throws `E_VALIDATION` carrying the full check
342
- * details. With `--fix`, each failing check first attempts its own repair (a
343
- * no-op for checks with no safe automatic remedy), then the battery re-runs.
771
+ * details. With `--fix`, each check that did not pass failed or warned —
772
+ * first attempts its own repair (a no-op for checks with no safe automatic
773
+ * remedy), then the battery re-runs.
344
774
  *
345
775
  * The `--fix` loop is best-effort: a repair that throws (e.g. a missing
346
776
  * prerequisite file the check itself would also flag) is logged at debug
@@ -350,7 +780,7 @@ const LOCAL_RUNTIME_CHECK_NAMES = new Set([
350
780
  var Doctor = class Doctor extends BaseCommand {
351
781
  static description = "Verify local runtime and project state.";
352
782
  static flags = {
353
- fix: Flags.boolean({ description: "Re-apply missing managed files." }),
783
+ fix: Flags.boolean({ description: "Repair missing files and stale managed wiring." }),
354
784
  image: Flags.string({ description: "Container image to check." }),
355
785
  port: Flags.integer({
356
786
  description: "Local HTTP port.",
@@ -388,7 +818,7 @@ var Doctor = class Doctor extends BaseCommand {
388
818
  if (hasConfig && flags.fix) {
389
819
  const before = await Promise.all(SANITY_CHECKS.map((check) => check.run(ctx)));
390
820
  for (const [index, check] of SANITY_CHECKS.entries()) {
391
- if (before[index]?.status !== "fail") continue;
821
+ if (before[index]?.status === "pass") continue;
392
822
  try {
393
823
  await check.fix(ctx);
394
824
  } catch (error) {
@@ -423,7 +853,7 @@ var Doctor = class Doctor extends BaseCommand {
423
853
  };
424
854
  if (failed.length > 0) {
425
855
  const advice = failureAdvice(failed, image, port, this.meta.cliVersion);
426
- throw new ZitadelError(failed.some((check) => check.name === "port") ? "E_PORT_IN_USE" : "E_VALIDATION", "Zitadel doctor found issues", {
856
+ 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", {
427
857
  hint: advice.hint,
428
858
  nextCommands: advice.nextCommands,
429
859
  details: data
@@ -475,6 +905,11 @@ function failureAdvice(failed, image, port, cliVersion) {
475
905
  hint: "Existing local runtime metadata was found, but the local Zitadel server is not healthy. Start it again or reset stale local data.",
476
906
  nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand("reset --force", cliVersion)]
477
907
  };
908
+ const typed = failed.find((check) => check.code !== void 0 && check.hint !== void 0);
909
+ if (typed?.hint !== void 0) return {
910
+ hint: typed.hint,
911
+ nextCommands: [publicCliCommand("doctor", cliVersion)]
912
+ };
478
913
  if (failed.some((check) => !LOCAL_RUNTIME_CHECK_NAMES.has(check.name))) return {
479
914
  hint: `Run \`${publicCliCommand("doctor --fix", cliVersion)}\` to re-apply missing managed files.`,
480
915
  nextCommands: [publicCliCommand("doctor --fix", cliVersion)]
@@ -492,6 +927,10 @@ function advisoryForWarnings(warnings, cliVersion) {
492
927
  nextActions.push(...advice.nextActions);
493
928
  nextCommands.push(...advice.nextCommands);
494
929
  }
930
+ if (warnings.some((check) => check.name === "claim")) {
931
+ nextActions.push(claimAction(cliVersion));
932
+ nextCommands.push(claimCommand(cliVersion));
933
+ }
495
934
  if (hasManagedRuntimeProcesses(warnings.find((check) => check.name === "managed-runtime-processes"))) {
496
935
  nextActions.push("Review other host-wide CLI-managed local Zitadel runtimes before starting a new one.");
497
936
  nextCommands.push(publicCliCommand("stop --all", cliVersion));