@zitadel/cli 0.1.0-alpha.17 → 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.
- package/README.md +145 -107
- package/SKILLS.md +88 -12
- package/dist/branding-BXj028-W.mjs +79 -0
- package/dist/branding-BXj028-W.mjs.map +1 -0
- package/dist/claim-state-BTGInAaN.mjs +66 -0
- package/dist/claim-state-BTGInAaN.mjs.map +1 -0
- package/dist/commands/apply.mjs +6 -5
- package/dist/commands/apply.mjs.map +1 -1
- package/dist/commands/branding/eject.mjs +125 -0
- package/dist/commands/branding/eject.mjs.map +1 -0
- package/dist/commands/claim.mjs +402 -0
- package/dist/commands/claim.mjs.map +1 -0
- package/dist/commands/doctor.mjs +477 -37
- package/dist/commands/doctor.mjs.map +1 -1
- package/dist/commands/eject.mjs +3 -3
- package/dist/commands/logs.mjs +2 -2
- package/dist/commands/plan.mjs +6 -5
- package/dist/commands/plan.mjs.map +1 -1
- package/dist/commands/reset.mjs +2 -2
- package/dist/commands/schemas/list.mjs +8 -8
- package/dist/commands/schemas/list.mjs.map +1 -1
- package/dist/commands/setup.mjs +305 -36
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +3 -3
- package/dist/commands/status.mjs +21 -13
- package/dist/commands/status.mjs.map +1 -1
- package/dist/commands/stop.mjs +3 -3
- package/dist/commands/stop.mjs.map +1 -1
- package/dist/{docker-pGcTrDIY.mjs → docker-Ciz9N6uI.mjs} +2 -2
- package/dist/{docker-pGcTrDIY.mjs.map → docker-Ciz9N6uI.mjs.map} +1 -1
- package/dist/{environment-BQF7LeCz.mjs → environment-CuHKxtIO.mjs} +1 -1
- package/dist/{environment-BQF7LeCz.mjs.map → environment-CuHKxtIO.mjs.map} +1 -1
- package/dist/{journey-guidance-CYVpQszz.mjs → journey-guidance-CtQUfWar.mjs} +14 -3
- package/dist/{journey-guidance-CYVpQszz.mjs.map → journey-guidance-CtQUfWar.mjs.map} +1 -1
- package/dist/{oclif-BRdjVNne.mjs → oclif-CL6-M13m.mjs} +201 -43
- package/dist/oclif-CL6-M13m.mjs.map +1 -0
- package/dist/{orca-Dip6DPJ2.mjs → orca-BWB4zOdw.mjs} +749 -192
- package/dist/orca-BWB4zOdw.mjs.map +1 -0
- package/dist/{ports-V8JfiNgT.mjs → ports-Cu2Y7aXQ.mjs} +2 -2
- package/dist/{ports-V8JfiNgT.mjs.map → ports-Cu2Y7aXQ.mjs.map} +1 -1
- package/dist/{processes-BVqYsxT8.mjs → processes-Bn2WngOi.mjs} +1 -1
- package/dist/{processes-BVqYsxT8.mjs.map → processes-Bn2WngOi.mjs.map} +1 -1
- package/dist/{project-BGgv7Bba.mjs → project-yejJSfdJ.mjs} +57 -4
- package/dist/project-yejJSfdJ.mjs.map +1 -0
- package/dist/scaffold-manifest-CKNFLX9T.mjs +81 -0
- package/dist/scaffold-manifest-CKNFLX9T.mjs.map +1 -0
- package/dist/{sync-Cd3z7Q1R.mjs → sync-Cx2_4rXW.mjs} +99 -50
- package/dist/sync-Cx2_4rXW.mjs.map +1 -0
- package/dist/user-schema-W1OsXdpc.mjs +91 -0
- package/dist/user-schema-W1OsXdpc.mjs.map +1 -0
- package/oclif.manifest.json +227 -5
- package/package.json +8 -6
- package/dist/oclif-BRdjVNne.mjs.map +0 -1
- package/dist/orca-Dip6DPJ2.mjs.map +0 -1
- package/dist/project-BGgv7Bba.mjs.map +0 -1
- package/dist/sync-Cd3z7Q1R.mjs.map +0 -1
- package/dist/user-schema-DDz5-lX5.mjs +0 -13
- package/dist/user-schema-DDz5-lX5.mjs.map +0 -1
package/dist/commands/doctor.mjs
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { t as
|
|
5
|
-
import {
|
|
6
|
-
import { n as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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";
|
|
9
11
|
import { join } from "node:path";
|
|
10
12
|
import { Flags } from "@oclif/core";
|
|
11
13
|
import consola from "consola";
|
|
14
|
+
import semver from "semver";
|
|
12
15
|
import { schemaConfigSchema } from "@zitadel/config/schemas";
|
|
13
16
|
//#region src/commands/doctor/checks/types.ts
|
|
14
17
|
/**
|
|
@@ -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,
|
|
151
|
-
* plus fresh framework detection, so a patcher
|
|
152
|
-
* Used by the
|
|
153
|
-
*
|
|
154
|
-
*
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
179
|
-
if (typeof
|
|
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,22 +212,385 @@ 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
|
|
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:
|
|
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
|
|
220
443
|
});
|
|
221
444
|
}
|
|
222
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
|
+
}
|
|
223
594
|
//#endregion
|
|
224
595
|
//#region src/commands/doctor/checks/project-match.ts
|
|
225
596
|
/** Verifies `.zitadel/secret`'s project_id matches `zitadel.json`'s project. */
|
|
@@ -310,6 +681,62 @@ var SchemaCheck = class extends AbstractSanityCheck {
|
|
|
310
681
|
}
|
|
311
682
|
};
|
|
312
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
|
|
313
740
|
//#region src/commands/doctor/checks/index.ts
|
|
314
741
|
/** Every diagnostic the `doctor` command runs, in display order. */
|
|
315
742
|
const SANITY_CHECKS = [
|
|
@@ -321,7 +748,10 @@ const SANITY_CHECKS = [
|
|
|
321
748
|
new FrameworkCheck(),
|
|
322
749
|
new SchemaCheck(),
|
|
323
750
|
new DependencyCheck(),
|
|
324
|
-
new
|
|
751
|
+
new DependencyVersionCheck(),
|
|
752
|
+
new ManagedFilesCheck(),
|
|
753
|
+
new ProjectMatchCheck(),
|
|
754
|
+
new ClaimCheck()
|
|
325
755
|
];
|
|
326
756
|
//#endregion
|
|
327
757
|
//#region src/commands/doctor/index.ts
|
|
@@ -338,8 +768,9 @@ const LOCAL_RUNTIME_CHECK_NAMES = new Set([
|
|
|
338
768
|
*
|
|
339
769
|
* Runs every registered {@link SANITY_CHECKS} entry and emits the aggregate
|
|
340
770
|
* result; if any check fails it throws `E_VALIDATION` carrying the full check
|
|
341
|
-
* details. With `--fix`, each
|
|
342
|
-
* no-op for checks with no safe automatic
|
|
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.
|
|
343
774
|
*
|
|
344
775
|
* The `--fix` loop is best-effort: a repair that throws (e.g. a missing
|
|
345
776
|
* prerequisite file the check itself would also flag) is logged at debug
|
|
@@ -349,7 +780,7 @@ const LOCAL_RUNTIME_CHECK_NAMES = new Set([
|
|
|
349
780
|
var Doctor = class Doctor extends BaseCommand {
|
|
350
781
|
static description = "Verify local runtime and project state.";
|
|
351
782
|
static flags = {
|
|
352
|
-
fix: Flags.boolean({ description: "
|
|
783
|
+
fix: Flags.boolean({ description: "Repair missing files and stale managed wiring." }),
|
|
353
784
|
image: Flags.string({ description: "Container image to check." }),
|
|
354
785
|
port: Flags.integer({
|
|
355
786
|
description: "Local HTTP port.",
|
|
@@ -387,7 +818,7 @@ var Doctor = class Doctor extends BaseCommand {
|
|
|
387
818
|
if (hasConfig && flags.fix) {
|
|
388
819
|
const before = await Promise.all(SANITY_CHECKS.map((check) => check.run(ctx)));
|
|
389
820
|
for (const [index, check] of SANITY_CHECKS.entries()) {
|
|
390
|
-
if (before[index]?.status
|
|
821
|
+
if (before[index]?.status === "pass") continue;
|
|
391
822
|
try {
|
|
392
823
|
await check.fix(ctx);
|
|
393
824
|
} catch (error) {
|
|
@@ -422,7 +853,7 @@ var Doctor = class Doctor extends BaseCommand {
|
|
|
422
853
|
};
|
|
423
854
|
if (failed.length > 0) {
|
|
424
855
|
const advice = failureAdvice(failed, image, port, this.meta.cliVersion);
|
|
425
|
-
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", {
|
|
426
857
|
hint: advice.hint,
|
|
427
858
|
nextCommands: advice.nextCommands,
|
|
428
859
|
details: data
|
|
@@ -474,6 +905,11 @@ function failureAdvice(failed, image, port, cliVersion) {
|
|
|
474
905
|
hint: "Existing local runtime metadata was found, but the local Zitadel server is not healthy. Start it again or reset stale local data.",
|
|
475
906
|
nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand("reset --force", cliVersion)]
|
|
476
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
|
+
};
|
|
477
913
|
if (failed.some((check) => !LOCAL_RUNTIME_CHECK_NAMES.has(check.name))) return {
|
|
478
914
|
hint: `Run \`${publicCliCommand("doctor --fix", cliVersion)}\` to re-apply missing managed files.`,
|
|
479
915
|
nextCommands: [publicCliCommand("doctor --fix", cliVersion)]
|
|
@@ -491,6 +927,10 @@ function advisoryForWarnings(warnings, cliVersion) {
|
|
|
491
927
|
nextActions.push(...advice.nextActions);
|
|
492
928
|
nextCommands.push(...advice.nextCommands);
|
|
493
929
|
}
|
|
930
|
+
if (warnings.some((check) => check.name === "claim")) {
|
|
931
|
+
nextActions.push(claimAction(cliVersion));
|
|
932
|
+
nextCommands.push(claimCommand(cliVersion));
|
|
933
|
+
}
|
|
494
934
|
if (hasManagedRuntimeProcesses(warnings.find((check) => check.name === "managed-runtime-processes"))) {
|
|
495
935
|
nextActions.push("Review other host-wide CLI-managed local Zitadel runtimes before starting a new one.");
|
|
496
936
|
nextCommands.push(publicCliCommand("stop --all", cliVersion));
|