@zitadel/cli 1.0.0-alpha.22 → 1.0.0-alpha.23
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 +2248 -227
- package/SKILLS.md +237 -65
- package/dist/groups-CMB0fMzE.mjs +22 -0
- package/dist/groups-CMB0fMzE.mjs.map +1 -0
- package/dist/index.mjs +16027 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib/oclif/help.mjs +67 -0
- package/dist/lib/oclif/help.mjs.map +1 -0
- package/oclif.manifest.json +4577 -185
- package/package.json +40 -8
- package/dist/box-B_mwKole.mjs +0 -35
- package/dist/box-B_mwKole.mjs.map +0 -1
- package/dist/branding-DvoH8e0D.mjs +0 -79
- package/dist/branding-DvoH8e0D.mjs.map +0 -1
- package/dist/claim-state-B6ZSKmhW.mjs +0 -125
- package/dist/claim-state-B6ZSKmhW.mjs.map +0 -1
- package/dist/commands/apply.mjs +0 -81
- package/dist/commands/apply.mjs.map +0 -1
- package/dist/commands/branding/eject.mjs +0 -104
- package/dist/commands/branding/eject.mjs.map +0 -1
- package/dist/commands/claim.mjs +0 -445
- package/dist/commands/claim.mjs.map +0 -1
- package/dist/commands/doctor.mjs +0 -1138
- package/dist/commands/doctor.mjs.map +0 -1
- package/dist/commands/eject.mjs +0 -157
- package/dist/commands/eject.mjs.map +0 -1
- package/dist/commands/logs.mjs +0 -58
- package/dist/commands/logs.mjs.map +0 -1
- package/dist/commands/plan.mjs +0 -65
- package/dist/commands/plan.mjs.map +0 -1
- package/dist/commands/reset.mjs +0 -79
- package/dist/commands/reset.mjs.map +0 -1
- package/dist/commands/schemas/list.mjs +0 -146
- package/dist/commands/schemas/list.mjs.map +0 -1
- package/dist/commands/setup.mjs +0 -1309
- package/dist/commands/setup.mjs.map +0 -1
- package/dist/commands/start.mjs +0 -288
- package/dist/commands/start.mjs.map +0 -1
- package/dist/commands/status.mjs +0 -151
- package/dist/commands/status.mjs.map +0 -1
- package/dist/commands/stop.mjs +0 -105
- package/dist/commands/stop.mjs.map +0 -1
- package/dist/designs-Ckz18Dpo.mjs +0 -38
- package/dist/designs-Ckz18Dpo.mjs.map +0 -1
- package/dist/docker-B1MjOMFo.mjs +0 -436
- package/dist/docker-B1MjOMFo.mjs.map +0 -1
- package/dist/environment-fibXmzM8.mjs +0 -17
- package/dist/environment-fibXmzM8.mjs.map +0 -1
- package/dist/journey-guidance-C9MxJNTg.mjs +0 -40
- package/dist/journey-guidance-C9MxJNTg.mjs.map +0 -1
- package/dist/oclif-JlsN1lfO.mjs +0 -1769
- package/dist/oclif-JlsN1lfO.mjs.map +0 -1
- package/dist/orca-CPMHzmg3.mjs +0 -3917
- package/dist/orca-CPMHzmg3.mjs.map +0 -1
- package/dist/package-manager-CDUsoGBy.mjs +0 -194
- package/dist/package-manager-CDUsoGBy.mjs.map +0 -1
- package/dist/ports-hidBnSW-.mjs +0 -116
- package/dist/ports-hidBnSW-.mjs.map +0 -1
- package/dist/processes-Cv19mJ2I.mjs +0 -120
- package/dist/processes-Cv19mJ2I.mjs.map +0 -1
- package/dist/project-Ba0nriNm.mjs +0 -140
- package/dist/project-Ba0nriNm.mjs.map +0 -1
- package/dist/sync-ChlLQ4vZ.mjs +0 -1633
- package/dist/sync-ChlLQ4vZ.mjs.map +0 -1
- package/dist/user-schema-C2mC-JTN.mjs +0 -91
- package/dist/user-schema-C2mC-JTN.mjs.map +0 -1
package/dist/commands/doctor.mjs
DELETED
|
@@ -1,1138 +0,0 @@
|
|
|
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-JlsN1lfO.mjs";
|
|
2
|
-
import { a as claimWindowClosedAction, i as claimState, r as claimCommand, t as claimAction } from "../claim-state-B6ZSKmhW.mjs";
|
|
3
|
-
import { s as issuerFromPort, t as createOrca } from "../orca-CPMHzmg3.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-Ba0nriNm.mjs";
|
|
5
|
-
import { i as updateScaffold, t as SCHEMAS_DIR } from "../user-schema-C2mC-JTN.mjs";
|
|
6
|
-
import { c as writeScaffoldManifest, n as detectPackageManager, o as hashScaffoldFile, s as readScaffoldManifest, t as addExactCommandFor } from "../package-manager-CDUsoGBy.mjs";
|
|
7
|
-
import { d as assertServerPackageAvailable, o as imageAvailable, r as dockerAvailable } from "../docker-B1MjOMFo.mjs";
|
|
8
|
-
import { n as dockerRuntimeGuidance, r as dockerUnavailableMessage, t as listenersForPort } from "../ports-hidBnSW-.mjs";
|
|
9
|
-
import { t as discoverManagedRuntimeProcesses } from "../processes-Cv19mJ2I.mjs";
|
|
10
|
-
import { chmod, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import { Flags } from "@oclif/core";
|
|
13
|
-
import consola from "consola";
|
|
14
|
-
import semver from "semver";
|
|
15
|
-
import { schemaConfigSchema } from "@zitadel/config/schemas";
|
|
16
|
-
//#region src/commands/doctor/checks/types.ts
|
|
17
|
-
/**
|
|
18
|
-
* Base class for checks: subclasses declare `name`, `path`, and a success
|
|
19
|
-
* `summary`, and implement the single {@link verify} method that throws on
|
|
20
|
-
* failure. {@link run} wraps it so a thrown error becomes a `fail` outcome
|
|
21
|
-
* carrying the error message, and success becomes a `pass` with `summary`.
|
|
22
|
-
*
|
|
23
|
-
* {@link fix} defaults to a no-op: checks whose failure has no safe automatic
|
|
24
|
-
* remedy (a missing secret, an invalid user schema) simply do not override it.
|
|
25
|
-
*/
|
|
26
|
-
var AbstractSanityCheck = class {
|
|
27
|
-
async run(ctx) {
|
|
28
|
-
try {
|
|
29
|
-
await this.verify(ctx);
|
|
30
|
-
return {
|
|
31
|
-
name: this.name,
|
|
32
|
-
status: "pass",
|
|
33
|
-
message: this.summary,
|
|
34
|
-
path: this.path
|
|
35
|
-
};
|
|
36
|
-
} catch (error) {
|
|
37
|
-
return {
|
|
38
|
-
name: this.name,
|
|
39
|
-
status: "fail",
|
|
40
|
-
message: error instanceof Error ? error.message : String(error),
|
|
41
|
-
path: this.path,
|
|
42
|
-
...error instanceof ZitadelError ? {
|
|
43
|
-
code: error.code,
|
|
44
|
-
...error.hint === void 0 ? {} : { hint: error.hint }
|
|
45
|
-
} : {}
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async fix(_ctx) {}
|
|
50
|
-
};
|
|
51
|
-
//#endregion
|
|
52
|
-
//#region src/commands/doctor/checks/config.ts
|
|
53
|
-
/** Verifies `zitadel.json` exists and parses. */
|
|
54
|
-
var ConfigCheck = class extends AbstractSanityCheck {
|
|
55
|
-
name = "config";
|
|
56
|
-
path = "zitadel.json";
|
|
57
|
-
summary = "zitadel.json parses";
|
|
58
|
-
async verify(ctx) {
|
|
59
|
-
await readZitadelConfig(ctx.cwd);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
//#endregion
|
|
63
|
-
//#region src/commands/doctor/checks/secret.ts
|
|
64
|
-
/** Verifies `.zitadel/secret` exists and parses. */
|
|
65
|
-
var SecretCheck = class extends AbstractSanityCheck {
|
|
66
|
-
name = "secret";
|
|
67
|
-
path = ".zitadel/secret";
|
|
68
|
-
summary = ".zitadel/secret parses";
|
|
69
|
-
async verify(ctx) {
|
|
70
|
-
await readZitadelSecret(ctx.cwd);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
//#endregion
|
|
74
|
-
//#region src/commands/doctor/checks/secret-permissions.ts
|
|
75
|
-
/** Verifies `.zitadel/secret` is locked down to `0600`, and re-locks it on fix. */
|
|
76
|
-
var SecretPermissionsCheck = class extends AbstractSanityCheck {
|
|
77
|
-
name = "secret-permissions";
|
|
78
|
-
path = ".zitadel/secret";
|
|
79
|
-
summary = ".zitadel/secret has 0600 permissions";
|
|
80
|
-
async verify(ctx) {
|
|
81
|
-
const mode = (await stat(join(ctx.cwd, ".zitadel/secret"))).mode & 511;
|
|
82
|
-
if (mode !== 384) throw new Error(`expected 0600, got ${mode.toString(8)}`);
|
|
83
|
-
}
|
|
84
|
-
async fix(ctx) {
|
|
85
|
-
if (ctx.dryRun) return;
|
|
86
|
-
await chmod(join(ctx.cwd, ".zitadel/secret"), 384);
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
//#endregion
|
|
90
|
-
//#region src/commands/doctor/checks/gitignore.ts
|
|
91
|
-
/** The entries `.gitignore` must carry to keep secrets and env files untracked. */
|
|
92
|
-
const REQUIRED_ENTRIES = [
|
|
93
|
-
".zitadel/secret",
|
|
94
|
-
".env*",
|
|
95
|
-
"!.env.example"
|
|
96
|
-
];
|
|
97
|
-
/** Verifies `.gitignore` excludes the local secret and env files; appends any missing. */
|
|
98
|
-
var GitignoreCheck = class extends AbstractSanityCheck {
|
|
99
|
-
name = "gitignore";
|
|
100
|
-
path = ".gitignore";
|
|
101
|
-
summary = ".gitignore protects local secret/env files";
|
|
102
|
-
async verify(ctx) {
|
|
103
|
-
const lines = (await readFile(join(ctx.cwd, ".gitignore"), "utf8")).split(/\r?\n/g);
|
|
104
|
-
for (const entry of REQUIRED_ENTRIES) if (!lines.includes(entry)) throw new Error(`missing ${entry}`);
|
|
105
|
-
}
|
|
106
|
-
async fix(ctx) {
|
|
107
|
-
const path = join(ctx.cwd, ".gitignore");
|
|
108
|
-
const existing = await readFile(path, "utf8").catch(() => "");
|
|
109
|
-
const lines = existing.split(/\r?\n/g);
|
|
110
|
-
const missing = REQUIRED_ENTRIES.filter((entry) => !lines.includes(entry));
|
|
111
|
-
if (missing.length === 0 || ctx.dryRun) return;
|
|
112
|
-
await writeFile(path, `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
//#endregion
|
|
116
|
-
//#region src/commands/doctor/checks/env-example.ts
|
|
117
|
-
/** The keys `.env.example` must document for a Zitadel-managed project. */
|
|
118
|
-
const REQUIRED_KEYS = [
|
|
119
|
-
"ZITADEL_PROJECT_ID",
|
|
120
|
-
"ZITADEL_ENVIRONMENT",
|
|
121
|
-
"ZITADEL_ISSUER"
|
|
122
|
-
];
|
|
123
|
-
/** Verifies `.env.example` documents the required Zitadel keys; appends any missing. */
|
|
124
|
-
var EnvExampleCheck = class extends AbstractSanityCheck {
|
|
125
|
-
name = "env-example";
|
|
126
|
-
path = ".env.example";
|
|
127
|
-
summary = ".env.example references required keys";
|
|
128
|
-
async verify(ctx) {
|
|
129
|
-
const contents = await readFile(join(ctx.cwd, ".env.example"), "utf8");
|
|
130
|
-
for (const key of REQUIRED_KEYS) if (!contents.includes(`${key}=`)) throw new Error(`missing ${key}`);
|
|
131
|
-
}
|
|
132
|
-
async fix(ctx) {
|
|
133
|
-
const path = join(ctx.cwd, ".env.example");
|
|
134
|
-
const existing = await readFile(path, "utf8").catch(() => "");
|
|
135
|
-
const missing = REQUIRED_KEYS.filter((key) => !existing.includes(`${key}=`));
|
|
136
|
-
if (missing.length === 0 || ctx.dryRun) return;
|
|
137
|
-
await writeFile(path, `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${missing.map((key) => `${key}=`).join("\n")}\n`);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
//#endregion
|
|
141
|
-
//#region src/commands/doctor/checks/framework.ts
|
|
142
|
-
/** Verifies the framework detected on disk matches the one recorded in config. */
|
|
143
|
-
var FrameworkCheck = class extends AbstractSanityCheck {
|
|
144
|
-
name = "framework";
|
|
145
|
-
path = "zitadel.json";
|
|
146
|
-
summary = "Detected framework matches recorded framework";
|
|
147
|
-
async verify(ctx) {
|
|
148
|
-
const config = await readZitadelConfig(ctx.cwd);
|
|
149
|
-
const detected = await ctx.orca.detect(ctx.cwd);
|
|
150
|
-
const recorded = isObject(config.framework) ? config.framework.id : void 0;
|
|
151
|
-
if (recorded !== detected.id) throw new Error(`expected ${String(recorded)}, detected ${detected.id}`);
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
//#endregion
|
|
155
|
-
//#region src/commands/doctor/patch-context.ts
|
|
156
|
-
/**
|
|
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.
|
|
166
|
-
*/
|
|
167
|
-
async function loadPatchContext(cwd, orca, cliVersion) {
|
|
168
|
-
const config = await readZitadelConfig(cwd);
|
|
169
|
-
const secret = await readZitadelSecret(cwd);
|
|
170
|
-
const framework = await orca.detect(cwd);
|
|
171
|
-
const scaffold = await readScaffoldManifest(cwd);
|
|
172
|
-
return {
|
|
173
|
-
framework,
|
|
174
|
-
rendererId: readRendererId(config),
|
|
175
|
-
issuer: await resolveIssuer(cwd, config, framework),
|
|
176
|
-
server: typeof config.server === "string" ? config.server : "",
|
|
177
|
-
cliVersion,
|
|
178
|
-
scaffoldedFramework: scaffold?.scaffolded_framework,
|
|
179
|
-
posture: scaffold?.posture ?? "page",
|
|
180
|
-
preset: readPreset(config),
|
|
181
|
-
useCase: readUseCase(config),
|
|
182
|
-
project: {
|
|
183
|
-
id: secret.project_id,
|
|
184
|
-
project_secret: secret.project_secret,
|
|
185
|
-
preview_secret: secret.preview_secret,
|
|
186
|
-
preview_origins: secret.preview_origins,
|
|
187
|
-
created_at: secret.created_at
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
async function resolveIssuer(cwd, config, facts) {
|
|
192
|
-
const fromConfig = readDevelopmentIssuer(config);
|
|
193
|
-
if (fromConfig && fromConfig.length > 0) return fromConfig;
|
|
194
|
-
const scaffold = await readScaffoldManifest(cwd);
|
|
195
|
-
if (typeof scaffold?.dev_port === "number") return issuerFromPort(scaffold.dev_port);
|
|
196
|
-
return facts.url;
|
|
197
|
-
}
|
|
198
|
-
//#endregion
|
|
199
|
-
//#region src/commands/doctor/checks/dependency.ts
|
|
200
|
-
/**
|
|
201
|
-
* Verifies the project still declares a Zitadel SDK dependency in
|
|
202
|
-
* `package.json`. The patcher adds a scoped package (e.g.
|
|
203
|
-
* `@zitadel/sdk-next`); the check is generic over the `@zitadel*`
|
|
204
|
-
* scope so any framework renderer's dependency satisfies it.
|
|
205
|
-
*/
|
|
206
|
-
var DependencyCheck = class extends AbstractSanityCheck {
|
|
207
|
-
name = "dependency";
|
|
208
|
-
path = "package.json";
|
|
209
|
-
summary = "package.json depends on a Zitadel SDK package";
|
|
210
|
-
async verify(ctx) {
|
|
211
|
-
const pkg = JSON.parse(await readFile(join(ctx.cwd, "package.json"), "utf8"));
|
|
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");
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Repairs by re-applying the patcher's managed artifacts: rebuilds the
|
|
216
|
-
* `PatchContext` from disk and calls `patcher.repair`, which re-adds the
|
|
217
|
-
* SDK dependency via its `add-dep` op. The exact package name is framework
|
|
218
|
-
* + renderer specific and known only to the patcher (which deliberately
|
|
219
|
-
* hides its file-op plan behind the family-neutral `Patcher` interface),
|
|
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.
|
|
223
|
-
*/
|
|
224
|
-
async fix(ctx) {
|
|
225
|
-
const patchCtx = await loadPatchContext(ctx.cwd, ctx.orca, ctx.cliVersion);
|
|
226
|
-
await ctx.orca.patcherFor(patchCtx.framework.id).repair(patchCtx, {
|
|
227
|
-
cwd: ctx.cwd,
|
|
228
|
-
dryRun: ctx.dryRun,
|
|
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 = 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
|
|
600
|
-
//#region src/commands/doctor/checks/project-match.ts
|
|
601
|
-
/** Verifies `.zitadel/secret`'s project_id matches `zitadel.json`'s project. */
|
|
602
|
-
var ProjectMatchCheck = class extends AbstractSanityCheck {
|
|
603
|
-
name = "project-match";
|
|
604
|
-
path = ".zitadel/secret";
|
|
605
|
-
summary = ".zitadel/secret project_id matches zitadel.json project";
|
|
606
|
-
async verify(ctx) {
|
|
607
|
-
const config = await readZitadelConfig(ctx.cwd);
|
|
608
|
-
const secret = await readZitadelSecret(ctx.cwd);
|
|
609
|
-
const configProject = typeof config.project === "string" ? config.project : void 0;
|
|
610
|
-
if (secret.project_id !== configProject) throw new Error(".zitadel/secret project_id does not match zitadel.json project");
|
|
611
|
-
}
|
|
612
|
-
};
|
|
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") {
|
|
725
|
-
const message = state.claimable ? `This project is temporary until you attach it to a team, and claiming is only possible within 14 days of creation. Run \`zitadel claim\` to make it permanent; nothing about the project changes.` : `The local record says this project's 14-day claim window has closed. Run \`zitadel claim\` to reconcile: a project attached from another machine reports its team, one never attached can no longer be claimed. Either way it keeps working; \`zitadel setup\` in a fresh directory gets you a new project you can attach.`;
|
|
726
|
-
return {
|
|
727
|
-
name: this.name,
|
|
728
|
-
status: "warn",
|
|
729
|
-
message,
|
|
730
|
-
details: { claimable: state.claimable },
|
|
731
|
-
path: this.path
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
return {
|
|
735
|
-
name: this.name,
|
|
736
|
-
status: "pass",
|
|
737
|
-
message: state.kind === "attached" ? `Project is attached to team ${state.team_id}` : "Project is not on a server where teams apply",
|
|
738
|
-
path: this.path
|
|
739
|
-
};
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* Deliberately a no-op. `doctor --fix` calls `fix` on every non-passing
|
|
743
|
-
* check, but claiming requires a human to sign in through a browser, so there
|
|
744
|
-
* is nothing safe to automate here.
|
|
745
|
-
*/
|
|
746
|
-
async fix(_ctx) {}
|
|
747
|
-
};
|
|
748
|
-
//#endregion
|
|
749
|
-
//#region src/commands/doctor/checks/index.ts
|
|
750
|
-
/** Every diagnostic the `doctor` command runs, in display order. */
|
|
751
|
-
const SANITY_CHECKS = [
|
|
752
|
-
new ConfigCheck(),
|
|
753
|
-
new SecretCheck(),
|
|
754
|
-
new SecretPermissionsCheck(),
|
|
755
|
-
new GitignoreCheck(),
|
|
756
|
-
new EnvExampleCheck(),
|
|
757
|
-
new FrameworkCheck(),
|
|
758
|
-
new SchemaCheck(),
|
|
759
|
-
new DependencyCheck(),
|
|
760
|
-
new DependencyVersionCheck(),
|
|
761
|
-
new ManagedFilesCheck(),
|
|
762
|
-
new ProjectMatchCheck(),
|
|
763
|
-
new ClaimCheck()
|
|
764
|
-
];
|
|
765
|
-
//#endregion
|
|
766
|
-
//#region src/commands/doctor/index.ts
|
|
767
|
-
const LOCAL_RUNTIME_CHECK_NAMES = new Set([
|
|
768
|
-
"server-binary",
|
|
769
|
-
"docker-cli",
|
|
770
|
-
"image",
|
|
771
|
-
"state-dir",
|
|
772
|
-
"port",
|
|
773
|
-
"runtime"
|
|
774
|
-
]);
|
|
775
|
-
/**
|
|
776
|
-
* `zitadel doctor` — verify generated files and local state.
|
|
777
|
-
*
|
|
778
|
-
* Runs every registered {@link SANITY_CHECKS} entry and emits the aggregate
|
|
779
|
-
* result; if any check fails it throws `E_VALIDATION` carrying the full check
|
|
780
|
-
* details. With `--fix`, each check that did not pass — failed or warned —
|
|
781
|
-
* first attempts its own repair (a no-op for checks with no safe automatic
|
|
782
|
-
* remedy), then the battery re-runs.
|
|
783
|
-
*
|
|
784
|
-
* The `--fix` loop is best-effort: a repair that throws (e.g. a missing
|
|
785
|
-
* prerequisite file the check itself would also flag) is logged at debug
|
|
786
|
-
* level and skipped, not propagated — the post-fix re-verify still reports
|
|
787
|
-
* whatever remains broken.
|
|
788
|
-
*/
|
|
789
|
-
var Doctor = class Doctor extends BaseCommand {
|
|
790
|
-
static description = "Verify local runtime and project state.";
|
|
791
|
-
static flags = {
|
|
792
|
-
fix: Flags.boolean({ description: "Repair missing files and stale managed wiring." }),
|
|
793
|
-
image: Flags.string({ description: "Container image to check." }),
|
|
794
|
-
port: Flags.integer({
|
|
795
|
-
description: "Local HTTP port.",
|
|
796
|
-
default: DEFAULT_LOCAL_SERVER_PORT
|
|
797
|
-
}),
|
|
798
|
-
runtime: Flags.string({
|
|
799
|
-
description: "Local runtime backend.",
|
|
800
|
-
options: ["binary", "docker"]
|
|
801
|
-
})
|
|
802
|
-
};
|
|
803
|
-
async run() {
|
|
804
|
-
const { flags } = await this.parse(Doctor);
|
|
805
|
-
const port = flags.port ?? 8080;
|
|
806
|
-
await this.toMeta(flags, {
|
|
807
|
-
resolveServer: false,
|
|
808
|
-
source: localServerUrl(port)
|
|
809
|
-
});
|
|
810
|
-
const { cwd, dryRun } = this.meta;
|
|
811
|
-
const existingRuntime = await readRuntimeMetadata(cwd);
|
|
812
|
-
const runtimeBackend = resolveRuntimeBackend({
|
|
813
|
-
runtime: flags.runtime,
|
|
814
|
-
image: flags.image,
|
|
815
|
-
envImage: this.meta.env.ZITADEL_LOCAL_IMAGE,
|
|
816
|
-
existingRuntime
|
|
817
|
-
});
|
|
818
|
-
const image = flags.image ?? this.meta.env.ZITADEL_LOCAL_IMAGE ?? defaultLocalServerImageForCliVersion(this.meta.cliVersion);
|
|
819
|
-
const runtimeChecks = await runLocalRuntimeChecks(cwd, runtimeBackend, image, port);
|
|
820
|
-
const hasConfig = await hasZitadelConfig(cwd);
|
|
821
|
-
const ctx = {
|
|
822
|
-
cwd,
|
|
823
|
-
orca: createOrca(),
|
|
824
|
-
cliVersion: this.meta.cliVersion,
|
|
825
|
-
dryRun
|
|
826
|
-
};
|
|
827
|
-
if (hasConfig && flags.fix) {
|
|
828
|
-
const before = await Promise.all(SANITY_CHECKS.map((check) => check.run(ctx)));
|
|
829
|
-
for (const [index, check] of SANITY_CHECKS.entries()) {
|
|
830
|
-
if (before[index]?.status === "pass") continue;
|
|
831
|
-
try {
|
|
832
|
-
await check.fix(ctx);
|
|
833
|
-
} catch (error) {
|
|
834
|
-
consola.debug(`doctor --fix: ${check.name} repair failed`, error);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
const projectChecks = hasConfig ? await Promise.all(SANITY_CHECKS.map((check) => check.run(ctx))) : [];
|
|
839
|
-
const checks = [...runtimeChecks, ...projectChecks];
|
|
840
|
-
const failed = checks.filter((check) => check.status === "fail");
|
|
841
|
-
const warnings = checks.filter((check) => check.status === "warn");
|
|
842
|
-
const warningAdvice = advisoryForWarnings(warnings, this.meta.cliVersion);
|
|
843
|
-
this.recordTelemetry({
|
|
844
|
-
runtime: runtimeBackend,
|
|
845
|
-
checks_total: checks.length,
|
|
846
|
-
checks_failed: failed.length,
|
|
847
|
-
checks_warn: warnings.length,
|
|
848
|
-
failed_checks: failed.length > 0 ? failed.map((check) => check.name).join(",") : void 0
|
|
849
|
-
});
|
|
850
|
-
const data = {
|
|
851
|
-
title: failed.length > 0 ? "Zitadel doctor found issues." : warnings.length > 0 ? "Zitadel doctor passed with warnings." : "Zitadel doctor passed.",
|
|
852
|
-
ok: failed.length === 0,
|
|
853
|
-
runtime: runtimeBackend,
|
|
854
|
-
...runtimeBackend === "docker" ? { image } : {},
|
|
855
|
-
port,
|
|
856
|
-
project: { lifecycle: hasConfig ? "configured" : "not-configured" },
|
|
857
|
-
checks,
|
|
858
|
-
...warningAdvice ? {
|
|
859
|
-
next_actions: warningAdvice.nextActions,
|
|
860
|
-
next_commands: warningAdvice.nextCommands
|
|
861
|
-
} : {}
|
|
862
|
-
};
|
|
863
|
-
if (failed.length > 0) {
|
|
864
|
-
const advice = failureAdvice(failed, image, port, this.meta.cliVersion);
|
|
865
|
-
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", {
|
|
866
|
-
hint: advice.hint,
|
|
867
|
-
nextCommands: advice.nextCommands,
|
|
868
|
-
details: data
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
return this.emit({
|
|
872
|
-
status: "ok",
|
|
873
|
-
data,
|
|
874
|
-
warnings: warnings.map((warning) => `${warning.name}: ${warning.message}`)
|
|
875
|
-
});
|
|
876
|
-
}
|
|
877
|
-
};
|
|
878
|
-
function failureAdvice(failed, image, port, cliVersion) {
|
|
879
|
-
const failedNames = new Set(failed.map((check) => check.name));
|
|
880
|
-
if (failedNames.has("server-binary")) return {
|
|
881
|
-
hint: "The Zitadel server npm package is not available. Reinstall the CLI package, then retry.",
|
|
882
|
-
nextCommands: [publicCliCommand("doctor", cliVersion)]
|
|
883
|
-
};
|
|
884
|
-
if (failedNames.has("docker-cli")) {
|
|
885
|
-
const advice = dockerRuntimeGuidance("doctor", cliVersion);
|
|
886
|
-
return {
|
|
887
|
-
hint: advice.hint,
|
|
888
|
-
nextCommands: advice.nextCommands
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
if (failedNames.has("image")) return {
|
|
892
|
-
hint: "The local Zitadel image is not available. Check Docker registry access, build it locally, or pass --image / ZITADEL_LOCAL_IMAGE.",
|
|
893
|
-
nextCommands: [`docker pull ${image}`, publicCliCommand("doctor", cliVersion)]
|
|
894
|
-
};
|
|
895
|
-
if (failedNames.has("state-dir")) return {
|
|
896
|
-
hint: "The local Zitadel state directory is not writable. Fix directory permissions, then rerun `zitadel doctor`.",
|
|
897
|
-
nextCommands: [publicCliCommand("doctor", cliVersion)]
|
|
898
|
-
};
|
|
899
|
-
if (failedNames.has("port")) {
|
|
900
|
-
const fallbackPort = port === 8080 ? port + 1 : DEFAULT_LOCAL_SERVER_PORT;
|
|
901
|
-
const stopCommand = publicCliCommand("stop --all", cliVersion);
|
|
902
|
-
const retryCommand = publicCliCommand("doctor", cliVersion);
|
|
903
|
-
const alternatePortCommand = publicCliCommand(`doctor --port ${String(fallbackPort)}`, cliVersion);
|
|
904
|
-
return {
|
|
905
|
-
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}\`.`,
|
|
906
|
-
nextCommands: [
|
|
907
|
-
stopCommand,
|
|
908
|
-
retryCommand,
|
|
909
|
-
alternatePortCommand
|
|
910
|
-
]
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
if (failedNames.has("runtime")) return {
|
|
914
|
-
hint: "Existing local runtime metadata was found, but the local Zitadel server is not healthy. Start it again or reset stale local data.",
|
|
915
|
-
nextCommands: [publicCliCommand("start", cliVersion), publicCliCommand("reset --force", cliVersion)]
|
|
916
|
-
};
|
|
917
|
-
const typed = failed.find((check) => check.code !== void 0 && check.hint !== void 0);
|
|
918
|
-
if (typed?.hint !== void 0) return {
|
|
919
|
-
hint: typed.hint,
|
|
920
|
-
nextCommands: [publicCliCommand("doctor", cliVersion)]
|
|
921
|
-
};
|
|
922
|
-
if (failed.some((check) => !LOCAL_RUNTIME_CHECK_NAMES.has(check.name))) return {
|
|
923
|
-
hint: `Run \`${publicCliCommand("doctor --fix", cliVersion)}\` to re-apply missing managed files.`,
|
|
924
|
-
nextCommands: [publicCliCommand("doctor --fix", cliVersion)]
|
|
925
|
-
};
|
|
926
|
-
return {
|
|
927
|
-
hint: "Fix the reported checks, then rerun `zitadel doctor`.",
|
|
928
|
-
nextCommands: [publicCliCommand("doctor", cliVersion)]
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
function advisoryForWarnings(warnings, cliVersion) {
|
|
932
|
-
const nextActions = [];
|
|
933
|
-
const nextCommands = [];
|
|
934
|
-
if (warnings.some((check) => check.name === "docker-cli")) {
|
|
935
|
-
const advice = dockerRuntimeGuidance("doctor", cliVersion);
|
|
936
|
-
nextActions.push(...advice.nextActions);
|
|
937
|
-
nextCommands.push(...advice.nextCommands);
|
|
938
|
-
}
|
|
939
|
-
const claimWarning = warnings.find((check) => check.name === "claim");
|
|
940
|
-
if (claimWarning) {
|
|
941
|
-
nextActions.push(claimWindowClosed(claimWarning) ? claimWindowClosedAction(cliVersion) : claimAction(cliVersion));
|
|
942
|
-
nextCommands.push(claimCommand(cliVersion));
|
|
943
|
-
}
|
|
944
|
-
if (hasManagedRuntimeProcesses(warnings.find((check) => check.name === "managed-runtime-processes"))) {
|
|
945
|
-
nextActions.push("Review other host-wide CLI-managed local Zitadel runtimes before starting a new one.");
|
|
946
|
-
nextCommands.push(publicCliCommand("stop --all", cliVersion));
|
|
947
|
-
}
|
|
948
|
-
const dependencyRemedy = remedyCommandOf(warnings.find((check) => check.name === "dependency-version"));
|
|
949
|
-
if (dependencyRemedy !== void 0) {
|
|
950
|
-
nextActions.push("Align the exactly-pinned @zitadel/* dependencies with the CLI version; scaffolded files and guidance target the CLI's train.");
|
|
951
|
-
nextCommands.push(dependencyRemedy);
|
|
952
|
-
}
|
|
953
|
-
if (nextActions.length === 0 && nextCommands.length === 0) return;
|
|
954
|
-
return {
|
|
955
|
-
nextActions: unique(nextActions),
|
|
956
|
-
nextCommands: unique(nextCommands)
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
async function runLocalRuntimeChecks(cwd, runtimeBackend, image, port) {
|
|
960
|
-
const runtime = await readRuntimeMetadata(cwd);
|
|
961
|
-
const managedRuntimeCheck = await checkManagedRuntimeProcesses(runtime);
|
|
962
|
-
if (runtimeBackend === "binary") return [
|
|
963
|
-
await check("server-binary", "Server npm package is available", async () => {
|
|
964
|
-
return `@zitadel/server ${await assertServerPackageAvailable()} is available`;
|
|
965
|
-
}),
|
|
966
|
-
await check("state-dir", "Local state directory is writable", async () => {
|
|
967
|
-
const probe = await assertLocalStateWritable(cwd);
|
|
968
|
-
return probe.checkedPath === probe.targetPath ? `${probe.targetPath} is writable` : `${probe.targetPath} can be created (${probe.checkedPath} is writable)`;
|
|
969
|
-
}, "warn"),
|
|
970
|
-
await check("port", `Port ${String(port)} is available`, () => checkPortAvailability(runtime, port)),
|
|
971
|
-
await checkRuntime(runtime, runtimeBackend),
|
|
972
|
-
managedRuntimeCheck
|
|
973
|
-
];
|
|
974
|
-
const docker = await check("docker-cli", "Docker is reachable", async () => {
|
|
975
|
-
let result;
|
|
976
|
-
try {
|
|
977
|
-
result = await dockerAvailable();
|
|
978
|
-
} catch (error) {
|
|
979
|
-
throw new Error(dockerUnavailableMessage(error), { cause: error });
|
|
980
|
-
}
|
|
981
|
-
if (result.status !== 0) throw new Error(dockerUnavailableMessage(result.stderr || "docker version failed"));
|
|
982
|
-
return `Docker engine ${result.stdout.trim() || "available"}`;
|
|
983
|
-
}, "warn");
|
|
984
|
-
return [
|
|
985
|
-
docker,
|
|
986
|
-
docker.status === "pass" ? await check("image", `Image ${image} is available`, async () => {
|
|
987
|
-
try {
|
|
988
|
-
return await imageAvailable(image) === "local" ? `Image ${image} is available locally` : `Image ${image} is available from the registry`;
|
|
989
|
-
} catch (error) {
|
|
990
|
-
throw new Error(imageUnavailableMessage(image, error), { cause: error });
|
|
991
|
-
}
|
|
992
|
-
}, "warn") : {
|
|
993
|
-
name: "image",
|
|
994
|
-
status: "warn",
|
|
995
|
-
message: "Skipped image check because Docker is not reachable."
|
|
996
|
-
},
|
|
997
|
-
await check("state-dir", "Local state directory is writable", async () => {
|
|
998
|
-
const probe = await assertLocalStateWritable(cwd);
|
|
999
|
-
return probe.checkedPath === probe.targetPath ? `${probe.targetPath} is writable` : `${probe.targetPath} can be created (${probe.checkedPath} is writable)`;
|
|
1000
|
-
}, "warn"),
|
|
1001
|
-
await check("port", `Port ${String(port)} is available`, () => checkPortAvailability(runtime, port)),
|
|
1002
|
-
await checkRuntime(runtime, runtimeBackend),
|
|
1003
|
-
managedRuntimeCheck
|
|
1004
|
-
];
|
|
1005
|
-
}
|
|
1006
|
-
async function checkPortAvailability(runtime, port) {
|
|
1007
|
-
if (runtime?.port === port && await checkLocalServerHealth(runtime.server_url)) return `${runtime.server_url} is already healthy`;
|
|
1008
|
-
const listeners = await listenersForPort(port);
|
|
1009
|
-
if (listeners.length > 0) throw new PortInUseCheckError(port, localServerUrl(port), listeners);
|
|
1010
|
-
return `Port ${String(port)} is available`;
|
|
1011
|
-
}
|
|
1012
|
-
var PortInUseCheckError = class extends Error {
|
|
1013
|
-
constructor(port, serverUrl, listeners) {
|
|
1014
|
-
super(`Port ${String(port)} is already in use by ${formatListeners(listeners)}`);
|
|
1015
|
-
this.port = port;
|
|
1016
|
-
this.serverUrl = serverUrl;
|
|
1017
|
-
this.listeners = listeners;
|
|
1018
|
-
}
|
|
1019
|
-
};
|
|
1020
|
-
async function checkManagedRuntimeProcesses(runtime) {
|
|
1021
|
-
const discovery = await discoverManagedRuntimeProcesses();
|
|
1022
|
-
if (!discovery.supported) return {
|
|
1023
|
-
name: "managed-runtime-processes",
|
|
1024
|
-
status: "warn",
|
|
1025
|
-
message: "Managed local runtime process discovery is unavailable.",
|
|
1026
|
-
details: {
|
|
1027
|
-
supported: false,
|
|
1028
|
-
error: discovery.error
|
|
1029
|
-
}
|
|
1030
|
-
};
|
|
1031
|
-
const processes = additionalManagedRuntimeProcesses(discovery.processes, runtime);
|
|
1032
|
-
if (processes.length === 0) return {
|
|
1033
|
-
name: "managed-runtime-processes",
|
|
1034
|
-
status: "pass",
|
|
1035
|
-
message: "No additional host-wide managed local runtime processes found.",
|
|
1036
|
-
details: {
|
|
1037
|
-
supported: true,
|
|
1038
|
-
scope: "host",
|
|
1039
|
-
processes: []
|
|
1040
|
-
}
|
|
1041
|
-
};
|
|
1042
|
-
return {
|
|
1043
|
-
name: "managed-runtime-processes",
|
|
1044
|
-
status: "warn",
|
|
1045
|
-
message: `${String(processes.length)} other host-wide managed local runtime process${processes.length === 1 ? "" : "es"} found.`,
|
|
1046
|
-
details: {
|
|
1047
|
-
supported: true,
|
|
1048
|
-
scope: "host",
|
|
1049
|
-
processes
|
|
1050
|
-
}
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
function additionalManagedRuntimeProcesses(processes, runtime) {
|
|
1054
|
-
if (runtime?.backend !== "binary") return processes;
|
|
1055
|
-
return processes.filter((processInfo) => processInfo.pid !== runtime.pid && processInfo.ppid !== runtime.pid);
|
|
1056
|
-
}
|
|
1057
|
-
async function checkRuntime(runtime, runtimeBackend) {
|
|
1058
|
-
return check("runtime", "Existing local runtime is healthy", async () => {
|
|
1059
|
-
if (!runtime) return "No existing runtime metadata";
|
|
1060
|
-
if (runtime.backend !== runtimeBackend) throw new Error(`Existing local runtime uses ${runtime.backend}; run start --runtime ${runtimeBackend} to switch backends.`);
|
|
1061
|
-
if (!await checkLocalServerHealth(runtime.server_url)) throw new Error(`${runtime.server_url} did not respond to /healthz`);
|
|
1062
|
-
return `${runtime.server_url} is healthy`;
|
|
1063
|
-
});
|
|
1064
|
-
}
|
|
1065
|
-
function formatListeners(listeners) {
|
|
1066
|
-
return listeners.map((listener) => [listener.command ?? "unknown", listener.pid ? `pid ${String(listener.pid)}` : void 0].filter(Boolean).join(" ")).join(", ");
|
|
1067
|
-
}
|
|
1068
|
-
/**
|
|
1069
|
-
* A warning may carry its own repair as `details.remedy_command` (today: the
|
|
1070
|
-
* dependency-version check's package-manager-aware exact-pin install).
|
|
1071
|
-
* Surfacing that string keeps the structured advisory identical to the
|
|
1072
|
-
* command quoted in the warning's prose.
|
|
1073
|
-
*/
|
|
1074
|
-
function remedyCommandOf(check) {
|
|
1075
|
-
if (check?.status !== "warn") return;
|
|
1076
|
-
const details = check.details;
|
|
1077
|
-
if (typeof details !== "object" || details === null) return;
|
|
1078
|
-
const remedy = details.remedy_command;
|
|
1079
|
-
return typeof remedy === "string" && remedy.length > 0 ? remedy : void 0;
|
|
1080
|
-
}
|
|
1081
|
-
/**
|
|
1082
|
-
* Reads the claim check's window classification out of its details. Absent
|
|
1083
|
-
* or malformed details mean "not closed". The classification only selects
|
|
1084
|
-
* the advisory wording (deadline nudge vs reconciliation); the claim
|
|
1085
|
-
* command stays suggested either way, because the local record may be stale
|
|
1086
|
-
* and running claim lets the server answer authoritatively.
|
|
1087
|
-
*/
|
|
1088
|
-
function claimWindowClosed(check) {
|
|
1089
|
-
const details = check.details;
|
|
1090
|
-
return typeof details === "object" && details !== null && details.claimable === false;
|
|
1091
|
-
}
|
|
1092
|
-
function hasManagedRuntimeProcesses(check) {
|
|
1093
|
-
if (!check || check.status !== "warn") return false;
|
|
1094
|
-
const details = check.details;
|
|
1095
|
-
return typeof details === "object" && details !== null && "supported" in details && details.supported === true && Array.isArray(details.processes) && (details.processes?.length ?? 0) > 0;
|
|
1096
|
-
}
|
|
1097
|
-
function unique(values) {
|
|
1098
|
-
return [...new Set(values)];
|
|
1099
|
-
}
|
|
1100
|
-
function resolveRuntimeBackend(input) {
|
|
1101
|
-
if (input.runtime === "binary" || input.runtime === "docker") return input.runtime;
|
|
1102
|
-
if (input.existingRuntime) return input.existingRuntime.backend;
|
|
1103
|
-
if (input.image || input.envImage) return "docker";
|
|
1104
|
-
return "binary";
|
|
1105
|
-
}
|
|
1106
|
-
async function check(name, fallback, run, failureStatus = "fail") {
|
|
1107
|
-
try {
|
|
1108
|
-
return {
|
|
1109
|
-
name,
|
|
1110
|
-
status: "pass",
|
|
1111
|
-
message: await run()
|
|
1112
|
-
};
|
|
1113
|
-
} catch (error) {
|
|
1114
|
-
if (error instanceof PortInUseCheckError) return {
|
|
1115
|
-
name,
|
|
1116
|
-
status: failureStatus,
|
|
1117
|
-
message: error.message,
|
|
1118
|
-
details: {
|
|
1119
|
-
port: error.port,
|
|
1120
|
-
server_url: error.serverUrl,
|
|
1121
|
-
listeners: error.listeners
|
|
1122
|
-
}
|
|
1123
|
-
};
|
|
1124
|
-
return {
|
|
1125
|
-
name,
|
|
1126
|
-
status: failureStatus,
|
|
1127
|
-
message: error instanceof Error ? error.message : fallback
|
|
1128
|
-
};
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
function imageUnavailableMessage(image, error) {
|
|
1132
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
1133
|
-
return `Image ${image} is not available to Docker${detail.trim() ? ` (${detail.trim()})` : ""}; \`zitadel start\` may need a pull or a different image.`;
|
|
1134
|
-
}
|
|
1135
|
-
//#endregion
|
|
1136
|
-
export { Doctor as default };
|
|
1137
|
-
|
|
1138
|
-
//# sourceMappingURL=doctor.mjs.map
|