@zitadel/cli 0.1.0-alpha.16 → 0.1.0-alpha.17
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/SKILLS.md +10 -1
- package/dist/commands/apply.mjs +10 -6
- package/dist/commands/apply.mjs.map +1 -1
- package/dist/commands/doctor.mjs +6 -7
- 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 +5 -4
- package/dist/commands/plan.mjs.map +1 -1
- package/dist/commands/reset.mjs +2 -2
- package/dist/commands/schemas/list.mjs +10 -6
- package/dist/commands/schemas/list.mjs.map +1 -1
- package/dist/commands/setup.mjs +31 -89
- package/dist/commands/setup.mjs.map +1 -1
- package/dist/commands/start.mjs +3 -4
- package/dist/commands/start.mjs.map +1 -1
- package/dist/commands/status.mjs +4 -4
- package/dist/commands/stop.mjs +3 -3
- package/dist/{docker-BbX0yOpg.mjs → docker-pGcTrDIY.mjs} +2 -2
- package/dist/{docker-BbX0yOpg.mjs.map → docker-pGcTrDIY.mjs.map} +1 -1
- package/dist/{environment-ABj1IWZt.mjs → environment-BQF7LeCz.mjs} +1 -1
- package/dist/{environment-ABj1IWZt.mjs.map → environment-BQF7LeCz.mjs.map} +1 -1
- package/dist/{journey-guidance-E_3_UwGt.mjs → journey-guidance-CYVpQszz.mjs} +2 -2
- package/dist/{journey-guidance-E_3_UwGt.mjs.map → journey-guidance-CYVpQszz.mjs.map} +1 -1
- package/dist/{oclif-SjHLB_XK.mjs → oclif-BRdjVNne.mjs} +21 -2
- package/dist/oclif-BRdjVNne.mjs.map +1 -0
- package/dist/{orca-_TmrJQMw.mjs → orca-Dip6DPJ2.mjs} +2 -2
- package/dist/{orca-_TmrJQMw.mjs.map → orca-Dip6DPJ2.mjs.map} +1 -1
- package/dist/{ports-XujDtA47.mjs → ports-V8JfiNgT.mjs} +25 -20
- package/dist/ports-V8JfiNgT.mjs.map +1 -0
- package/dist/{processes-p2PPbKGG.mjs → processes-BVqYsxT8.mjs} +1 -1
- package/dist/{processes-p2PPbKGG.mjs.map → processes-BVqYsxT8.mjs.map} +1 -1
- package/dist/{project-DXc7q4wN.mjs → project-BGgv7Bba.mjs} +2 -2
- package/dist/{project-DXc7q4wN.mjs.map → project-BGgv7Bba.mjs.map} +1 -1
- package/dist/{sync-Bu_iLIbb.mjs → sync-Cd3z7Q1R.mjs} +82 -5
- package/dist/sync-Cd3z7Q1R.mjs.map +1 -0
- package/oclif.manifest.json +1 -1
- package/package.json +6 -6
- package/dist/docker-guidance-3UZESTfb.mjs +0 -21
- package/dist/docker-guidance-3UZESTfb.mjs.map +0 -1
- package/dist/oclif-SjHLB_XK.mjs.map +0 -1
- package/dist/ports-XujDtA47.mjs.map +0 -1
- package/dist/sync-Bu_iLIbb.mjs.map +0 -1
|
@@ -1,23 +1,33 @@
|
|
|
1
|
+
import { x as publicCliCommand } from "./oclif-BRdjVNne.mjs";
|
|
1
2
|
import { execFile } from "node:child_process";
|
|
3
|
+
//#region src/lib/local-server/docker-guidance.ts
|
|
4
|
+
function dockerUnavailableMessage(error) {
|
|
5
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
6
|
+
return `Docker is not reachable${detail.trim() ? ` (${detail.trim()})` : ""}; managed local runtime commands need Docker, but remote or cloud setup can continue with --server <url>.`;
|
|
7
|
+
}
|
|
8
|
+
function dockerRuntimeGuidance(retryCommand, cliVersion) {
|
|
9
|
+
return {
|
|
10
|
+
hint: "Docker is required only for the managed local runtime (`zitadel start` and `--server local`). Start Docker, then retry; or target a remote server with `--server <url>`.",
|
|
11
|
+
nextActions: [
|
|
12
|
+
"Start Docker Desktop, Docker Engine, or a Docker-compatible runtime such as Colima.",
|
|
13
|
+
`Run \`docker version\`, then rerun \`${publicCliCommand(retryCommand, cliVersion)}\`.`,
|
|
14
|
+
"If you are not using the managed local runtime, run setup with `--server <url>` instead."
|
|
15
|
+
],
|
|
16
|
+
nextCommands: ["docker version", publicCliCommand(retryCommand, cliVersion)]
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
2
20
|
//#region src/lib/prober/ports.ts
|
|
3
21
|
/**
|
|
4
|
-
* Enumerate TCP
|
|
5
|
-
*
|
|
6
|
-
* -P -n -F
|
|
7
|
-
*
|
|
22
|
+
* Enumerate TCP LISTEN sockets on loopback/wildcard interfaces (`127.0.0.1`,
|
|
23
|
+
* `::1`, or the wildcard `*`) with enough metadata for CLI diagnostics.
|
|
24
|
+
* Spawns `lsof -iTCP -sTCP:LISTEN -P -n -F pcn` and parses its
|
|
25
|
+
* machine-readable output.
|
|
8
26
|
*
|
|
9
27
|
* Never throws. Returns `[]` whenever lsof is unavailable (e.g. Windows,
|
|
10
28
|
* unusual PATH), the spawn errors, exits non-zero, or the call exceeds
|
|
11
29
|
* `timeoutMs` (default 1000ms). The caller treats an empty list the same as
|
|
12
|
-
* "no listeners
|
|
13
|
-
*/
|
|
14
|
-
async function listListeningPorts(opts) {
|
|
15
|
-
return uniqueSortedPorts(await listListeningTcpListeners(opts));
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Enumerate TCP LISTEN sockets on loopback/wildcard interfaces with enough
|
|
19
|
-
* metadata for CLI diagnostics. Never throws; an empty array means either no
|
|
20
|
-
* visible listeners or that process inspection is unavailable.
|
|
30
|
+
* "no visible listeners."
|
|
21
31
|
*/
|
|
22
32
|
async function listListeningTcpListeners(opts) {
|
|
23
33
|
const timeoutMs = opts?.timeoutMs ?? 1e3;
|
|
@@ -96,16 +106,11 @@ function parseLsofListeners(stdout) {
|
|
|
96
106
|
}
|
|
97
107
|
return listeners.sort((a, b) => a.port - b.port || (a.pid ?? 0) - (b.pid ?? 0));
|
|
98
108
|
}
|
|
99
|
-
function uniqueSortedPorts(listeners) {
|
|
100
|
-
const ports = /* @__PURE__ */ new Set();
|
|
101
|
-
for (const listener of listeners) ports.add(listener.port);
|
|
102
|
-
return [...ports].sort((a, b) => a - b);
|
|
103
|
-
}
|
|
104
109
|
/** Recognised loopback host strings as emitted by `lsof -F n`. */
|
|
105
110
|
function isLoopback(host) {
|
|
106
111
|
return host === "*" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
|
|
107
112
|
}
|
|
108
113
|
//#endregion
|
|
109
|
-
export {
|
|
114
|
+
export { dockerRuntimeGuidance as n, dockerUnavailableMessage as r, listenersForPort as t };
|
|
110
115
|
|
|
111
|
-
//# sourceMappingURL=ports-
|
|
116
|
+
//# sourceMappingURL=ports-V8JfiNgT.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports-V8JfiNgT.mjs","names":[],"sources":["../src/lib/local-server/docker-guidance.ts","../src/lib/prober/ports.ts"],"sourcesContent":["import { publicCliCommand } from \"../public-cli\";\n\nexport type DockerRuntimeGuidance = {\n hint: string;\n nextActions: string[];\n nextCommands: string[];\n};\n\nexport function dockerUnavailableMessage(error: unknown): string {\n const detail = error instanceof Error ? error.message : String(error);\n const suffix = detail.trim() ? ` (${detail.trim()})` : \"\";\n return `Docker is not reachable${suffix}; managed local runtime commands need Docker, but remote or cloud setup can continue with --server <url>.`;\n}\n\nexport function dockerRuntimeGuidance(\n retryCommand: \"doctor\" | \"start\",\n cliVersion: string,\n): DockerRuntimeGuidance {\n return {\n hint:\n \"Docker is required only for the managed local runtime (`zitadel start` and `--server local`). Start Docker, then retry; or target a remote server with `--server <url>`.\",\n nextActions: [\n \"Start Docker Desktop, Docker Engine, or a Docker-compatible runtime such as Colima.\",\n `Run \\`docker version\\`, then rerun \\`${publicCliCommand(retryCommand, cliVersion)}\\`.`,\n \"If you are not using the managed local runtime, run setup with `--server <url>` instead.\",\n ],\n nextCommands: [\"docker version\", publicCliCommand(retryCommand, cliVersion)],\n };\n}\n","import { execFile } from \"node:child_process\";\n\nexport type TcpListener = Readonly<{\n address: string;\n command?: string;\n host: string;\n pid?: number;\n port: number;\n}>;\n\n/**\n * Enumerate TCP LISTEN sockets on loopback/wildcard interfaces (`127.0.0.1`,\n * `::1`, or the wildcard `*`) with enough metadata for CLI diagnostics.\n * Spawns `lsof -iTCP -sTCP:LISTEN -P -n -F pcn` and parses its\n * machine-readable output.\n *\n * Never throws. Returns `[]` whenever lsof is unavailable (e.g. Windows,\n * unusual PATH), the spawn errors, exits non-zero, or the call exceeds\n * `timeoutMs` (default 1000ms). The caller treats an empty list the same as\n * \"no visible listeners.\"\n */\nexport async function listListeningTcpListeners(opts?: {\n readonly timeoutMs?: number;\n}): Promise<ReadonlyArray<TcpListener>> {\n const timeoutMs = opts?.timeoutMs ?? 1000;\n try {\n const stdout = await runLsof(timeoutMs);\n return parseLsofListeners(stdout);\n } catch {\n return [];\n }\n}\n\nexport async function listenersForPort(\n port: number,\n opts?: { readonly timeoutMs?: number },\n): Promise<ReadonlyArray<TcpListener>> {\n return (await listListeningTcpListeners(opts)).filter((listener) => listener.port === port);\n}\n\n/**\n * Spawn `lsof` with the canned argv and resolve to its stdout. Hand-rolled\n * rather than `util.promisify(execFile)` because the latter resolves with a\n * `{stdout, stderr}` object via its custom-promisify symbol — a heavier shape\n * to mock and worse to read at the call site, where we only ever want stdout.\n */\nfunction runLsof(timeoutMs: number): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n execFile(\n \"lsof\",\n [\"-iTCP\", \"-sTCP:LISTEN\", \"-P\", \"-n\", \"-F\", \"pcn\"],\n { timeout: timeoutMs, encoding: \"utf8\" },\n (err, stdout) => {\n if (err) {\n reject(err);\n return;\n }\n resolve(stdout);\n },\n );\n });\n}\n\n/**\n * Parse the pid (`p`), command (`c`), and name/address (`n`) records emitted by\n * `lsof -F pcn`. Address records end in `:<port>` (e.g. `n*:8080`,\n * `n127.0.0.1:3000`, `n[::1]:5050`). Only loopback/wildcard hosts are kept;\n * external interface bindings are ignored.\n */\nfunction parseLsofListeners(stdout: string): ReadonlyArray<TcpListener> {\n const listeners: TcpListener[] = [];\n let pid: number | undefined;\n let command: string | undefined;\n for (const line of stdout.split(/\\r?\\n/)) {\n if (line.startsWith(\"p\")) {\n const parsed = Number.parseInt(line.slice(1), 10);\n pid = Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;\n command = undefined;\n continue;\n }\n if (line.startsWith(\"c\")) {\n command = line.slice(1) || undefined;\n continue;\n }\n if (!line.startsWith(\"n\")) {\n continue;\n }\n const address = line.slice(1);\n const colon = address.lastIndexOf(\":\");\n if (colon < 0) {\n continue;\n }\n const host = address.slice(0, colon);\n const portStr = address.slice(colon + 1);\n if (!isLoopback(host)) {\n continue;\n }\n const port = Number.parseInt(portStr, 10);\n if (Number.isFinite(port) && port > 0 && port < 65536) {\n listeners.push({ address, command, host, pid, port });\n }\n }\n return listeners.sort((a, b) => a.port - b.port || (a.pid ?? 0) - (b.pid ?? 0));\n}\n\n/** Recognised loopback host strings as emitted by `lsof -F n`. */\nfunction isLoopback(host: string): boolean {\n return host === \"*\" || host === \"127.0.0.1\" || host === \"[::1]\" || host === \"::1\";\n}\n"],"mappings":";;;AAQA,SAAgB,yBAAyB,OAAwB;CAC/D,MAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAErE,QAAO,0BADQ,OAAO,MAAM,GAAG,KAAK,OAAO,MAAM,CAAC,KAAK,GACf;;AAG1C,SAAgB,sBACd,cACA,YACuB;AACvB,QAAO;EACL,MACE;EACF,aAAa;GACX;GACA,wCAAwC,iBAAiB,cAAc,WAAW,CAAC;GACnF;GACD;EACD,cAAc,CAAC,kBAAkB,iBAAiB,cAAc,WAAW,CAAC;EAC7E;;;;;;;;;;;;;;;ACNH,eAAsB,0BAA0B,MAER;CACtC,MAAM,YAAY,MAAM,aAAa;AACrC,KAAI;AAEF,SAAO,mBAAmB,MADL,QAAQ,UAAU,CACN;SAC3B;AACN,SAAO,EAAE;;;AAIb,eAAsB,iBACpB,MACA,MACqC;AACrC,SAAQ,MAAM,0BAA0B,KAAK,EAAE,QAAQ,aAAa,SAAS,SAAS,KAAK;;;;;;;;AAS7F,SAAS,QAAQ,WAAoC;AACnD,QAAO,IAAI,SAAiB,SAAS,WAAW;AAC9C,WACE,QACA;GAAC;GAAS;GAAgB;GAAM;GAAM;GAAM;GAAM,EAClD;GAAE,SAAS;GAAW,UAAU;GAAQ,GACvC,KAAK,WAAW;AACf,OAAI,KAAK;AACP,WAAO,IAAI;AACX;;AAEF,WAAQ,OAAO;IAElB;GACD;;;;;;;;AASJ,SAAS,mBAAmB,QAA4C;CACtE,MAAM,YAA2B,EAAE;CACnC,IAAI;CACJ,IAAI;AACJ,MAAK,MAAM,QAAQ,OAAO,MAAM,QAAQ,EAAE;AACxC,MAAI,KAAK,WAAW,IAAI,EAAE;GACxB,MAAM,SAAS,OAAO,SAAS,KAAK,MAAM,EAAE,EAAE,GAAG;AACjD,SAAM,OAAO,SAAS,OAAO,IAAI,SAAS,IAAI,SAAS,KAAA;AACvD,aAAU,KAAA;AACV;;AAEF,MAAI,KAAK,WAAW,IAAI,EAAE;AACxB,aAAU,KAAK,MAAM,EAAE,IAAI,KAAA;AAC3B;;AAEF,MAAI,CAAC,KAAK,WAAW,IAAI,CACvB;EAEF,MAAM,UAAU,KAAK,MAAM,EAAE;EAC7B,MAAM,QAAQ,QAAQ,YAAY,IAAI;AACtC,MAAI,QAAQ,EACV;EAEF,MAAM,OAAO,QAAQ,MAAM,GAAG,MAAM;EACpC,MAAM,UAAU,QAAQ,MAAM,QAAQ,EAAE;AACxC,MAAI,CAAC,WAAW,KAAK,CACnB;EAEF,MAAM,OAAO,OAAO,SAAS,SAAS,GAAG;AACzC,MAAI,OAAO,SAAS,KAAK,IAAI,OAAO,KAAK,OAAO,MAC9C,WAAU,KAAK;GAAE;GAAS;GAAS;GAAM;GAAK;GAAM,CAAC;;AAGzD,QAAO,UAAU,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,MAAM,EAAE,OAAO,GAAG;;;AAIjF,SAAS,WAAW,MAAuB;AACzC,QAAO,SAAS,OAAO,SAAS,eAAe,SAAS,WAAW,SAAS"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"processes-
|
|
1
|
+
{"version":3,"file":"processes-BVqYsxT8.mjs","names":[],"sources":["../src/lib/local-server/processes.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { basename } from \"node:path\";\n\nexport type ManagedRuntimeProcess = Readonly<{\n command: string;\n data_dir?: string;\n kind: \"server-binary\" | \"server-wrapper\";\n pid: number;\n ppid?: number;\n}>;\n\nexport type ManagedRuntimeDiscovery = Readonly<{\n error?: string;\n processes: ReadonlyArray<ManagedRuntimeProcess>;\n supported: boolean;\n}>;\n\ntype ProcessRow = Readonly<{\n command: string;\n pid: number;\n ppid?: number;\n}>;\n\nexport async function discoverManagedRuntimeProcesses(opts?: {\n readonly timeoutMs?: number;\n}): Promise<ManagedRuntimeDiscovery> {\n if (process.platform === \"win32\") {\n return { supported: false, processes: [], error: \"process sweep is not available on Windows\" };\n }\n\n let rows: ReadonlyArray<ProcessRow>;\n try {\n rows = parsePsRows(await runPs([\"axo\", \"pid=,ppid=,command=\"], opts?.timeoutMs ?? 1000));\n } catch (error) {\n return { supported: false, processes: [], error: errorMessage(error) };\n }\n\n const candidates = rows.filter((row) => isRuntimeCandidate(row.command));\n const hydrated = await Promise.all(\n candidates.map(async (row) => ({\n ...row,\n command: await hydrateCommand(row.pid, row.command, opts?.timeoutMs ?? 1000),\n })),\n );\n const processes = hydrated\n .map(toManagedRuntimeProcess)\n .filter((candidate): candidate is ManagedRuntimeProcess => candidate !== undefined);\n return { supported: true, processes: uniqueByPid(processes) };\n}\n\nfunction runPs(args: string[], timeoutMs: number): Promise<string> {\n return new Promise<string>((resolve, reject) => {\n execFile(\"ps\", args, { timeout: timeoutMs, encoding: \"utf8\" }, (err, stdout) => {\n if (err) {\n reject(err);\n return;\n }\n resolve(stdout);\n });\n });\n}\n\nasync function hydrateCommand(pid: number, fallback: string, timeoutMs: number): Promise<string> {\n try {\n const hydrated = await runPs([\"eww\", \"-p\", String(pid), \"-o\", \"command=\"], timeoutMs);\n return hydrated.trim() || fallback;\n } catch {\n return fallback;\n }\n}\n\nfunction parsePsRows(stdout: string): ReadonlyArray<ProcessRow> {\n const rows: ProcessRow[] = [];\n for (const line of stdout.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (!trimmed) {\n continue;\n }\n const match = trimmed.match(/^(\\d+)\\s+(\\d+)\\s+(.+)$/);\n if (!match) {\n continue;\n }\n rows.push({\n pid: Number(match[1]),\n ppid: Number(match[2]),\n command: match[3] ?? \"\",\n });\n }\n return rows;\n}\n\nfunction toManagedRuntimeProcess(row: ProcessRow): ManagedRuntimeProcess | undefined {\n const dataDir = extractDataDir(row.command);\n const wrapper = isServerWrapper(row.command);\n const binary = isServerBinary(row.command);\n if (!wrapper && !binary) {\n return undefined;\n }\n if (!wrapper && !isCliLocalDataDir(dataDir)) {\n return undefined;\n }\n return {\n pid: row.pid,\n ppid: row.ppid,\n command: stripEnvironmentNoise(row.command),\n kind: wrapper ? \"server-wrapper\" : \"server-binary\",\n ...(dataDir ? { data_dir: dataDir } : {}),\n };\n}\n\nfunction isRuntimeCandidate(command: string): boolean {\n return (\n isServerWrapper(command) ||\n isServerBinary(command) ||\n command.includes(\"NEXTGEN_SERVER_DATA_DIR=\") ||\n basename(command.split(/\\s+/)[0] ?? \"\") === \"nextgen\"\n );\n}\n\nfunction isServerWrapper(command: string): boolean {\n return command.includes(\"zitadel-server.js\");\n}\n\nfunction isServerBinary(command: string): boolean {\n if (/@zitadel\\/server-[^/\\s]+\\/bin\\/nextgen(?:\\.exe)?(?:\\s|$)/.test(command)) {\n return true;\n }\n return (\n isCliLocalDataDir(extractDataDir(command)) &&\n commandTokens(command).some((token) => basename(token) === \"nextgen\")\n );\n}\n\nfunction extractDataDir(command: string): string | undefined {\n const match = command.match(/NEXTGEN_SERVER_DATA_DIR=(\"[^\"]+\"|'[^']+'|[^\\s]+)/);\n const value = match?.[1];\n if (!value) {\n return undefined;\n }\n return value.replace(/^[\"']|[\"']$/g, \"\");\n}\n\nfunction isCliLocalDataDir(path: string | undefined): boolean {\n return Boolean(path && /(?:^|\\/)\\.zitadel\\/local\\/nextgen-data\\/?$/.test(path));\n}\n\nfunction stripEnvironmentNoise(command: string): string {\n return command\n .replace(/(^|\\s)[A-Z_][A-Z0-9_]*=(?:\"[^\"]+\"|'[^']+'|[^\\s]+)/g, \" \")\n .replace(/\\s{2,}/g, \" \")\n .trim();\n}\n\nfunction commandTokens(command: string): string[] {\n return command.split(/\\s+/).filter((token) => !/^[A-Z_][A-Z0-9_]*=/.test(token));\n}\n\nfunction uniqueByPid(\n processes: ReadonlyArray<ManagedRuntimeProcess>,\n): ReadonlyArray<ManagedRuntimeProcess> {\n const byPid = new Map<number, ManagedRuntimeProcess>();\n for (const processInfo of processes) {\n byPid.set(processInfo.pid, processInfo);\n }\n return [...byPid.values()].sort((a, b) => a.pid - b.pid);\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as ZitadelError, T as parseJsonObject, w as isObject } from "./oclif-BRdjVNne.mjs";
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
//#region src/lib/project.ts
|
|
@@ -84,4 +84,4 @@ function isNotFound(error) {
|
|
|
84
84
|
//#endregion
|
|
85
85
|
export { readZitadelConfig as a, readRendererId as i, hasZitadelSecret as n, readZitadelSecret as o, readDevelopmentIssuer as r, hasZitadelConfig as t };
|
|
86
86
|
|
|
87
|
-
//# sourceMappingURL=project-
|
|
87
|
+
//# sourceMappingURL=project-BGgv7Bba.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-
|
|
1
|
+
{"version":3,"file":"project-BGgv7Bba.mjs","names":[],"sources":["../src/lib/project.ts"],"sourcesContent":["import { readFile, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { ZitadelError } from \"./errors\";\nimport { isObject, parseJsonObject } from \"./json\";\n\n/**\n * Reports whether `cwd` has already been initialized, i.e. a committed\n * `zitadel.json` exists. Used to decide whether setup should run or skip.\n */\nexport async function hasZitadelConfig(cwd: string): Promise<boolean> {\n return exists(join(cwd, \"zitadel.json\"));\n}\n\n/**\n * Reports whether local secret material (`.zitadel/secret`) is present. Gates\n * commands that need credentials, and signals that secrets were already pulled.\n */\nexport async function hasZitadelSecret(cwd: string): Promise<boolean> {\n return exists(join(cwd, \".zitadel/secret\"));\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch (error) {\n if (isNotFound(error)) {\n return false;\n }\n throw error;\n }\n}\n\n/**\n * Shape of the project secret persisted at `.zitadel/secret`. Holds the\n * project identity plus the credentials used to talk to the platform in\n * preview and production. Validated structurally by {@link readZitadelSecret}.\n */\nexport type ZitadelSecret = {\n project_id: string;\n project_secret: string;\n preview_secret: string;\n preview_origins: string[];\n created_at: string;\n};\n\n/**\n * Reads and parses `zitadel.json` into a plain object. Translates a missing\n * file into an actionable `E_VALIDATION` error pointing at `zitadel setup`;\n * other errors (e.g. malformed JSON) propagate unchanged.\n */\nexport async function readZitadelConfig(cwd: string): Promise<Record<string, unknown>> {\n try {\n return parseJsonObject(await readFile(join(cwd, \"zitadel.json\"), \"utf8\"), \"zitadel.json\");\n } catch (error) {\n if (isNotFound(error)) {\n throw new ZitadelError(\"E_VALIDATION\", \"zitadel.json was not found\", {\n hint: \"Run `zitadel setup` first.\",\n nextCommands: [\"zitadel setup\"],\n });\n }\n throw error;\n }\n}\n\n/**\n * Reads, parses, and structurally validates `.zitadel/secret`, returning it\n * as a {@link ZitadelSecret}. A missing file becomes an actionable\n * `E_VALIDATION` error pointing at `zitadel setup` / `zitadel doctor --fix`;\n * a present-but-incomplete file throws so callers never proceed with partial\n * credentials.\n */\nexport async function readZitadelSecret(cwd: string): Promise<ZitadelSecret> {\n try {\n const secret = parseJsonObject(\n await readFile(join(cwd, \".zitadel/secret\"), \"utf8\"),\n \".zitadel/secret\",\n );\n if (\n typeof secret.project_id !== \"string\" ||\n typeof secret.project_secret !== \"string\" ||\n typeof secret.preview_secret !== \"string\" ||\n !Array.isArray(secret.preview_origins)\n ) {\n throw new Error(\".zitadel/secret is missing required fields\");\n }\n return secret as ZitadelSecret;\n } catch (error) {\n if (isNotFound(error)) {\n throw new ZitadelError(\"E_VALIDATION\", \".zitadel/secret was not found\", {\n hint: \"Run `zitadel setup` first, or restore the project secret with `zitadel doctor --fix`.\",\n nextCommands: [\"zitadel setup\", \"zitadel doctor --fix\"],\n });\n }\n throw error;\n }\n}\n\n/**\n * Reads the configured renderer id from a parsed `zitadel.json`, normalising the\n * legacy `default` alias to `react` and falling back to `react` when unset. The\n * value is validated downstream by `getRenderer`, so callers need not re-check.\n */\nexport function readRendererId(config: Record<string, unknown>): string {\n const branding = isObject(config.branding) ? config.branding : undefined;\n const value = branding && typeof branding.renderer === \"string\" ? branding.renderer : \"react\";\n return value === \"default\" ? \"react\" : value;\n}\n\n/** Reads `environments.development.issuer` from a parsed `zitadel.json`, if present. */\nexport function readDevelopmentIssuer(config: Record<string, unknown>): string | undefined {\n if (isObject(config.environments) && isObject(config.environments.development)) {\n const issuer = config.environments.development.issuer;\n return typeof issuer === \"string\" ? issuer : undefined;\n }\n return undefined;\n}\n\nfunction isNotFound(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as { code?: string }).code === \"ENOENT\"\n );\n}\n"],"mappings":";;;;;;;;AAUA,eAAsB,iBAAiB,KAA+B;AACpE,QAAO,OAAO,KAAK,KAAK,eAAe,CAAC;;;;;;AAO1C,eAAsB,iBAAiB,KAA+B;AACpE,QAAO,OAAO,KAAK,KAAK,kBAAkB,CAAC;;AAG7C,eAAe,OAAO,MAAgC;AACpD,KAAI;AACF,QAAM,KAAK,KAAK;AAChB,SAAO;UACA,OAAO;AACd,MAAI,WAAW,MAAM,CACnB,QAAO;AAET,QAAM;;;;;;;;AAsBV,eAAsB,kBAAkB,KAA+C;AACrF,KAAI;AACF,SAAO,gBAAgB,MAAM,SAAS,KAAK,KAAK,eAAe,EAAE,OAAO,EAAE,eAAe;UAClF,OAAO;AACd,MAAI,WAAW,MAAM,CACnB,OAAM,IAAI,aAAa,gBAAgB,8BAA8B;GACnE,MAAM;GACN,cAAc,CAAC,gBAAgB;GAChC,CAAC;AAEJ,QAAM;;;;;;;;;;AAWV,eAAsB,kBAAkB,KAAqC;AAC3E,KAAI;EACF,MAAM,SAAS,gBACb,MAAM,SAAS,KAAK,KAAK,kBAAkB,EAAE,OAAO,EACpD,kBACD;AACD,MACE,OAAO,OAAO,eAAe,YAC7B,OAAO,OAAO,mBAAmB,YACjC,OAAO,OAAO,mBAAmB,YACjC,CAAC,MAAM,QAAQ,OAAO,gBAAgB,CAEtC,OAAM,IAAI,MAAM,6CAA6C;AAE/D,SAAO;UACA,OAAO;AACd,MAAI,WAAW,MAAM,CACnB,OAAM,IAAI,aAAa,gBAAgB,iCAAiC;GACtE,MAAM;GACN,cAAc,CAAC,iBAAiB,uBAAuB;GACxD,CAAC;AAEJ,QAAM;;;;;;;;AASV,SAAgB,eAAe,QAAyC;CACtE,MAAM,WAAW,SAAS,OAAO,SAAS,GAAG,OAAO,WAAW,KAAA;CAC/D,MAAM,QAAQ,YAAY,OAAO,SAAS,aAAa,WAAW,SAAS,WAAW;AACtF,QAAO,UAAU,YAAY,UAAU;;;AAIzC,SAAgB,sBAAsB,QAAqD;AACzF,KAAI,SAAS,OAAO,aAAa,IAAI,SAAS,OAAO,aAAa,YAAY,EAAE;EAC9E,MAAM,SAAS,OAAO,aAAa,YAAY;AAC/C,SAAO,OAAO,WAAW,WAAW,SAAS,KAAA;;;AAKjD,SAAS,WAAW,OAAyB;AAC3C,QACE,OAAO,UAAU,YACjB,UAAU,QACV,UAAU,SACT,MAA4B,SAAS"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as ZitadelError, E as stableStringify, w as isObject } from "./oclif-BRdjVNne.mjs";
|
|
2
2
|
import { t as SCHEMAS_DIR } from "./user-schema-DDz5-lX5.mjs";
|
|
3
3
|
import { readFile, readdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -265,9 +265,14 @@ function defaultSwapWarning(action, flowCreatePaths, trackedFlowPaths) {
|
|
|
265
265
|
if (trackedFlowPaths.filter((path) => path !== action.path).length + flowCreatePaths.filter((path) => path !== action.path).length === 0) return;
|
|
266
266
|
return {
|
|
267
267
|
rule: "warn/default-flow-swap",
|
|
268
|
-
message: `
|
|
268
|
+
message: `once applied, this flow becomes the new default for ${humanList(purposes)}: pages that do not explicitly set flow-name on <zitadel-login> will start rendering it. If that is not intended, scope it with "audience" or set flow-name on the pages that should use it.`
|
|
269
269
|
};
|
|
270
270
|
}
|
|
271
|
+
/** ["login", "register"] → "login and register" (Oxford-free prose list). */
|
|
272
|
+
function humanList(items) {
|
|
273
|
+
if (items.length <= 1) return items[0] ?? "";
|
|
274
|
+
return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
275
|
+
}
|
|
271
276
|
/**
|
|
272
277
|
* Locate the local content of the schema a flow's `user_schema` pins.
|
|
273
278
|
* A repin action already names the schema file (the pin is adopting that
|
|
@@ -479,6 +484,7 @@ async function runSyncLoop(cwd, syncers) {
|
|
|
479
484
|
const actions = await buildSyncPlan(cwd, syncers);
|
|
480
485
|
for (const action of actions) if (action.kind === "create" || action.kind === "update") for (const warning of action.warnings ?? []) consola$1.warn(`${action.path}: ${warning.message}`);
|
|
481
486
|
const filesUpdated = [];
|
|
487
|
+
const applied = [];
|
|
482
488
|
const repinned = /* @__PURE__ */ new Map();
|
|
483
489
|
const writeBack = async (action, canonical, fallbackHash) => {
|
|
484
490
|
if (!canonical) return fallbackHash;
|
|
@@ -504,6 +510,12 @@ async function runSyncLoop(cwd, syncers) {
|
|
|
504
510
|
};
|
|
505
511
|
await updateState(cwd, action.path, entry);
|
|
506
512
|
consola$1.info(`Created a new ${action.syncer.kind} on Zitadel from ${action.path} (id ${id})`);
|
|
513
|
+
applied.push({
|
|
514
|
+
kind: action.syncer.kind,
|
|
515
|
+
action: "create",
|
|
516
|
+
file: action.path,
|
|
517
|
+
id
|
|
518
|
+
});
|
|
507
519
|
break;
|
|
508
520
|
}
|
|
509
521
|
case "revise": {
|
|
@@ -520,6 +532,13 @@ async function runSyncLoop(cwd, syncers) {
|
|
|
520
532
|
filesUpdated.push(flowPath);
|
|
521
533
|
consola$1.info(`Re-pinned user_schema in ${flowPath} to ${id}`);
|
|
522
534
|
}
|
|
535
|
+
applied.push({
|
|
536
|
+
kind: action.syncer.kind,
|
|
537
|
+
action: "revision",
|
|
538
|
+
file: action.path,
|
|
539
|
+
id,
|
|
540
|
+
previous_id: action.previousId
|
|
541
|
+
});
|
|
523
542
|
break;
|
|
524
543
|
}
|
|
525
544
|
case "update": {
|
|
@@ -539,12 +558,24 @@ async function runSyncLoop(cwd, syncers) {
|
|
|
539
558
|
const fallbackHash = newId ? hashForState(action.syncer, content) : action.hash;
|
|
540
559
|
await updateState(cwd, action.path, { hash: await writeBack(action, canonical, fallbackHash) });
|
|
541
560
|
consola$1.info(`Updated the ${action.syncer.kind} on Zitadel from ${action.path}`);
|
|
561
|
+
applied.push({
|
|
562
|
+
kind: action.syncer.kind,
|
|
563
|
+
action: "update",
|
|
564
|
+
file: action.path,
|
|
565
|
+
id: action.id
|
|
566
|
+
});
|
|
542
567
|
break;
|
|
543
568
|
}
|
|
544
569
|
case "delete":
|
|
545
570
|
await action.syncer.delete(action.id);
|
|
546
571
|
await removeFromState(cwd, action.path);
|
|
547
572
|
consola$1.info(`Deleted the ${action.syncer.kind} on Zitadel because ${action.path} was removed locally`);
|
|
573
|
+
applied.push({
|
|
574
|
+
kind: action.syncer.kind,
|
|
575
|
+
action: "delete",
|
|
576
|
+
file: action.path,
|
|
577
|
+
id: action.id
|
|
578
|
+
});
|
|
548
579
|
break;
|
|
549
580
|
case "skip":
|
|
550
581
|
consola$1.debug(`Skipped ${action.path} (${action.reason})`);
|
|
@@ -553,7 +584,10 @@ async function runSyncLoop(cwd, syncers) {
|
|
|
553
584
|
const remainingPins = new Set((await readLocalFlowUserSchemas(cwd)).values());
|
|
554
585
|
const finalState = await readState(cwd);
|
|
555
586
|
for (const [path, entry] of Object.entries(finalState.resources)) if (entry.previousId && !remainingPins.has(entry.previousId)) await updateState(cwd, path, { previousId: void 0 });
|
|
556
|
-
return {
|
|
587
|
+
return {
|
|
588
|
+
filesUpdated: [...new Set(filesUpdated)],
|
|
589
|
+
applied
|
|
590
|
+
};
|
|
557
591
|
}
|
|
558
592
|
/**
|
|
559
593
|
* Rewrite a flow file's `user_schema` pin from `previousId` to `newId`,
|
|
@@ -720,6 +754,49 @@ function collectPlanWarnings(actions) {
|
|
|
720
754
|
return out;
|
|
721
755
|
}
|
|
722
756
|
/**
|
|
757
|
+
* Enumerate the non-`skip` actions of a {@link buildSyncPlan} result as
|
|
758
|
+
* envelope-ready {@link PlanResourceChange} rows. Pure; the structural
|
|
759
|
+
* counterpart of {@link summarizePlan} — counts and rows always agree.
|
|
760
|
+
*/
|
|
761
|
+
function enumeratePlanResources(actions) {
|
|
762
|
+
const out = [];
|
|
763
|
+
for (const action of actions) switch (action.kind) {
|
|
764
|
+
case "create":
|
|
765
|
+
out.push({
|
|
766
|
+
kind: action.syncer.kind,
|
|
767
|
+
action: "create",
|
|
768
|
+
file: action.path
|
|
769
|
+
});
|
|
770
|
+
break;
|
|
771
|
+
case "update":
|
|
772
|
+
out.push({
|
|
773
|
+
kind: action.syncer.kind,
|
|
774
|
+
action: "update",
|
|
775
|
+
file: action.path,
|
|
776
|
+
id: action.id
|
|
777
|
+
});
|
|
778
|
+
break;
|
|
779
|
+
case "revise":
|
|
780
|
+
out.push({
|
|
781
|
+
kind: action.syncer.kind,
|
|
782
|
+
action: "revision",
|
|
783
|
+
file: action.path,
|
|
784
|
+
previous_id: action.previousId
|
|
785
|
+
});
|
|
786
|
+
break;
|
|
787
|
+
case "delete":
|
|
788
|
+
out.push({
|
|
789
|
+
kind: action.syncer.kind,
|
|
790
|
+
action: "delete",
|
|
791
|
+
file: action.path,
|
|
792
|
+
id: action.id
|
|
793
|
+
});
|
|
794
|
+
break;
|
|
795
|
+
case "skip": break;
|
|
796
|
+
}
|
|
797
|
+
return out;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
723
800
|
* Render a {@link buildSyncPlan} result as a human-readable Terraform-style
|
|
724
801
|
* plan. TTY-aware: colors and bold are emitted only when `tty` is true.
|
|
725
802
|
* Returns the empty-state message when every action is `skip`.
|
|
@@ -1077,6 +1154,6 @@ function renderWarnings(warnings, blkPad, tty, lines) {
|
|
|
1077
1154
|
for (const warning of warnings ?? []) lines.push(paint(`${blkPad}# warning: ${warning.message}`, A.yellow, tty));
|
|
1078
1155
|
}
|
|
1079
1156
|
//#endregion
|
|
1080
|
-
export {
|
|
1157
|
+
export { buildSyncPlan as a, writeBackResource as c, FLOWS_DIR as d, summarizePlan as i, updateState as l, enumeratePlanResources as n, hashForState as o, renderPlan as r, runSyncLoop as s, collectPlanWarnings as t, makeSyncers as u };
|
|
1081
1158
|
|
|
1082
|
-
//# sourceMappingURL=sync-
|
|
1159
|
+
//# sourceMappingURL=sync-Cd3z7Q1R.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-Cd3z7Q1R.mjs","names":[],"sources":["../src/lib/flows/env-refs.ts","../src/lib/flows/index.ts","../src/lib/sync/syncers.ts","../src/lib/sync/flow-validation.ts","../src/lib/sync/state.ts","../src/lib/sync/loop.ts","../src/lib/sync/plan-renderer.ts"],"sourcesContent":["import { isObject } from \"../json\";\n\n/**\n * Collects the environment variables a flows document depends on, sorted and\n * de-duplicated. Recognises two reference styles: inline `${VAR}` interpolations\n * inside string values, and keys ending in `_env` whose value names a single\n * variable. `apply`/`plan` use this to fail before contacting the platform when\n * a required variable is absent.\n */\nexport function flowEnvRefs(value: unknown): string[] {\n const refs = new Set<string>();\n const visit = (node: unknown): void => {\n if (typeof node === \"string\") {\n for (const match of node.matchAll(/\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g)) {\n const ref = match[1];\n if (ref) {\n refs.add(ref);\n }\n }\n } else if (Array.isArray(node)) {\n node.forEach(visit);\n } else if (isObject(node)) {\n for (const [key, child] of Object.entries(node)) {\n if (key.endsWith(\"_env\") && typeof child === \"string\" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(child)) {\n refs.add(child);\n } else {\n visit(child);\n }\n }\n }\n };\n visit(value);\n return [...refs].sort();\n}\n","/**\n * Public surface for the flow domain. Every caller outside this module\n * imports from here (not from individual files) so the package\n * boundary stays observable.\n *\n * **Source of truth.** The wire shape lives in\n * `@zitadel/api/generated/model` (orval-generated from the\n * OpenAPI spec). Callers that need the type import\n * `CreateFlowDefinitionBodyFlowDefinition` from there directly;\n * callers that need the runtime validator import\n * `CreateFlowDefinitionBody` from\n * `@zitadel/api/generated/endpoints/zitadelNextGen.zod`. This\n * module owns only the CLI-specific concerns: the password-flow\n * builder, env-var reference scanning, and the file-level\n * `validateFlows` helper that surfaces `E_VALIDATION` errors against\n * the generated Zod.\n *\n * **Dependency rule.** No upward imports (`commands/`, `sync/`, etc.)\n * and no filesystem I/O. It depends sideways only on shared utilities\n * under `apps/cli/src/lib/` — today `lib/errors` (`ZitadelError`).\n */\nexport { buildFlow } from \"./build\";\nexport { validateFlows } from \"./validate\";\nexport { flowEnvRefs } from \"./env-refs\";\n\n/**\n * Relative directory (from the project root) where local flow files\n * live. Owned here so callers (`commands/*`, `sync/syncers.ts`) and\n * tests share a single source of truth for the path; the runtime\n * never depends on it directly because `lib/flows` does not touch\n * the filesystem.\n */\nexport const FLOWS_DIR = \".zitadel/flows\";\n","import type {\n CreateFlowDefinition201,\n CreateFlowDefinitionBodyFlowDefinition,\n UpdateFlowDefinition200,\n UpdateFlowDefinitionBodyFlowDefinition,\n CreateSchemaBody,\n GetSchemaById200,\n GetFlowDefinition200,\n} from \"@zitadel/api/generated/model\";\nimport { consola } from \"consola\";\n\nimport type { ZitadelClient } from \"@zitadel/api/client\";\nimport { DEFAULT_FLOW_SCHEMA_URI } from \"@zitadel/config/defaults\";\nimport { normalizeFlowBody, normalizeSchemaBody } from \"@zitadel/config/normalize\";\nimport { flowConfigSchema, schemaConfigSchema } from \"@zitadel/config/schemas\";\n\nimport { FLOWS_DIR, flowEnvRefs } from \"../flows\";\nimport { SCHEMAS_DIR } from \"../user-schema\";\nimport { ZitadelError } from \"../errors\";\nimport type { ResourceSyncer } from \"./types.js\";\n\n/** Runtime environment lookup used to resolve `${VAR}` / `*_env` references. */\ntype EnvLookup = Record<string, string | undefined>;\n\n/**\n * Build the syncer list with the context every syncer needs: the\n * `project_id` flow creates carry, and the runtime `env` against which\n * each file's `${VAR}` / `*_env` references are checked. Callers (apply /\n * plan / setup) read `project_id` from `.zitadel/secret` and pass the\n * process environment. The returned array is treated as read-only by the\n * sync loop.\n */\nexport function makeSyncers(opts: {\n client: ZitadelClient;\n projectId: string;\n env: EnvLookup;\n}): ReadonlyArray<ResourceSyncer> {\n return [\n new SchemaSyncer(opts.client, opts.projectId, opts.env),\n new FlowDefinitionSyncer(opts.client, opts.projectId, opts.env),\n ];\n}\n\n/**\n * Assert that every env var a resource references — `${VAR}` placeholders and\n * the `*_env` convention — is present in `env`, throwing `E_VALIDATION` listing\n * the missing names. Shared by every syncer so the check is identical for\n * schemas and flows, and runs in the sync engine before any platform call.\n */\nfunction assertEnvRefs(data: object, env: EnvLookup): void {\n const missing = flowEnvRefs(data).filter((name) => !env[name]);\n if (missing.length > 0) {\n throw new ZitadelError(\"E_VALIDATION\", `Missing environment variables: ${missing.join(\", \")}`);\n }\n}\n\nclass SchemaSyncer implements ResourceSyncer {\n readonly kind = \"schema\";\n readonly directory = SCHEMAS_DIR;\n readonly mutable = false;\n readonly revisioned = true;\n readonly normalize = normalizeSchemaBody;\n // Deliberately no `normalizeWrite`: the server stores schema bytes\n // verbatim, so stripping spelled-out x-* defaults from the local file\n // would drop them from the next published revision. Canonical schema\n // bodies are written back as-is; `normalize` is comparison-only.\n\n constructor(\n private readonly client: ZitadelClient,\n private readonly projectId: string,\n private readonly env: EnvLookup,\n ) {}\n\n /**\n * Parse against the generated `CreateSchemaBody` Zod (the orval-emitted\n * equivalent of `api/openapi/endpoints/schemas/user-schema.yaml`). The\n * generated schema is a union of `user-schema` and `schema-url`\n * discriminated on `kind`; both are valid on-disk bodies.\n */\n validate(data: object): void {\n const result = schemaConfigSchema.safeParse(data);\n if (!result.success) {\n throw new ZitadelError(\"E_VALIDATION\", \"Schema file is not a valid Zitadel schema body\", {\n details: { issues: result.error.issues },\n });\n }\n assertEnvRefs(data, this.env);\n }\n\n /**\n * `POST /schemas` mints a new immutable row. The server allocates the\n * opaque id; the CLI records it in state and re-pins flows against it.\n * The create response carries only the id, so the canonical stored body\n * comes from a follow-up fetch; a fetch failure degrades to no\n * write-back rather than failing the create.\n */\n async create(data: object): Promise<{ id: string; canonical?: object }> {\n const result = await this.client.createSchema(data as CreateSchemaBody, {\n project_id: this.projectId,\n });\n try {\n return { id: result.id, canonical: await this.fetch(result.id) };\n } catch (err) {\n consola.debug(`fetch created schema ${result.id} failed:`, err);\n return { id: result.id };\n }\n }\n\n /**\n * Not called by the sync loop: schemas are `revisioned`, so a hash change\n * publishes a new immutable revision through {@link create} rather than\n * mutating an existing row. Kept as a required interface member; throws\n * loudly if a caller reaches it.\n */\n async update(_id: string, _data: object): Promise<{ canonical?: object }> {\n throw new ZitadelError(\"E_NOT_IMPLEMENTED\", \"schemas are revisioned — edit publishes a new revision, not an update\");\n }\n\n async delete(id: string): Promise<void> {\n // Schemas are immutable on the platform: no PATCH, no DELETE in the\n // generated client. The sync loop's delete branch (`loop.ts`) still\n // schedules a delete action when a state entry exists and the\n // on-disk file is gone — `mutable` only gates updates, not deletes.\n // We deliberately fail loud here so the user notices that removing\n // a schema file is not a supported way to retire it.\n throw new ZitadelError(\"E_NOT_IMPLEMENTED\", `schema delete is not supported (${id})`);\n }\n\n async fetch(id: string): Promise<object> {\n const body = await this.client.getSchemaById(encodeURIComponent(id), {\n project_id: this.projectId,\n });\n return body as unknown as GetSchemaById200;\n }\n}\n\nclass FlowDefinitionSyncer implements ResourceSyncer {\n readonly kind = \"flow\";\n readonly directory = FLOWS_DIR;\n readonly mutable = true;\n readonly revisioned = false;\n readonly normalize = normalizeFlowBody;\n // For flows the comparison form doubles as the file form: everything it\n // strips (envelope keys, the empty `audience` echo) is transport noise.\n readonly normalizeWrite = normalizeFlowBody;\n\n constructor(\n private readonly client: ZitadelClient,\n private readonly projectId: string,\n private readonly env: EnvLookup,\n ) {}\n\n /**\n * Validates one flow file against the canonical `flowConfigSchema` (the\n * same Zod `validateFlows` and doctor use), then checks env references.\n */\n validate(data: object): void {\n const result = flowConfigSchema.safeParse(data);\n if (!result.success) {\n throw new ZitadelError(\"E_VALIDATION\", \"Flow file is not a valid Zitadel flow body\", {\n details: { issues: result.error.issues },\n });\n }\n assertEnvRefs(data, this.env);\n }\n\n /**\n * Wraps the bare on-disk flow body in the spec's create-envelope\n * (`api/openapi/components/flows/flow-definition-create-request.yaml`)\n * before sending. The file on disk stays bare so it is human-editable;\n * only the wire request carries `project_id` and the surrounding\n * envelope.\n */\n async create(data: object): Promise<{ id: string; canonical?: object }> {\n const result = (await this.client.createFlowDefinition({\n project_id: this.projectId,\n schema_uri: DEFAULT_FLOW_SCHEMA_URI,\n flow_definition: data as CreateFlowDefinitionBodyFlowDefinition,\n })) as CreateFlowDefinition201;\n return { id: result.id, canonical: result.flow_definition as object };\n }\n\n /**\n * PUT completely replaces the flow definition. The wire request wraps the\n * bare on-disk flow in the `{ flow_definition }` update envelope\n * (`api/openapi/components/flows/flow-definition-update-request.yaml`) and\n * carries `project_id` as a query parameter; the file on disk stays bare so\n * it is human-editable.\n */\n async update(id: string, data: object): Promise<{ canonical?: object }> {\n const result = (await this.client.updateFlowDefinition(\n id,\n { flow_definition: data as UpdateFlowDefinitionBodyFlowDefinition },\n { project_id: this.projectId },\n )) as UpdateFlowDefinition200;\n return { canonical: result.flow_definition as object };\n }\n\n async delete(id: string): Promise<void> {\n await this.client.deleteFlowDefinition(id, { project_id: this.projectId });\n }\n\n /**\n * `GET /flow_definitions/:id` returns a detail envelope with metadata\n * (`id`, `project_id`, `created_at`, `updated_at`) plus `flow_definition`.\n * Return only `flow_definition` so diffs compare with the on-disk bare body.\n */\n async fetch(id: string): Promise<object> {\n const envelope = (await this.client.getFlowDefinition(\n id,\n { project_id: this.projectId },\n )) as GetFlowDefinition200;\n\n return envelope.flow_definition as object;\n }\n}\n","import { consola } from \"consola\";\n\nimport { validateFlowDefinition, type FlowValidationIssue } from \"@zitadel/config/validate\";\n\nimport { ZitadelError } from \"../errors\";\nimport { FLOWS_DIR } from \"../flows\";\nimport type { ResourceEntry, SyncAction } from \"./types.js\";\n\n/**\n * Pre-flight semantic validation for every flow this plan uploads,\n * mirroring the server-side `ValidateFlowDefinition` (see\n * `@zitadel/config/validate`). Runs inside `buildSyncPlan`, so `plan`\n * reports the violations the server would only reveal on apply — and\n * `apply` fails before any platform mutation instead of half-applied\n * (a schema revision would otherwise publish before the flow update\n * fails).\n *\n * Only actions that upload a flow (create/update, including repin-forced\n * updates) are validated: an unchanged flow is never re-judged, so a\n * validator behavior change cannot break an already-applied project.\n *\n * Warning-severity issues ANNOTATE the actions in place (`action.warnings`)\n * for plan rendering — `actions` is deliberately typed mutable because the\n * caller (`buildSyncPlan`, which owns the array it just built) expects the\n * annotations. Error-severity issues aggregate into one E_VALIDATION\n * carrying every violation.\n *\n * Escape hatch: setting `ZITADEL_SKIP_FLOW_VALIDATION` skips the check —\n * insurance against a port bug rejecting something the server accepts.\n */\nexport function validatePlannedFlows(opts: {\n actions: SyncAction[];\n scannedContents: ReadonlyMap<string, object>;\n stateResources: Readonly<Record<string, ResourceEntry>>;\n}): void {\n if (process.env.ZITADEL_SKIP_FLOW_VALIDATION) {\n consola.warn(\"Flow validation skipped (ZITADEL_SKIP_FLOW_VALIDATION is set)\");\n return;\n }\n\n const failures: Array<{ path: string; issue: FlowValidationIssue }> = [];\n let anyRepin = false;\n\n // The default-swap warning needs plan-wide context: a project's very\n // first flow becoming the default is expected, an additional one\n // silently taking over /login is the footgun.\n const flowCreatePaths = opts.actions\n .filter((a) => a.kind === \"create\" && a.syncer.kind === \"flow\")\n .map((a) => a.path);\n const trackedFlowPaths = Object.keys(opts.stateResources).filter((path) =>\n path.startsWith(`${FLOWS_DIR}/`),\n );\n\n for (const action of opts.actions) {\n if (action.kind !== \"create\" && action.kind !== \"update\") {\n continue;\n }\n if (action.syncer.kind !== \"flow\") {\n continue;\n }\n\n const issues = validateFlowDefinition(\n action.content,\n resolveSchemaContent(action, opts.scannedContents, opts.stateResources),\n );\n const warnings = issues.filter((issue) => issue.severity === \"warning\");\n if (warnings.length > 0) {\n action.warnings = warnings.map(({ rule, message }) => ({ rule, message }));\n }\n for (const issue of issues) {\n if (issue.severity === \"error\") {\n failures.push({ path: action.path, issue });\n anyRepin ||= action.repin !== undefined;\n }\n }\n\n const swapWarning = defaultSwapWarning(action, flowCreatePaths, trackedFlowPaths);\n if (swapWarning !== undefined) {\n action.warnings = [...(action.warnings ?? []), swapWarning];\n }\n }\n\n if (failures.length === 0) {\n return;\n }\n\n const noun = failures.length === 1 ? \"issue\" : \"issues\";\n throw new ZitadelError(\n \"E_VALIDATION\",\n `Flow validation failed (${failures.length} ${noun}):\\n` +\n failures.map(({ path, issue }) => ` - ${path}: ${issue.message}`).join(\"\\n\"),\n {\n hint:\n \"These are the same rules the server enforces on apply. Fix the flow definition(s) and re-run plan.\" +\n (anyRepin\n ? \" A failing flow is adopting an edited schema revision — update its steps[].fields to match the edited schema (or restore the removed/renamed properties).\"\n : \"\"),\n details: {\n issues: failures.map(({ path, issue }) => ({\n path,\n rule: issue.rule,\n message: issue.message,\n ...(issue.step === undefined ? {} : { step: issue.step }),\n })),\n },\n },\n );\n}\n\n/**\n * Warn when applying a NEW active, unscoped flow into a project that\n * already has flows: the engine's default selection is newest-unscoped-\n * wins (ties broken by id), so this flow silently becomes what\n * `<zitadel-login>` renders for its purposes unless the widget pins a\n * `flow-name`. Deliberately engine-level rather than a validator rule:\n * the verdict depends on the action kind and the rest of the plan/state,\n * which `validateFlowDefinition` never sees. A project's first flow is\n * exempt — becoming the default is the point of creating it.\n */\nfunction defaultSwapWarning(\n action: Extract<SyncAction, { kind: \"create\" | \"update\" }>,\n flowCreatePaths: readonly string[],\n trackedFlowPaths: readonly string[],\n): { rule: string; message: string } | undefined {\n if (action.kind !== \"create\") {\n return undefined; // updates keep their created_at; the default cannot move\n }\n const body = action.content as {\n status?: unknown;\n purposes?: Record<string, unknown>;\n audience?: { team_ids?: unknown[]; app_ids?: unknown[] };\n };\n if (body.status !== \"active\") {\n return undefined;\n }\n const scoped =\n (body.audience?.team_ids?.length ?? 0) > 0 || (body.audience?.app_ids?.length ?? 0) > 0;\n if (scoped) {\n return undefined;\n }\n const purposes = Object.keys(body.purposes ?? {});\n if (purposes.length === 0) {\n return undefined;\n }\n const otherFlows =\n trackedFlowPaths.filter((path) => path !== action.path).length +\n flowCreatePaths.filter((path) => path !== action.path).length;\n if (otherFlows === 0) {\n return undefined;\n }\n return {\n rule: \"warn/default-flow-swap\",\n message:\n `once applied, this flow becomes the new default for ${humanList(purposes)}: ` +\n `pages that do not explicitly set flow-name on <zitadel-login> will start rendering it. ` +\n `If that is not intended, scope it with \"audience\" or set flow-name on the pages that should use it.`,\n };\n}\n\n/** [\"login\", \"register\"] → \"login and register\" (Oxford-free prose list). */\nfunction humanList(items: readonly string[]): string {\n if (items.length <= 1) return items[0] ?? \"\";\n return `${items.slice(0, -1).join(\", \")} and ${items[items.length - 1]}`;\n}\n\n/**\n * Locate the local content of the schema a flow's `user_schema` pins.\n * A repin action already names the schema file (the pin is adopting that\n * file's next/new revision); otherwise the pinned id is looked up in\n * state and its file body taken from this scan. Unresolvable refs (e.g.\n * an un-substituted `${USER_SCHEMA_URL}` placeholder or a server-side\n * schema not tracked locally) return undefined — flow-local rules still\n * run, schema-dependent rules are skipped.\n */\nfunction resolveSchemaContent(\n action: Extract<SyncAction, { kind: \"create\" | \"update\" }>,\n scannedContents: ReadonlyMap<string, object>,\n stateResources: Readonly<Record<string, ResourceEntry>>,\n): object | undefined {\n if (action.repin) {\n return scannedContents.get(action.repin.schemaPath);\n }\n const ref = (action.content as { user_schema?: unknown }).user_schema;\n if (typeof ref !== \"string\" || ref === \"\") {\n return undefined;\n }\n for (const [path, entry] of Object.entries(stateResources)) {\n if (entry.id === ref) {\n return scannedContents.get(path);\n }\n }\n consola.debug(`flow validation: no local schema for user_schema ${ref}; flow-local rules only`);\n return undefined;\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport type { ResourceEntry, ZitadelState } from \"./types.js\";\n\n/**\n * Read and parse `.zitadel/state.json`. Throws if the file is\n * missing or malformed; callers run `zitadel setup` first to bring\n * the file into existence.\n */\nexport async function readState(cwd: string): Promise<ZitadelState> {\n const raw = await readFile(join(cwd, \".zitadel/state.json\"), \"utf8\");\n return JSON.parse(raw) as ZitadelState;\n}\n\n/**\n * Merge an entry into the state file under `key`, preserving any\n * fields the caller did not override. Reads the file, writes it back\n * with sorted keys disabled (state is engine-managed, not human-\n * authored, so deterministic ordering isn't required here).\n */\nexport async function updateState(\n cwd: string,\n key: string,\n entry: ResourceEntry,\n): Promise<void> {\n const current = await readState(cwd);\n const updated: ZitadelState = {\n ...current,\n resources: {\n ...current.resources,\n [key]: { ...current.resources[key], ...entry },\n },\n };\n await writeFile(join(cwd, \".zitadel/state.json\"), JSON.stringify(updated, null, 2));\n}\n\n/**\n * Remove an entry from the state file. No-op if the key is absent.\n */\nexport async function removeFromState(cwd: string, key: string): Promise<void> {\n const current = await readState(cwd);\n const { [key]: _removed, ...rest } = current.resources;\n const updated: ZitadelState = { ...current, resources: rest };\n await writeFile(join(cwd, \".zitadel/state.json\"), JSON.stringify(updated, null, 2));\n}\n","import { createHash } from \"node:crypto\";\nimport { readdir, readFile, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { consola } from \"consola\";\n\nimport { FLOWS_DIR } from \"../flows\";\nimport { stableStringify } from \"../json\";\nimport { validatePlannedFlows } from \"./flow-validation.js\";\nimport type { PlanResourceChange } from \"./plan-renderer.js\";\nimport { readState, removeFromState, updateState } from \"./state.js\";\nimport type { ResourceEntry, ResourceSyncer, SyncAction } from \"./types.js\";\n\n/**\n * Compute the sync plan for `cwd` against the state file and (when\n * `fetchOld` is true) the platform API. The plan is read-only: it\n * decides what create/update/revise/delete operations need to happen but\n * performs none of them. Pass it to {@link runSyncLoop} to execute.\n *\n * Validates every on-disk file (via `syncer.validate`) before planning any\n * work — a single malformed schema or flow aborts the whole run with\n * `E_VALIDATION` before any platform mutation. Both `plan` and `apply`\n * reach this code path.\n *\n * Bearer auth + base URL live in the api package's runtime registries\n * (`runtime/{auth,base-url}`). Callers set them once at command boot;\n * the sync engine doesn't carry a client.\n *\n * @param cwd - Project root.\n * @param syncers - Per-resource adapters. Order is preserved in the output.\n * @param fetchOld - When true, the planner fetches each delete/update target\n * from the platform to populate `oldContent` for diff rendering.\n */\nexport async function buildSyncPlan(\n cwd: string,\n syncers: ReadonlyArray<ResourceSyncer>,\n fetchOld = false,\n): Promise<ReadonlyArray<SyncAction>> {\n const state = await readState(cwd);\n const actions: SyncAction[] = [];\n\n // Walk local flow files once, up front: revisioned schemas need to name every\n // flow whose `user_schema` currently pins the previous revision, so the\n // executor can re-pin those flows after the new revision is published.\n const localFlows = await readLocalFlowUserSchemas(cwd);\n\n // Revisions this plan will publish, keyed by the superseded id. Schema\n // syncers run before the flow syncer (makeSyncers order), so every pending\n // revise is known by the time flow files are planned.\n const pendingRevisions = new Map<string, { schemaPath: string }>();\n\n // Revisions an interrupted earlier run already published (state advanced,\n // `previousId` recorded) whose flows were never rewritten: superseded id →\n // the concrete new id. Lets a rerun finish the re-pin with no new revise.\n const recoveredRevisions = new Map<string, { schemaPath: string; newId: string }>();\n for (const [schemaPath, entry] of Object.entries(state.resources)) {\n if (entry.previousId && entry.id && entry.previousId !== entry.id) {\n recoveredRevisions.set(entry.previousId, { schemaPath, newId: entry.id });\n }\n }\n\n // Every scanned file body, keyed by project-relative path. Schema syncers\n // run before the flow syncer, so by the time a re-pin is planned the new\n // schema revision's content is available for the pre-flight field check.\n const scannedContents = new Map<string, object>();\n\n for (const syncer of syncers) {\n const dirPath = join(cwd, syncer.directory);\n consola.debug(`scanning ${syncer.directory}`);\n const onDisk = await readJsonDir(dirPath);\n\n for (const content of onDisk.values()) {\n syncer.validate(content);\n }\n\n for (const [filePath, entry] of Object.entries(state.resources)) {\n if (!filePath.startsWith(syncer.directory)) {\n continue;\n }\n if (onDisk.has(join(cwd, filePath)) || !entry.id) {\n continue;\n }\n\n let oldContent: object | null = null;\n if (fetchOld && syncer.fetch) {\n try {\n oldContent = await syncer.fetch(entry.id);\n } catch (err) {\n consola.debug(`fetch ${syncer.kind} ${entry.id} failed:`, err);\n }\n }\n actions.push({ kind: \"delete\", path: filePath, syncer, id: entry.id, oldContent });\n }\n\n for (const [absPath, content] of onDisk.entries()) {\n const relPath = absPath.slice(cwd.length + 1);\n scannedContents.set(relPath, content);\n const entry = state.resources[relPath];\n const hash = hashForState(syncer, content);\n\n // A flow pinned to a schema revision superseded in this plan (or by an\n // interrupted earlier run) needs its `user_schema` rewritten — whether\n // the flow is untouched, edited, or brand new (a create in the same\n // run as the revise must not POST the stale id).\n const flowRef = localFlows.get(relPath);\n const pending = flowRef ? pendingRevisions.get(flowRef) : undefined;\n const recovered = flowRef ? recoveredRevisions.get(flowRef) : undefined;\n const repin = pending\n ? { previousId: flowRef as string, schemaPath: pending.schemaPath }\n : recovered\n ? {\n previousId: flowRef as string,\n schemaPath: recovered.schemaPath,\n newId: recovered.newId,\n }\n : undefined;\n\n if (!entry?.id) {\n actions.push({\n kind: \"create\",\n path: relPath,\n syncer,\n content,\n hash,\n ...(repin ? { repin } : {}),\n });\n continue;\n }\n\n // State files written before normalized hashing hold legacy hashes\n // (order-sensitive, un-normalized). Accepting the legacy format —\n // both over the raw file and over its stably-sorted form, since\n // setup-era hashes were computed on sorted keys — keeps an untouched\n // or merely reordered file a skip; a spurious mismatch here would\n // publish a garbage schema revision. Writes always store the new\n // format, so state converges on the next real change.\n const unchanged =\n entry.hash === hash ||\n entry.hash === hashResourceContent(content) ||\n entry.hash === hashResourceContent(JSON.parse(stableStringify(content)) as object);\n if (unchanged && !(repin && syncer.mutable)) {\n actions.push({ kind: \"skip\", path: relPath, reason: \"no-change\" });\n continue;\n }\n\n if (syncer.revisioned) {\n const oldContent = await fetchOldIfAsked(syncer, entry.id, fetchOld);\n pendingRevisions.set(entry.id, { schemaPath: relPath });\n actions.push({\n kind: \"revise\",\n path: relPath,\n syncer,\n content,\n hash,\n previousId: entry.id,\n oldContent,\n affectedPaths: findFlowsPinnedTo(entry.id, localFlows),\n });\n continue;\n }\n\n if (!syncer.mutable) {\n actions.push({ kind: \"skip\", path: relPath, reason: \"immutable\" });\n continue;\n }\n\n const oldContent = await fetchOldIfAsked(syncer, entry.id, fetchOld);\n actions.push({\n kind: \"update\",\n path: relPath,\n syncer,\n id: entry.id,\n content,\n hash,\n oldContent,\n ...(repin ? { repin } : {}),\n });\n }\n }\n\n // Semantic pre-flight over every flow this plan uploads — the rules the\n // server would otherwise enforce mid-apply, after the schema revision\n // already published. Throws E_VALIDATION before any mutation.\n validatePlannedFlows({ actions, scannedContents, stateResources: state.resources });\n\n return actions;\n}\n\n/** Result of {@link runSyncLoop}: what changed on the platform and locally. */\nexport type SyncLoopResult = {\n /**\n * Project-relative paths of files updated from the server's canonical\n * responses (write-back). Surfaced in human and `--json` output so a\n * local rewrite is never silent.\n */\n filesUpdated: string[];\n /**\n * The platform resources this run touched, in execution order. Unlike a\n * plan-time enumeration, `id` carries the resulting platform id (the\n * created id, the newly published revision id, or the id updated or\n * deleted). Feeds the `apply` `--json` `changes` array.\n */\n applied: PlanResourceChange[];\n};\n\n/**\n * Execute every action returned by {@link buildSyncPlan} against the\n * platform. Updates the local state file (`.zitadel/state.json`) as\n * each action completes so an interrupted run can resume. After each\n * mutation, the server's canonical body is written back to the local\n * file (when it differs in normalized form), so repo config matches\n * live state by construction and the next `plan` is empty.\n *\n * The platform target (base URL + bearer auth) lives in the api\n * package's runtime registries; callers set them before invoking this.\n *\n * @param cwd - Project root.\n * @param syncers - Per-resource adapters; same list passed to\n * `buildSyncPlan`.\n */\nexport async function runSyncLoop(\n cwd: string,\n syncers: ReadonlyArray<ResourceSyncer>,\n): Promise<SyncLoopResult> {\n const actions = await buildSyncPlan(cwd, syncers);\n for (const action of actions) {\n if (action.kind === \"create\" || action.kind === \"update\") {\n for (const warning of action.warnings ?? []) {\n consola.warn(`${action.path}: ${warning.message}`);\n }\n }\n }\n const filesUpdated: string[] = [];\n const applied: PlanResourceChange[] = [];\n // Revisions published by this run: superseded id → new id. Update actions\n // carrying a `repin` patch their `user_schema` from here.\n const repinned = new Map<string, string>();\n\n const writeBack = async (\n action: Extract<SyncAction, { kind: \"create\" | \"revise\" | \"update\" }>,\n canonical: object | undefined,\n fallbackHash: string,\n ): Promise<string> => {\n if (!canonical) {\n return fallbackHash;\n }\n const { hash, changed } = await writeBackResource(cwd, action.path, action.syncer, canonical);\n if (changed) {\n filesUpdated.push(action.path);\n consola.info(`Updated ${action.path} from the server's canonical response`);\n }\n return hash;\n };\n\n for (const action of actions) {\n switch (action.kind) {\n case \"create\": {\n let content = action.content;\n const newId = action.repin\n ? (repinned.get(action.repin.previousId) ?? action.repin.newId)\n : undefined;\n if (newId) {\n // A flow created in the same run as (or after an interrupted)\n // schema revise must adopt the new revision — POSTing the stale\n // pin would fail validation, and its canonical echo would revert\n // the re-pinned local file.\n content = { ...(content as Record<string, unknown>), user_schema: newId };\n }\n const { id, canonical } = await action.syncer.create(content);\n const fallbackHash = newId ? hashForState(action.syncer, content) : action.hash;\n const entry: ResourceEntry = { id, hash: await writeBack(action, canonical, fallbackHash) };\n await updateState(cwd, action.path, entry);\n consola.info(\n `Created a new ${action.syncer.kind} on Zitadel from ${action.path} (id ${id})`,\n );\n applied.push({ kind: action.syncer.kind, action: \"create\", file: action.path, id });\n break;\n }\n case \"revise\": {\n const { id, canonical } = await action.syncer.create(action.content);\n // `previousId` lands in state before the flow files are rewritten:\n // if the process dies in between, the next plan recovers the re-pin\n // from state instead of publishing a duplicate revision.\n const entry: ResourceEntry = {\n id,\n hash: await writeBack(action, canonical, action.hash),\n previousId: action.previousId,\n };\n await updateState(cwd, action.path, entry);\n repinned.set(action.previousId, id);\n consola.info(\n `Published a new ${action.syncer.kind} revision on Zitadel from ${action.path} (id ${id})`,\n );\n for (const flowPath of action.affectedPaths) {\n if (await repinFlowFile(cwd, flowPath, action.previousId, id)) {\n filesUpdated.push(flowPath);\n consola.info(`Re-pinned user_schema in ${flowPath} to ${id}`);\n }\n }\n applied.push({\n kind: action.syncer.kind,\n action: \"revision\",\n file: action.path,\n id,\n previous_id: action.previousId,\n });\n break;\n }\n case \"update\": {\n let content = action.content;\n const newId = action.repin\n ? (repinned.get(action.repin.previousId) ?? action.repin.newId)\n : undefined;\n if (newId && action.repin) {\n // The plan captured the flow before the revise rewrote its file;\n // patch the pin in memory so the wire request adopts the new\n // revision without re-reading disk. The file rewrite below is a\n // no-op when this run's revise already re-pinned it — it matters\n // for crash recovery, where no revise ran this time.\n content = { ...(content as Record<string, unknown>), user_schema: newId };\n if (await repinFlowFile(cwd, action.path, action.repin.previousId, newId)) {\n filesUpdated.push(action.path);\n consola.info(`Re-pinned user_schema in ${action.path} to ${newId}`);\n }\n }\n const { canonical } = await action.syncer.update(action.id, content);\n const fallbackHash = newId ? hashForState(action.syncer, content) : action.hash;\n await updateState(cwd, action.path, {\n hash: await writeBack(action, canonical, fallbackHash),\n });\n consola.info(`Updated the ${action.syncer.kind} on Zitadel from ${action.path}`);\n applied.push({ kind: action.syncer.kind, action: \"update\", file: action.path, id: action.id });\n break;\n }\n case \"delete\": {\n await action.syncer.delete(action.id);\n await removeFromState(cwd, action.path);\n consola.info(\n `Deleted the ${action.syncer.kind} on Zitadel because ${action.path} was removed locally`,\n );\n applied.push({ kind: action.syncer.kind, action: \"delete\", file: action.path, id: action.id });\n break;\n }\n case \"skip\": {\n consola.debug(`Skipped ${action.path} (${action.reason})`);\n break;\n }\n }\n }\n\n // `previousId` exists to recover interrupted re-pins; once no local flow\n // pins the superseded revision, drop it — otherwise a developer who later\n // pins that old revision on purpose would get force-bumped by recovery.\n const remainingPins = new Set((await readLocalFlowUserSchemas(cwd)).values());\n const finalState = await readState(cwd);\n for (const [path, entry] of Object.entries(finalState.resources)) {\n if (entry.previousId && !remainingPins.has(entry.previousId)) {\n await updateState(cwd, path, { previousId: undefined });\n }\n }\n\n return { filesUpdated: [...new Set(filesUpdated)], applied };\n}\n\n/**\n * Rewrite a flow file's `user_schema` pin from `previousId` to `newId`,\n * lockfile-style. Prefers a targeted text replacement so the author's\n * formatting survives a one-string change; falls back to parse +\n * `stableStringify` when the raw text doesn't contain exactly one pin.\n * Returns false when the file doesn't pin `previousId` (already re-pinned\n * or hand-edited) — never throws for an unreadable file.\n */\nasync function repinFlowFile(\n cwd: string,\n relPath: string,\n previousId: string,\n newId: string,\n): Promise<boolean> {\n const absPath = join(cwd, relPath);\n let raw: string;\n try {\n raw = await readFile(absPath, \"utf8\");\n } catch (err) {\n consola.debug(`read ${relPath} for re-pin failed:`, err);\n return false;\n }\n\n const pinPattern = new RegExp(\n `(\"user_schema\"\\\\s*:\\\\s*)${escapeRegExp(JSON.stringify(previousId))}`,\n \"g\",\n );\n if (raw.match(pinPattern)?.length === 1) {\n await writeFile(\n absPath,\n raw.replace(pinPattern, (_match, prefix: string) => `${prefix}${JSON.stringify(newId)}`),\n );\n return true;\n }\n\n try {\n const doc = JSON.parse(raw) as Record<string, unknown>;\n if (doc.user_schema !== previousId) {\n return false;\n }\n doc.user_schema = newId;\n await writeFile(absPath, `${stableStringify(doc)}\\n`);\n return true;\n } catch (err) {\n consola.debug(`re-pin ${relPath} failed:`, err);\n return false;\n }\n}\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Reconcile a local file with the server's canonical body. What gets\n * written is the `normalizeWrite` form (strips pure transport noise like\n * the empty `audience` echo; for schemas the canonical body verbatim —\n * spelled-out x-* defaults must survive, or the next apply would publish\n * a revision without them). Equality is judged in the `normalize`\n * comparison form, so the file is rewritten only when it materially\n * differs from live state; hand-formatted files stay untouched otherwise.\n * Returns the state hash of the written form.\n */\nexport async function writeBackResource(\n cwd: string,\n relPath: string,\n syncer: Pick<ResourceSyncer, \"normalize\" | \"normalizeWrite\">,\n canonical: object,\n): Promise<{ hash: string; changed: boolean }> {\n let writeBody = syncer.normalizeWrite?.(canonical) ?? canonical;\n const compare = (body: object) => stableStringify(syncer.normalize?.(body) ?? body);\n const absPath = join(cwd, relPath);\n let changed = true;\n try {\n const onDisk = JSON.parse(await readFile(absPath, \"utf8\")) as Record<string, unknown>;\n changed = compare(writeBody) !== compare(onDisk);\n // `$schema` is a local editor affordance (points at the dialect spec in\n // .zitadel/meta/); the server never echoes it, so a rewrite would drop\n // it. Carry the on-disk pointer over unless the canonical body has its\n // own (user-schema documents do).\n if (typeof onDisk.$schema === \"string\" && !(\"$schema\" in writeBody)) {\n writeBody = { ...writeBody, $schema: onDisk.$schema };\n }\n } catch (err) {\n consola.debug(`read ${relPath} for write-back failed:`, err);\n }\n if (changed) {\n await writeFile(absPath, `${stableStringify(writeBody)}\\n`);\n }\n return { hash: hashForState(syncer, writeBody), changed };\n}\n\nasync function fetchOldIfAsked(\n syncer: ResourceSyncer,\n id: string,\n fetchOld: boolean,\n): Promise<object | null> {\n if (!fetchOld || !syncer.fetch) {\n return null;\n }\n try {\n return await syncer.fetch(id);\n } catch (err) {\n consola.debug(`fetch ${syncer.kind} ${id} failed:`, err);\n return null;\n }\n}\n\nasync function readJsonDir(dirPath: string): Promise<Map<string, object>> {\n const result = new Map<string, object>();\n let entries: string[];\n try {\n entries = await readdir(dirPath);\n } catch (err) {\n if (typeof err === \"object\" && err !== null && \"code\" in err && err.code === \"ENOENT\") {\n return result;\n }\n throw err;\n }\n for (const entry of entries.filter((e) => e.endsWith(\".json\"))) {\n const filePath = join(dirPath, entry);\n const raw = await readFile(filePath, \"utf8\");\n result.set(filePath, JSON.parse(raw) as object);\n }\n return result;\n}\n\n/**\n * Walk `.zitadel/flows/*.json` once and return each flow's `user_schema` value\n * keyed by project-root-relative path. A flow file without a `user_schema` (or\n * with a non-string one) is skipped: it does not pin a schema revision, so it\n * cannot be affected by one.\n */\nasync function readLocalFlowUserSchemas(cwd: string): Promise<Map<string, string>> {\n const result = new Map<string, string>();\n const flows = await readJsonDir(join(cwd, FLOWS_DIR));\n for (const [absPath, content] of flows.entries()) {\n const relPath = absPath.slice(cwd.length + 1);\n if (\n typeof content === \"object\" &&\n content !== null &&\n \"user_schema\" in content &&\n typeof (content as { user_schema: unknown }).user_schema === \"string\"\n ) {\n result.set(relPath, (content as { user_schema: string }).user_schema);\n }\n }\n return result;\n}\n\nfunction findFlowsPinnedTo(\n previousId: string,\n localFlows: Map<string, string>,\n): ReadonlyArray<string> {\n const affected: string[] = [];\n for (const [relPath, ref] of localFlows.entries()) {\n if (ref === previousId) {\n affected.push(relPath);\n }\n }\n return affected;\n}\n\n/**\n * Legacy content hash: order-sensitive and normalization-blind. Kept only\n * so state entries written by older CLI versions still match; new hashes\n * come from {@link hashForState}.\n */\nexport function hashResourceContent(data: object): string {\n return createHash(\"sha256\").update(JSON.stringify(data)).digest(\"hex\");\n}\n\n/**\n * The content hash stored in `.zitadel/state.json`: key-order-insensitive\n * (via `stableStringify`) and computed on the syncer's normalized form, so\n * reordering keys or spelling out a meta-schema default does not read as an\n * edit.\n */\nexport function hashForState(\n syncer: Pick<ResourceSyncer, \"normalize\">,\n data: object,\n): string {\n const normalized = syncer.normalize?.(data) ?? data;\n return createHash(\"sha256\").update(stableStringify(normalized)).digest(\"hex\");\n}\n","import { stableStringify } from \"../json\";\nimport type { ResourceSyncer, SyncAction, SyncPlanSummary } from \"./types.js\";\n\n/**\n * Count the non-`skip` actions in a {@link buildSyncPlan} result. Pure; the\n * single source of truth for the plan counts shared by the `plan` /\n * `apply --dry-run` JSON payload and {@link renderPlan}'s summary line.\n */\nexport function summarizePlan(actions: ReadonlyArray<SyncAction>): SyncPlanSummary {\n const active = actions.filter((a) => a.kind !== \"skip\");\n return {\n creates: active.filter((a) => a.kind === \"create\").length,\n updates: active.filter((a) => a.kind === \"update\").length,\n revisions: active.filter((a) => a.kind === \"revise\").length,\n deletes: active.filter((a) => a.kind === \"delete\").length,\n total: active.length,\n };\n}\n\n/**\n * Collect the plan-time validation warnings across all actions, tagged\n * with the file they belong to. Feeds the `plan` / `apply --dry-run`\n * `--json` payload so agents can read warnings structurally.\n */\nexport function collectPlanWarnings(\n actions: ReadonlyArray<SyncAction>,\n): Array<{ path: string; rule: string; message: string }> {\n const out: Array<{ path: string; rule: string; message: string }> = [];\n for (const action of actions) {\n if (action.kind !== \"create\" && action.kind !== \"update\") {\n continue;\n }\n for (const warning of action.warnings ?? []) {\n out.push({ path: action.path, rule: warning.rule, message: warning.message });\n }\n }\n return out;\n}\n\n/**\n * One entry of the `changes` array in the `plan` / `apply` `--json`\n * payloads: which resource an action touches and how. `action` speaks the\n * envelope's public vocabulary (`revision`, matching the `revisions`\n * counter, not the internal `revise`). `id` is the platform id an\n * update/delete targets; `previous_id` is the revision being superseded.\n * The file path is the resource identity, mirroring {@link renderPlan}.\n */\nexport type PlanResourceChange = {\n kind: ResourceSyncer[\"kind\"];\n action: \"create\" | \"update\" | \"revision\" | \"delete\";\n file: string;\n id?: string;\n previous_id?: string;\n};\n\n/**\n * Enumerate the non-`skip` actions of a {@link buildSyncPlan} result as\n * envelope-ready {@link PlanResourceChange} rows. Pure; the structural\n * counterpart of {@link summarizePlan} — counts and rows always agree.\n */\nexport function enumeratePlanResources(\n actions: ReadonlyArray<SyncAction>,\n): PlanResourceChange[] {\n const out: PlanResourceChange[] = [];\n for (const action of actions) {\n switch (action.kind) {\n case \"create\":\n out.push({ kind: action.syncer.kind, action: \"create\", file: action.path });\n break;\n case \"update\":\n out.push({ kind: action.syncer.kind, action: \"update\", file: action.path, id: action.id });\n break;\n case \"revise\":\n out.push({\n kind: action.syncer.kind,\n action: \"revision\",\n file: action.path,\n previous_id: action.previousId,\n });\n break;\n case \"delete\":\n out.push({ kind: action.syncer.kind, action: \"delete\", file: action.path, id: action.id });\n break;\n case \"skip\":\n break;\n }\n }\n return out;\n}\n\n/**\n * Render a {@link buildSyncPlan} result as a human-readable Terraform-style\n * plan. TTY-aware: colors and bold are emitted only when `tty` is true.\n * Returns the empty-state message when every action is `skip`.\n *\n * @param actions - The action list produced by `buildSyncPlan`. Read-only;\n * the function never mutates the input.\n * @param tty - True when stdout is a TTY; controls ANSI emission.\n */\nexport function renderPlan(actions: ReadonlyArray<SyncAction>, tty: boolean): string {\n const active = actions.filter((a) => a.kind !== \"skip\");\n\n if (active.length === 0) {\n return paint(\n \"No changes. Your Zitadel configuration matches the current state.\",\n A.bold,\n tty,\n );\n }\n\n const out: string[] = [];\n out.push(paint(\"Zitadel will perform the following actions:\", A.bold, tty));\n\n for (const action of active) {\n out.push(\"\");\n out.push(...renderBlock(action, tty));\n }\n\n out.push(\"\");\n\n const { creates, updates, revisions, deletes } = summarizePlan(actions);\n\n const parts: string[] = [];\n if (creates > 0) {\n parts.push(`${creates} to add`);\n }\n if (updates > 0) {\n parts.push(`${updates} to change`);\n }\n if (revisions > 0) {\n parts.push(`${revisions} new revision${revisions === 1 ? \"\" : \"s\"}`);\n }\n if (deletes > 0) {\n parts.push(`${deletes} to destroy`);\n }\n\n out.push(paint(`Plan: ${parts.join(\", \")}.`, A.bold, tty));\n\n const warningCount = active.reduce(\n (count, action) =>\n count +\n ((action.kind === \"create\" || action.kind === \"update\") && action.warnings\n ? action.warnings.length\n : 0),\n 0,\n );\n if (warningCount > 0) {\n out.push(\n paint(\n `Warnings: ${warningCount} (non-blocking — see the # warning lines above).`,\n A.yellow,\n tty,\n ),\n );\n }\n\n return out.join(\"\\n\");\n}\n\nconst A = {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n green: \"\\x1b[32m\",\n red: \"\\x1b[31m\",\n yellow: \"\\x1b[33m\",\n} as const;\n\nfunction paint(text: string, code: string, tty: boolean): string {\n return tty ? `${code}${text}${A.reset}` : text;\n}\n\nfunction isPrimitive(v: unknown): v is string | number | boolean | null {\n return v === null || typeof v === \"string\" || typeof v === \"number\" || typeof v === \"boolean\";\n}\n\nfunction isPlainObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nconst KNOWN_AFTER_APPLY = \"(known after apply)\";\n\nfunction escapeString(s: string): string {\n return s\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\t/g, \"\\\\t\");\n}\n\nfunction fmtPrimitive(v: string | number | boolean | null): string {\n if (v === null) {\n return \"null\";\n }\n if (typeof v === \"string\" && v === KNOWN_AFTER_APPLY) {\n return KNOWN_AFTER_APPLY;\n }\n if (typeof v === \"string\") {\n return `\"${escapeString(v)}\"`;\n }\n return String(v);\n}\n\n/**\n * Indentation contract (matches Terraform exactly):\n * prefixCol = column index of the +/-/~ character\n * field content starts at prefixCol + 2 (one space gap after prefix)\n * nested object/array content: prefixCol + 4 for the child prefixCol\n * closing } or ] : prefixCol + 2 columns of plain spaces, no prefix\n */\ntype ChangePrefix = \"+\" | \"-\" | \"~\" | \" \";\n\nfunction prefixAnsi(p: ChangePrefix): string {\n if (p === \"+\") {\n return A.green;\n }\n if (p === \"-\") {\n return A.red;\n }\n if (p === \"~\") {\n return A.yellow;\n }\n return \"\";\n}\n\ninterface RenderCtx {\n tty: boolean;\n deleteMode: boolean;\n}\n\nfunction renderFields(\n obj: Record<string, unknown>,\n prefix: ChangePrefix,\n prefixCol: number,\n ctx: RenderCtx,\n lines: string[],\n): void {\n const pad = \" \".repeat(prefixCol);\n const ansi = prefixAnsi(prefix);\n const col = (s: string) => paint(s, ansi, ctx.tty);\n\n const keys = Object.keys(obj).sort();\n const maxLen = keys.reduce((m, k) => Math.max(m, k.length), 0);\n\n for (const key of keys) {\n const val = obj[key];\n const pk = key.padEnd(maxLen);\n\n if (isPrimitive(val)) {\n const formatted = fmtPrimitive(val);\n const suffix = ctx.deleteMode ? \" -> null\" : \"\";\n lines.push(col(`${pad}${prefix} ${pk} = ${formatted}${suffix}`));\n } else if (Array.isArray(val)) {\n if (val.length === 0) {\n lines.push(col(`${pad}${prefix} ${pk} = []`));\n } else {\n lines.push(col(`${pad}${prefix} ${pk} = [`));\n renderArrayItems(val, prefix, prefixCol + 4, ctx, lines);\n lines.push(col(`${\" \".repeat(prefixCol + 2)}]`));\n }\n } else if (isPlainObject(val)) {\n if (Object.keys(val).length === 0) {\n lines.push(col(`${pad}${prefix} ${pk} = {}`));\n } else {\n lines.push(col(`${pad}${prefix} ${pk} = {`));\n renderFields(val, prefix, prefixCol + 4, ctx, lines);\n lines.push(col(`${\" \".repeat(prefixCol + 2)}}`));\n }\n }\n }\n}\n\n/**\n * Renders the items of an array. Unlike {@link renderFields}, primitive\n * elements never get a trailing ` -> null` suffix even under `deleteMode` —\n * Terraform only annotates scalar object-field removals that way, not array\n * items.\n */\nfunction renderArrayItems(\n arr: ReadonlyArray<unknown>,\n prefix: ChangePrefix,\n prefixCol: number,\n ctx: RenderCtx,\n lines: string[],\n): void {\n const pad = \" \".repeat(prefixCol);\n const ansi = prefixAnsi(prefix);\n const col = (s: string) => paint(s, ansi, ctx.tty);\n\n for (const item of arr) {\n if (isPrimitive(item)) {\n const formatted = fmtPrimitive(item);\n lines.push(col(`${pad}${prefix} ${formatted},`));\n } else if (Array.isArray(item)) {\n if (item.length === 0) {\n lines.push(col(`${pad}${prefix} [],`));\n } else {\n lines.push(col(`${pad}${prefix} [`));\n renderArrayItems(item, prefix, prefixCol + 4, ctx, lines);\n lines.push(col(`${\" \".repeat(prefixCol + 2)}],`));\n }\n } else if (isPlainObject(item)) {\n if (Object.keys(item).length === 0) {\n lines.push(col(`${pad}${prefix} {},`));\n } else {\n lines.push(col(`${pad}${prefix} {`));\n renderFields(item, prefix, prefixCol + 4, ctx, lines);\n lines.push(col(`${\" \".repeat(prefixCol + 2)}},`));\n }\n }\n }\n}\n\n/**\n * Walks both old and new objects, emitting Terraform-style change lines.\n * Returns true if any actual change line (+ / - / ~) was emitted.\n *\n * Edge cases:\n * - Changed arrays render as a full remove + full add (no LCS diff).\n * - Nested objects recurse, and the outer key is only marked `~` if a child\n * actually changed; unchanged children render with the neutral prefix.\n * - A value whose type changed (e.g. string → object) also renders as a\n * remove + add pair.\n */\nfunction renderDiff(\n oldObj: Record<string, unknown>,\n newObj: Record<string, unknown>,\n prefixCol: number,\n tty: boolean,\n lines: string[],\n): boolean {\n const allKeys = [...new Set([...Object.keys(oldObj), ...Object.keys(newObj)])].sort();\n const maxLen = allKeys.reduce((m, k) => Math.max(m, k.length), 0);\n const pad = \" \".repeat(prefixCol);\n let hasChanges = false;\n\n for (const key of allKeys) {\n const pk = key.padEnd(maxLen);\n const hasOld = Object.prototype.hasOwnProperty.call(oldObj, key);\n const hasNew = Object.prototype.hasOwnProperty.call(newObj, key);\n const oldVal = oldObj[key];\n const newVal = newObj[key];\n\n if (!hasOld) {\n hasChanges = true;\n const col = (s: string) => paint(s, A.green, tty);\n if (isPrimitive(newVal)) {\n lines.push(col(`${pad}+ ${pk} = ${fmtPrimitive(newVal)}`));\n } else if (Array.isArray(newVal)) {\n lines.push(col(`${pad}+ ${pk} = [`));\n renderArrayItems(newVal, \"+\", prefixCol + 4, { tty, deleteMode: false }, lines);\n lines.push(col(`${\" \".repeat(prefixCol + 2)}]`));\n } else if (isPlainObject(newVal)) {\n lines.push(col(`${pad}+ ${pk} = {`));\n renderFields(newVal, \"+\", prefixCol + 4, { tty, deleteMode: false }, lines);\n lines.push(col(`${\" \".repeat(prefixCol + 2)}}`));\n }\n } else if (!hasNew) {\n hasChanges = true;\n const col = (s: string) => paint(s, A.red, tty);\n if (isPrimitive(oldVal)) {\n lines.push(col(`${pad}- ${pk} = ${fmtPrimitive(oldVal)} -> null`));\n } else if (Array.isArray(oldVal)) {\n lines.push(col(`${pad}- ${pk} = [`));\n renderArrayItems(oldVal, \"-\", prefixCol + 4, { tty, deleteMode: false }, lines);\n lines.push(col(`${\" \".repeat(prefixCol + 2)}]`));\n } else if (isPlainObject(oldVal)) {\n lines.push(col(`${pad}- ${pk} = {`));\n renderFields(oldVal, \"-\", prefixCol + 4, { tty, deleteMode: true }, lines);\n lines.push(col(`${\" \".repeat(prefixCol + 2)}}`));\n }\n } else if (isPrimitive(oldVal) && isPrimitive(newVal)) {\n if (oldVal === newVal) {\n lines.push(`${pad} ${pk} = ${fmtPrimitive(newVal)}`);\n } else {\n hasChanges = true;\n const col = (s: string) => paint(s, A.yellow, tty);\n lines.push(col(`${pad}~ ${pk} = ${fmtPrimitive(oldVal)} -> ${fmtPrimitive(newVal)}`));\n }\n } else if (Array.isArray(oldVal) && Array.isArray(newVal)) {\n // Key-order-insensitive equality: the server echoes objects in its own\n // field order while local files are stably sorted — that difference is\n // not a change.\n if (stableStringify(oldVal) === stableStringify(newVal)) {\n if (newVal.length === 0) {\n lines.push(`${pad} ${pk} = []`);\n } else {\n lines.push(`${pad} ${pk} = [`);\n renderArrayItems(newVal, \" \", prefixCol + 4, { tty, deleteMode: false }, lines);\n lines.push(`${\" \".repeat(prefixCol + 2)}]`);\n }\n } else {\n hasChanges = true;\n const colR = (s: string) => paint(s, A.red, tty);\n const colA = (s: string) => paint(s, A.green, tty);\n if (oldVal.length === 0) {\n lines.push(colR(`${pad}- ${pk} = []`));\n } else {\n lines.push(colR(`${pad}- ${pk} = [`));\n renderArrayItems(oldVal, \"-\", prefixCol + 4, { tty, deleteMode: false }, lines);\n lines.push(colR(`${\" \".repeat(prefixCol + 2)}]`));\n }\n if (newVal.length === 0) {\n lines.push(colA(`${pad}+ ${pk} = []`));\n } else {\n lines.push(colA(`${pad}+ ${pk} = [`));\n renderArrayItems(newVal, \"+\", prefixCol + 4, { tty, deleteMode: false }, lines);\n lines.push(colA(`${\" \".repeat(prefixCol + 2)}]`));\n }\n }\n } else if (isPlainObject(oldVal) && isPlainObject(newVal)) {\n const childLines: string[] = [];\n const childHasChanges = renderDiff(oldVal, newVal, prefixCol + 4, tty, childLines);\n if (childHasChanges) {\n hasChanges = true;\n const col = (s: string) => paint(s, A.yellow, tty);\n lines.push(col(`${pad}~ ${pk} = {`));\n lines.push(...childLines);\n lines.push(col(`${\" \".repeat(prefixCol + 2)}}`));\n } else if (childLines.length > 0) {\n lines.push(`${pad} ${pk} = {`);\n lines.push(...childLines);\n lines.push(`${\" \".repeat(prefixCol + 2)}}`);\n } else {\n lines.push(`${pad} ${pk} = {}`);\n }\n } else {\n hasChanges = true;\n const colR = (s: string) => paint(s, A.red, tty);\n const colA = (s: string) => paint(s, A.green, tty);\n if (isPrimitive(oldVal)) {\n lines.push(colR(`${pad}- ${pk} = ${fmtPrimitive(oldVal)} -> null`));\n }\n if (isPrimitive(newVal)) {\n lines.push(colA(`${pad}+ ${pk} = ${fmtPrimitive(newVal)}`));\n }\n }\n }\n\n return hasChanges;\n}\n\n/**\n * Column layout (matches Terraform's per-block format):\n * BLOCK_COL = 2 — where the +/-/~ sits on the resource opening line\n * FIELD_COL = 6 — where the +/-/~ sits on first-level field lines\n * closing } — at BLOCK_COL + 2 = 4, no prefix\n */\nconst BLOCK_COL = 2;\nconst FIELD_COL = 6;\n\nfunction resourceName(path: string): string {\n return path.split(\"/\").pop() ?? path;\n}\n\n/**\n * Diff both sides in the syncer's canonical form so server-echoed noise\n * (empty `audience`, spelled-out meta-schema defaults) never renders as a\n * change the author didn't make. Rendering only — upload payloads stay raw.\n */\nfunction normalized(\n syncer: Pick<ResourceSyncer, \"normalize\">,\n content: object,\n): Record<string, unknown> {\n return (syncer.normalize?.(content) ?? content) as Record<string, unknown>;\n}\n\n/**\n * Renders one Terraform-style resource block for a single `SyncAction`.\n *\n * Per-case notes:\n * - **create**: a synthetic `id = (known after apply)` is injected into the\n * rendered fields so it sorts alphabetically alongside the real keys.\n * - **delete**: when `oldContent` is null (the fetch failed), the body\n * collapses to a single `- id = \"<id>\" -> null` line.\n * - **update**: when `oldContent` is null (no read endpoint for this\n * resource kind), the field diff is replaced with a placeholder\n * \"field diff unavailable\" line.\n * - **skip**: omitted from the output entirely, matching Terraform's\n * default of not showing no-change resources.\n */\nfunction renderBlock(action: SyncAction, tty: boolean): string[] {\n const lines: string[] = [];\n const blkPad = \" \".repeat(BLOCK_COL);\n const closePad = \" \".repeat(BLOCK_COL + 2);\n\n switch (action.kind) {\n case \"create\": {\n const header = `${blkPad}# ${action.path} will be created`;\n const opening = `${blkPad}+ resource \"${action.syncer.kind}\" \"${resourceName(action.path)}\" {`;\n lines.push(paint(header, A.bold, tty));\n lines.push(paint(opening, A.green, tty));\n\n const display: Record<string, unknown> = {\n id: KNOWN_AFTER_APPLY,\n ...(action.content as Record<string, unknown>),\n };\n if (action.repin) {\n // The executor POSTs this flow with the new revision id, not the\n // stale pin still in the file — render what will actually be sent.\n display.user_schema = action.repin.newId ?? KNOWN_AFTER_APPLY;\n }\n renderFields(display, \"+\", FIELD_COL, { tty, deleteMode: false }, lines);\n lines.push(`${closePad}}`);\n renderWarnings(action.warnings, blkPad, tty, lines);\n break;\n }\n\n case \"delete\": {\n const header = `${blkPad}# ${action.path} will be destroyed`;\n const opening = `${blkPad}- resource \"${action.syncer.kind}\" \"${resourceName(action.path)}\" {`;\n lines.push(paint(header, A.bold, tty));\n lines.push(paint(opening, A.red, tty));\n\n if (action.oldContent) {\n const display: Record<string, unknown> = {\n id: action.id,\n ...(action.oldContent as Record<string, unknown>),\n };\n renderFields(display, \"-\", FIELD_COL, { tty, deleteMode: true }, lines);\n } else {\n lines.push(paint(`${\" \".repeat(FIELD_COL)}- id = \"${action.id}\" -> null`, A.red, tty));\n }\n lines.push(`${closePad}}`);\n break;\n }\n\n case \"update\": {\n const headerSuffix = action.repin ? \" (re-pin user_schema)\" : \"\";\n const header = `${blkPad}# ${action.path} will be updated in-place${headerSuffix}`;\n const opening = `${blkPad}~ resource \"${action.syncer.kind}\" \"${resourceName(action.path)}\" {`;\n lines.push(paint(header, A.bold, tty));\n lines.push(paint(opening, A.yellow, tty));\n\n // A repin update ships with `user_schema` rewritten to the revision id\n // the revise mints (or already minted, for crash recovery) — render the\n // content the executor will actually PUT.\n const newContent = action.repin\n ? {\n ...normalized(action.syncer, action.content),\n user_schema: action.repin.newId ?? KNOWN_AFTER_APPLY,\n }\n : normalized(action.syncer, action.content);\n\n if (action.oldContent) {\n renderDiff(normalized(action.syncer, action.oldContent), newContent, FIELD_COL, tty, lines);\n } else if (action.repin) {\n lines.push(\n paint(\n `${\" \".repeat(FIELD_COL)}~ user_schema = \"${action.repin.previousId}\" -> ${action.repin.newId ? `\"${action.repin.newId}\"` : KNOWN_AFTER_APPLY}`,\n A.yellow,\n tty,\n ),\n );\n } else {\n lines.push(\n `${\" \".repeat(FIELD_COL)} # (field diff unavailable — no read endpoint for ${action.syncer.kind})`,\n );\n }\n lines.push(`${closePad}}`);\n renderWarnings(action.warnings, blkPad, tty, lines);\n break;\n }\n\n case \"revise\": {\n const header = `${blkPad}# ${action.path} will publish a new revision`;\n const opening = `${blkPad}~ resource \"${action.syncer.kind}\" \"${resourceName(action.path)}\" {`;\n lines.push(paint(header, A.bold, tty));\n lines.push(paint(opening, A.yellow, tty));\n\n if (action.oldContent) {\n const oldWithId: Record<string, unknown> = {\n id: action.previousId,\n ...normalized(action.syncer, action.oldContent),\n };\n const newWithId: Record<string, unknown> = {\n id: KNOWN_AFTER_APPLY,\n ...normalized(action.syncer, action.content),\n };\n renderDiff(oldWithId, newWithId, FIELD_COL, tty, lines);\n } else {\n lines.push(\n `${\" \".repeat(FIELD_COL)} # (field diff unavailable — no read endpoint for ${action.syncer.kind})`,\n );\n }\n lines.push(`${closePad}}`);\n if (action.affectedPaths.length > 0) {\n lines.push(\n paint(\n `${blkPad}# user_schema will be re-pinned to the new revision ${KNOWN_AFTER_APPLY} in:`,\n A.yellow,\n tty,\n ),\n );\n for (const path of action.affectedPaths) {\n lines.push(paint(`${blkPad}# - ${path}`, A.yellow, tty));\n }\n }\n break;\n }\n\n case \"skip\":\n break;\n }\n\n return lines;\n}\n\n/**\n * Emit one yellow `# warning:` comment line per plan-time validation\n * warning, below the action's closing brace (same channel as the revise\n * re-pin announcement). Warnings never block the plan.\n */\nfunction renderWarnings(\n warnings: ReadonlyArray<{ message: string }> | undefined,\n blkPad: string,\n tty: boolean,\n lines: string[],\n): void {\n for (const warning of warnings ?? []) {\n lines.push(paint(`${blkPad}# warning: ${warning.message}`, A.yellow, tty));\n }\n}\n"],"mappings":"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"}
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zitadel/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.17",
|
|
4
4
|
"description": "Agent-friendly Zitadel CLI",
|
|
5
5
|
"homepage": "https://github.com/zitadel/nextgen/tree/main/apps/cli#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
"safe-stable-stringify": "^2.5.0",
|
|
61
61
|
"zod": "^4.3.6",
|
|
62
62
|
"picocolors": "^1.1.1",
|
|
63
|
-
"@zitadel/api": "0.1.0-alpha.
|
|
64
|
-
"@zitadel/config": "0.1.0-alpha.
|
|
65
|
-
"@zitadel/server": "0.1.0-alpha.
|
|
63
|
+
"@zitadel/api": "0.1.0-alpha.17",
|
|
64
|
+
"@zitadel/config": "0.1.0-alpha.17",
|
|
65
|
+
"@zitadel/server": "0.1.0-alpha.17"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@types/node": "^25.6.0",
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
"oclif": "^4.17.46",
|
|
71
71
|
"tsdown": "^0.21.10",
|
|
72
72
|
"vitest": "^3.0.0",
|
|
73
|
-
"@zitadel/
|
|
74
|
-
"@zitadel/
|
|
73
|
+
"@zitadel/sdk-next": "0.1.0-alpha.17",
|
|
74
|
+
"@zitadel/api-mock": "0.0.0"
|
|
75
75
|
},
|
|
76
76
|
"scripts": {
|
|
77
77
|
"build": "tsdown",
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { b as publicCliCommand } from "./oclif-SjHLB_XK.mjs";
|
|
2
|
-
//#region src/lib/local-server/docker-guidance.ts
|
|
3
|
-
function dockerUnavailableMessage(error) {
|
|
4
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
5
|
-
return `Docker is not reachable${detail.trim() ? ` (${detail.trim()})` : ""}; managed local runtime commands need Docker, but remote or cloud setup can continue with --server <url>.`;
|
|
6
|
-
}
|
|
7
|
-
function dockerRuntimeGuidance(retryCommand, cliVersion) {
|
|
8
|
-
return {
|
|
9
|
-
hint: "Docker is required only for the managed local runtime (`zitadel start` and `--server local`). Start Docker, then retry; or target a remote server with `--server <url>`.",
|
|
10
|
-
nextActions: [
|
|
11
|
-
"Start Docker Desktop, Docker Engine, or a Docker-compatible runtime such as Colima.",
|
|
12
|
-
`Run \`docker version\`, then rerun \`${publicCliCommand(retryCommand, cliVersion)}\`.`,
|
|
13
|
-
"If you are not using the managed local runtime, run setup with `--server <url>` instead."
|
|
14
|
-
],
|
|
15
|
-
nextCommands: ["docker version", publicCliCommand(retryCommand, cliVersion)]
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
//#endregion
|
|
19
|
-
export { dockerUnavailableMessage as n, dockerRuntimeGuidance as t };
|
|
20
|
-
|
|
21
|
-
//# sourceMappingURL=docker-guidance-3UZESTfb.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"docker-guidance-3UZESTfb.mjs","names":[],"sources":["../src/lib/local-server/docker-guidance.ts"],"sourcesContent":["import { publicCliCommand } from \"../public-cli\";\n\nexport type DockerRuntimeGuidance = {\n hint: string;\n nextActions: string[];\n nextCommands: string[];\n};\n\nexport function dockerUnavailableMessage(error: unknown): string {\n const detail = error instanceof Error ? error.message : String(error);\n const suffix = detail.trim() ? ` (${detail.trim()})` : \"\";\n return `Docker is not reachable${suffix}; managed local runtime commands need Docker, but remote or cloud setup can continue with --server <url>.`;\n}\n\nexport function dockerRuntimeGuidance(\n retryCommand: \"doctor\" | \"start\",\n cliVersion: string,\n): DockerRuntimeGuidance {\n return {\n hint:\n \"Docker is required only for the managed local runtime (`zitadel start` and `--server local`). Start Docker, then retry; or target a remote server with `--server <url>`.\",\n nextActions: [\n \"Start Docker Desktop, Docker Engine, or a Docker-compatible runtime such as Colima.\",\n `Run \\`docker version\\`, then rerun \\`${publicCliCommand(retryCommand, cliVersion)}\\`.`,\n \"If you are not using the managed local runtime, run setup with `--server <url>` instead.\",\n ],\n nextCommands: [\"docker version\", publicCliCommand(retryCommand, cliVersion)],\n };\n}\n"],"mappings":";;AAQA,SAAgB,yBAAyB,OAAwB;CAC/D,MAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAErE,QAAO,0BADQ,OAAO,MAAM,GAAG,KAAK,OAAO,MAAM,CAAC,KAAK,GACf;;AAG1C,SAAgB,sBACd,cACA,YACuB;AACvB,QAAO;EACL,MACE;EACF,aAAa;GACX;GACA,wCAAwC,iBAAiB,cAAc,WAAW,CAAC;GACnF;GACD;EACD,cAAc,CAAC,kBAAkB,iBAAiB,cAAc,WAAW,CAAC;EAC7E"}
|