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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +145 -107
  2. package/SKILLS.md +88 -12
  3. package/dist/branding-BXj028-W.mjs +79 -0
  4. package/dist/branding-BXj028-W.mjs.map +1 -0
  5. package/dist/claim-state-BTGInAaN.mjs +66 -0
  6. package/dist/claim-state-BTGInAaN.mjs.map +1 -0
  7. package/dist/commands/apply.mjs +6 -5
  8. package/dist/commands/apply.mjs.map +1 -1
  9. package/dist/commands/branding/eject.mjs +125 -0
  10. package/dist/commands/branding/eject.mjs.map +1 -0
  11. package/dist/commands/claim.mjs +402 -0
  12. package/dist/commands/claim.mjs.map +1 -0
  13. package/dist/commands/doctor.mjs +477 -37
  14. package/dist/commands/doctor.mjs.map +1 -1
  15. package/dist/commands/eject.mjs +3 -3
  16. package/dist/commands/logs.mjs +2 -2
  17. package/dist/commands/plan.mjs +6 -5
  18. package/dist/commands/plan.mjs.map +1 -1
  19. package/dist/commands/reset.mjs +2 -2
  20. package/dist/commands/schemas/list.mjs +8 -8
  21. package/dist/commands/schemas/list.mjs.map +1 -1
  22. package/dist/commands/setup.mjs +305 -36
  23. package/dist/commands/setup.mjs.map +1 -1
  24. package/dist/commands/start.mjs +3 -3
  25. package/dist/commands/status.mjs +21 -13
  26. package/dist/commands/status.mjs.map +1 -1
  27. package/dist/commands/stop.mjs +3 -3
  28. package/dist/commands/stop.mjs.map +1 -1
  29. package/dist/{docker-pGcTrDIY.mjs → docker-Ciz9N6uI.mjs} +2 -2
  30. package/dist/{docker-pGcTrDIY.mjs.map → docker-Ciz9N6uI.mjs.map} +1 -1
  31. package/dist/{environment-BQF7LeCz.mjs → environment-CuHKxtIO.mjs} +1 -1
  32. package/dist/{environment-BQF7LeCz.mjs.map → environment-CuHKxtIO.mjs.map} +1 -1
  33. package/dist/{journey-guidance-CYVpQszz.mjs → journey-guidance-CtQUfWar.mjs} +14 -3
  34. package/dist/{journey-guidance-CYVpQszz.mjs.map → journey-guidance-CtQUfWar.mjs.map} +1 -1
  35. package/dist/{oclif-BRdjVNne.mjs → oclif-CL6-M13m.mjs} +201 -43
  36. package/dist/oclif-CL6-M13m.mjs.map +1 -0
  37. package/dist/{orca-Dip6DPJ2.mjs → orca-BWB4zOdw.mjs} +749 -192
  38. package/dist/orca-BWB4zOdw.mjs.map +1 -0
  39. package/dist/{ports-V8JfiNgT.mjs → ports-Cu2Y7aXQ.mjs} +2 -2
  40. package/dist/{ports-V8JfiNgT.mjs.map → ports-Cu2Y7aXQ.mjs.map} +1 -1
  41. package/dist/{processes-BVqYsxT8.mjs → processes-Bn2WngOi.mjs} +1 -1
  42. package/dist/{processes-BVqYsxT8.mjs.map → processes-Bn2WngOi.mjs.map} +1 -1
  43. package/dist/{project-BGgv7Bba.mjs → project-yejJSfdJ.mjs} +57 -4
  44. package/dist/project-yejJSfdJ.mjs.map +1 -0
  45. package/dist/scaffold-manifest-CKNFLX9T.mjs +81 -0
  46. package/dist/scaffold-manifest-CKNFLX9T.mjs.map +1 -0
  47. package/dist/{sync-Cd3z7Q1R.mjs → sync-Cx2_4rXW.mjs} +99 -50
  48. package/dist/sync-Cx2_4rXW.mjs.map +1 -0
  49. package/dist/user-schema-W1OsXdpc.mjs +91 -0
  50. package/dist/user-schema-W1OsXdpc.mjs.map +1 -0
  51. package/oclif.manifest.json +227 -5
  52. package/package.json +8 -6
  53. package/dist/oclif-BRdjVNne.mjs.map +0 -1
  54. package/dist/orca-Dip6DPJ2.mjs.map +0 -1
  55. package/dist/project-BGgv7Bba.mjs.map +0 -1
  56. package/dist/sync-Cd3z7Q1R.mjs.map +0 -1
  57. package/dist/user-schema-DDz5-lX5.mjs +0 -13
  58. package/dist/user-schema-DDz5-lX5.mjs.map +0 -1
@@ -1,4 +1,4 @@
1
- import { x as publicCliCommand } from "./oclif-BRdjVNne.mjs";
1
+ import { k as publicCliCommand } from "./oclif-CL6-M13m.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  //#region src/lib/local-server/docker-guidance.ts
4
4
  function dockerUnavailableMessage(error) {
@@ -113,4 +113,4 @@ function isLoopback(host) {
113
113
  //#endregion
114
114
  export { dockerRuntimeGuidance as n, dockerUnavailableMessage as r, listenersForPort as t };
115
115
 
116
- //# sourceMappingURL=ports-V8JfiNgT.mjs.map
116
+ //# sourceMappingURL=ports-Cu2Y7aXQ.mjs.map
@@ -1 +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
+ {"version":3,"file":"ports-Cu2Y7aXQ.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"}
@@ -117,4 +117,4 @@ function errorMessage(error) {
117
117
  //#endregion
118
118
  export { discoverManagedRuntimeProcesses as t };
119
119
 
120
- //# sourceMappingURL=processes-BVqYsxT8.mjs.map
120
+ //# sourceMappingURL=processes-Bn2WngOi.mjs.map
@@ -1 +1 @@
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
+ {"version":3,"file":"processes-Bn2WngOi.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,5 +1,5 @@
1
- import { D as ZitadelError, T as parseJsonObject, w as isObject } from "./oclif-BRdjVNne.mjs";
2
- import { readFile, stat } from "node:fs/promises";
1
+ import { C as parseJsonObject, E as ZitadelError, S as isObject, T as stableStringify, n as DEFAULT_SERVER } from "./oclif-CL6-M13m.mjs";
2
+ import { chmod, readFile, rename, stat, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  //#region src/lib/project.ts
5
5
  /**
@@ -62,6 +62,25 @@ async function readZitadelSecret(cwd) {
62
62
  }
63
63
  }
64
64
  /**
65
+ * Writes `.zitadel/secret` back after a command mutated it (today only
66
+ * `zitadel claim`, which records `claimed_at` and `team_id`).
67
+ *
68
+ * Atomic by construction: the bytes land in a sibling temp file that is
69
+ * `chmod`ed and then `rename`d over the target, so a crash mid-write can never
70
+ * leave a truncated or world-readable credentials file — the same
71
+ * temp-then-rename the file-writer uses for managed files. Serialised with
72
+ * {@link stableStringify} at mode `0600` to match exactly how setup's patcher
73
+ * first wrote it, so a claim does not churn the file's formatting or loosen
74
+ * the permissions `doctor` asserts.
75
+ */
76
+ async function writeZitadelSecret(cwd, secret) {
77
+ const path = join(cwd, ".zitadel/secret");
78
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
79
+ await writeFile(tmp, `${stableStringify(secret)}\n`, { mode: 384 });
80
+ await chmod(tmp, 384).catch(() => void 0);
81
+ await rename(tmp, path);
82
+ }
83
+ /**
65
84
  * Reads the configured renderer id from a parsed `zitadel.json`, normalising the
66
85
  * legacy `default` alias to `react` and falling back to `react` when unset. The
67
86
  * value is validated downstream by `getRenderer`, so callers need not re-check.
@@ -71,6 +90,40 @@ function readRendererId(config) {
71
90
  const value = branding && typeof branding.renderer === "string" ? branding.renderer : "react";
72
91
  return value === "default" ? "react" : value;
73
92
  }
93
+ /**
94
+ * Reads the recorded sign-in preset from a parsed `zitadel.json`, if present.
95
+ * Setup persists it (see the patcher's `projectConfig`); absent means
96
+ * password-first per the setup default.
97
+ */
98
+ function readPreset(config) {
99
+ return typeof config.preset === "string" ? config.preset : void 0;
100
+ }
101
+ /**
102
+ * Reads the recorded use case from a parsed `zitadel.json`, if present.
103
+ * Guidance-only, like the field itself — behavior lives in the generated
104
+ * schema/flow files.
105
+ */
106
+ function readUseCase(config) {
107
+ return typeof config.useCase === "string" ? config.useCase : void 0;
108
+ }
109
+ /**
110
+ * Reads the top-level `server` origin from a parsed `zitadel.json`, falling
111
+ * back to {@link DEFAULT_SERVER} when the key is absent.
112
+ *
113
+ * Top-level only, unlike `resolveServer`, which prefers the selected
114
+ * environment's `server` before falling back here. That is the point: an
115
+ * environment override says where one invocation should talk, not where the
116
+ * project lives. No practical difference today, since setup always writes a
117
+ * concrete top-level `server`.
118
+ *
119
+ * This is the server the project actually lives on, which is not always the one
120
+ * a command is talking to: `doctor` pins its own source to the local runtime URL
121
+ * and `status --server local` overrides it. Anything reasoning about the
122
+ * project itself (rather than about this invocation) has to read it from here.
123
+ */
124
+ function readProjectServer(config) {
125
+ return typeof config.server === "string" ? config.server : DEFAULT_SERVER;
126
+ }
74
127
  /** Reads `environments.development.issuer` from a parsed `zitadel.json`, if present. */
75
128
  function readDevelopmentIssuer(config) {
76
129
  if (isObject(config.environments) && isObject(config.environments.development)) {
@@ -82,6 +135,6 @@ function isNotFound(error) {
82
135
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
83
136
  }
84
137
  //#endregion
85
- export { readZitadelConfig as a, readRendererId as i, hasZitadelSecret as n, readZitadelSecret as o, readDevelopmentIssuer as r, hasZitadelConfig as t };
138
+ export { readProjectServer as a, readZitadelConfig as c, readPreset as i, readZitadelSecret as l, hasZitadelSecret as n, readRendererId as o, readDevelopmentIssuer as r, readUseCase as s, hasZitadelConfig as t, writeZitadelSecret as u };
86
139
 
87
- //# sourceMappingURL=project-BGgv7Bba.mjs.map
140
+ //# sourceMappingURL=project-yejJSfdJ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-yejJSfdJ.mjs","names":[],"sources":["../src/lib/project.ts"],"sourcesContent":["import { chmod, readFile, rename, stat, writeFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\nimport { ZitadelError } from \"./errors\";\nimport { isObject, parseJsonObject, stableStringify } from \"./json\";\nimport { DEFAULT_SERVER } from \"./server\";\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 * When the project was attached to an owning team, recorded by\n * `zitadel claim`. Absent until then. The durable record lives on the\n * platform (the team's project grant); this is a local cache so commands can\n * answer \"is this already attached?\" without a round trip.\n */\n claimed_at?: string;\n /** The team that owns the project, recorded alongside {@link claimed_at}. */\n team_id?: 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 * Writes `.zitadel/secret` back after a command mutated it (today only\n * `zitadel claim`, which records `claimed_at` and `team_id`).\n *\n * Atomic by construction: the bytes land in a sibling temp file that is\n * `chmod`ed and then `rename`d over the target, so a crash mid-write can never\n * leave a truncated or world-readable credentials file — the same\n * temp-then-rename the file-writer uses for managed files. Serialised with\n * {@link stableStringify} at mode `0600` to match exactly how setup's patcher\n * first wrote it, so a claim does not churn the file's formatting or loosen\n * the permissions `doctor` asserts.\n */\nexport async function writeZitadelSecret(cwd: string, secret: ZitadelSecret): Promise<void> {\n const path = join(cwd, \".zitadel/secret\");\n const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;\n await writeFile(tmp, `${stableStringify(secret)}\\n`, { mode: 0o600 });\n // `writeFile`'s mode is masked by the process umask, so re-assert it.\n await chmod(tmp, 0o600).catch(() => undefined);\n await rename(tmp, path);\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/**\n * Reads the recorded sign-in preset from a parsed `zitadel.json`, if present.\n * Setup persists it (see the patcher's `projectConfig`); absent means\n * password-first per the setup default.\n */\nexport function readPreset(config: Record<string, unknown>): string | undefined {\n return typeof config.preset === \"string\" ? config.preset : undefined;\n}\n\n/**\n * Reads the recorded use case from a parsed `zitadel.json`, if present.\n * Guidance-only, like the field itself — behavior lives in the generated\n * schema/flow files.\n */\nexport function readUseCase(config: Record<string, unknown>): string | undefined {\n return typeof config.useCase === \"string\" ? config.useCase : undefined;\n}\n\n/**\n * Reads the top-level `server` origin from a parsed `zitadel.json`, falling\n * back to {@link DEFAULT_SERVER} when the key is absent.\n *\n * Top-level only, unlike `resolveServer`, which prefers the selected\n * environment's `server` before falling back here. That is the point: an\n * environment override says where one invocation should talk, not where the\n * project lives. No practical difference today, since setup always writes a\n * concrete top-level `server`.\n *\n * This is the server the project actually lives on, which is not always the one\n * a command is talking to: `doctor` pins its own source to the local runtime URL\n * and `status --server local` overrides it. Anything reasoning about the\n * project itself (rather than about this invocation) has to read it from here.\n */\nexport function readProjectServer(config: Record<string, unknown>): string {\n return typeof config.server === \"string\" ? config.server : DEFAULT_SERVER;\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":";;;;;;;;AAWA,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;;;;;;;;AA+BV,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;;;;;;;;;;;;;;;AAgBV,eAAsB,mBAAmB,KAAa,QAAsC;CAC1F,MAAM,OAAO,KAAK,KAAK,kBAAkB;CACzC,MAAM,MAAM,GAAG,KAAK,OAAO,QAAQ,IAAI,GAAG,KAAK,KAAK;AACpD,OAAM,UAAU,KAAK,GAAG,gBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,KAAO,CAAC;AAErE,OAAM,MAAM,KAAK,IAAM,CAAC,YAAY,KAAA,EAAU;AAC9C,OAAM,OAAO,KAAK,KAAK;;;;;;;AAQzB,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;;;;;;;AAQzC,SAAgB,WAAW,QAAqD;AAC9E,QAAO,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS,KAAA;;;;;;;AAQ7D,SAAgB,YAAY,QAAqD;AAC/E,QAAO,OAAO,OAAO,YAAY,WAAW,OAAO,UAAU,KAAA;;;;;;;;;;;;;;;;;AAkB/D,SAAgB,kBAAkB,QAAyC;AACzE,QAAO,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;;;AAI7D,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"}
@@ -0,0 +1,81 @@
1
+ import { S as isObject } from "./oclif-CL6-M13m.mjs";
2
+ import { i as updateScaffold, n as readState } from "./user-schema-W1OsXdpc.mjs";
3
+ import { readFile } from "node:fs/promises";
4
+ import { isAbsolute, join, relative, sep } from "node:path";
5
+ import { createHash } from "node:crypto";
6
+ //#region src/lib/scaffold-manifest.ts
7
+ /**
8
+ * Content hash of one scaffolded file as recorded in the scaffold manifest.
9
+ * Deliberately hashes the raw text (not a parsed structure): the manifest
10
+ * answers "are these still the bytes setup wrote?", nothing more. The sync
11
+ * loop's hashers work on JSON objects and serve a different contract.
12
+ */
13
+ function hashScaffoldFile(contents) {
14
+ return createHash("sha256").update(contents).digest("hex");
15
+ }
16
+ /**
17
+ * Read the scaffold manifest from `.zitadel/state.json`, or `undefined` when
18
+ * the file, the section, its `files` record, or any file entry is missing or
19
+ * malformed (each entry needs a string `hash` and a known `class`).
20
+ * `undefined` sends the doctor managed-files check into its template-derived
21
+ * fallback mode, so a corrupt or future-shaped manifest degrades rather than
22
+ * breaks.
23
+ */
24
+ async function readScaffoldManifest(cwd) {
25
+ try {
26
+ const scaffold = (await readState(cwd)).scaffold;
27
+ if (!isObject(scaffold)) return;
28
+ if (!isObject(scaffold.files)) return;
29
+ for (const entry of Object.values(scaffold.files)) {
30
+ if (!isObject(entry)) return;
31
+ if (typeof entry.hash !== "string") return;
32
+ if (entry.class !== "infrastructure" && entry.class !== "presentation") return;
33
+ }
34
+ if (scaffold.posture !== void 0 && scaffold.posture !== "page" && scaffold.posture !== "widget") return;
35
+ return scaffold;
36
+ } catch {
37
+ return;
38
+ }
39
+ }
40
+ /**
41
+ * Record which managed app files `zitadel setup` actually wrote (or found
42
+ * already matching), keyed by project-root-relative posix path with the
43
+ * content hash and ownership class. Only marked files that the scaffold run
44
+ * touched are recorded — a conditional file whose op never ran (e.g. the
45
+ * framework home page on a pre-existing app) is simply absent, which is what
46
+ * lets `doctor` later distinguish "CLI wrote this and it vanished" from "the
47
+ * CLI never owned this".
48
+ */
49
+ async function writeScaffoldManifest(options) {
50
+ const { cwd, actions, written, scaffoldedFramework, devPort, posture } = options;
51
+ const touched = new Set(written.map((path) => relativePosix(cwd, path)));
52
+ const files = {};
53
+ for (const path of actions.markedFiles) {
54
+ const key = relativePosix(cwd, path);
55
+ if (!touched.has(key)) continue;
56
+ let contents;
57
+ try {
58
+ contents = await readFile(join(cwd, key), "utf8");
59
+ } catch {
60
+ continue;
61
+ }
62
+ files[key] = {
63
+ hash: hashScaffoldFile(contents),
64
+ class: actions.fileClasses?.[path] ?? "presentation"
65
+ };
66
+ }
67
+ await updateScaffold(cwd, {
68
+ files,
69
+ ...scaffoldedFramework ? { scaffolded_framework: true } : {},
70
+ ...typeof devPort === "number" ? { dev_port: devPort } : {},
71
+ ...posture === void 0 ? {} : { posture }
72
+ });
73
+ }
74
+ /** Normalizes an absolute or relative path to a cwd-relative posix key. */
75
+ function relativePosix(cwd, path) {
76
+ return (isAbsolute(path) ? relative(cwd, path) : path).split(sep).join("/");
77
+ }
78
+ //#endregion
79
+ export { readScaffoldManifest as n, writeScaffoldManifest as r, hashScaffoldFile as t };
80
+
81
+ //# sourceMappingURL=scaffold-manifest-CKNFLX9T.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold-manifest-CKNFLX9T.mjs","names":[],"sources":["../src/lib/scaffold-manifest.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport { join, isAbsolute, relative, sep } from \"node:path\";\n\nimport { isObject } from \"./json\";\nimport type { EjectActions } from \"./orca/patchers/types\";\nimport { readState, updateScaffold } from \"./sync/state\";\nimport type { ScaffoldManifest, ScaffoldPosture } from \"./sync/types\";\n\n/**\n * Content hash of one scaffolded file as recorded in the scaffold manifest.\n * Deliberately hashes the raw text (not a parsed structure): the manifest\n * answers \"are these still the bytes setup wrote?\", nothing more. The sync\n * loop's hashers work on JSON objects and serve a different contract.\n */\nexport function hashScaffoldFile(contents: string): string {\n return createHash(\"sha256\").update(contents).digest(\"hex\");\n}\n\n/**\n * Read the scaffold manifest from `.zitadel/state.json`, or `undefined` when\n * the file, the section, its `files` record, or any file entry is missing or\n * malformed (each entry needs a string `hash` and a known `class`).\n * `undefined` sends the doctor managed-files check into its template-derived\n * fallback mode, so a corrupt or future-shaped manifest degrades rather than\n * breaks.\n */\nexport async function readScaffoldManifest(cwd: string): Promise<ScaffoldManifest | undefined> {\n try {\n const state = await readState(cwd);\n const scaffold = state.scaffold;\n // isObject rejects arrays too — `typeof [] === \"object\"`, and an array\n // `files` would otherwise enter manifest mode as \"0 tracked\".\n if (!isObject(scaffold)) {\n return undefined;\n }\n if (!isObject(scaffold.files)) {\n return undefined;\n }\n for (const entry of Object.values(scaffold.files)) {\n if (!isObject(entry)) {\n return undefined;\n }\n if (typeof entry.hash !== \"string\") {\n return undefined;\n }\n if (entry.class !== \"infrastructure\" && entry.class !== \"presentation\") {\n return undefined;\n }\n }\n // A posture value this CLI does not know is a future-shaped manifest —\n // degrade like any other malformed section rather than restoring a\n // posture the templates cannot render.\n if (\n scaffold.posture !== undefined &&\n scaffold.posture !== \"page\" &&\n scaffold.posture !== \"widget\"\n ) {\n return undefined;\n }\n return scaffold as unknown as ScaffoldManifest;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Record which managed app files `zitadel setup` actually wrote (or found\n * already matching), keyed by project-root-relative posix path with the\n * content hash and ownership class. Only marked files that the scaffold run\n * touched are recorded — a conditional file whose op never ran (e.g. the\n * framework home page on a pre-existing app) is simply absent, which is what\n * lets `doctor` later distinguish \"CLI wrote this and it vanished\" from \"the\n * CLI never owned this\".\n */\nexport async function writeScaffoldManifest(options: {\n cwd: string;\n actions: EjectActions;\n /** Paths the scaffold reported written or skipped-as-identical. */\n written: ReadonlyArray<string>;\n scaffoldedFramework?: boolean;\n devPort?: number;\n posture?: ScaffoldPosture;\n}): Promise<void> {\n const { cwd, actions, written, scaffoldedFramework, devPort, posture } = options;\n const touched = new Set(written.map((path) => relativePosix(cwd, path)));\n const files: ScaffoldManifest[\"files\"] = {};\n for (const path of actions.markedFiles) {\n const key = relativePosix(cwd, path);\n if (!touched.has(key)) {\n continue;\n }\n let contents: string;\n try {\n contents = await readFile(join(cwd, key), \"utf8\");\n } catch {\n continue;\n }\n files[key] = {\n hash: hashScaffoldFile(contents),\n class: actions.fileClasses?.[path] ?? \"presentation\",\n };\n }\n await updateScaffold(cwd, {\n files,\n ...(scaffoldedFramework ? { scaffolded_framework: true } : {}),\n ...(typeof devPort === \"number\" ? { dev_port: devPort } : {}),\n ...(posture === undefined ? {} : { posture }),\n });\n}\n\n/** Normalizes an absolute or relative path to a cwd-relative posix key. */\nfunction relativePosix(cwd: string, path: string): string {\n const rel = isAbsolute(path) ? relative(cwd, path) : path;\n return rel.split(sep).join(\"/\");\n}\n"],"mappings":";;;;;;;;;;;;AAeA,SAAgB,iBAAiB,UAA0B;AACzD,QAAO,WAAW,SAAS,CAAC,OAAO,SAAS,CAAC,OAAO,MAAM;;;;;;;;;;AAW5D,eAAsB,qBAAqB,KAAoD;AAC7F,KAAI;EAEF,MAAM,YAAW,MADG,UAAU,IAAI,EACX;AAGvB,MAAI,CAAC,SAAS,SAAS,CACrB;AAEF,MAAI,CAAC,SAAS,SAAS,MAAM,CAC3B;AAEF,OAAK,MAAM,SAAS,OAAO,OAAO,SAAS,MAAM,EAAE;AACjD,OAAI,CAAC,SAAS,MAAM,CAClB;AAEF,OAAI,OAAO,MAAM,SAAS,SACxB;AAEF,OAAI,MAAM,UAAU,oBAAoB,MAAM,UAAU,eACtD;;AAMJ,MACE,SAAS,YAAY,KAAA,KACrB,SAAS,YAAY,UACrB,SAAS,YAAY,SAErB;AAEF,SAAO;SACD;AACN;;;;;;;;;;;;AAaJ,eAAsB,sBAAsB,SAQ1B;CAChB,MAAM,EAAE,KAAK,SAAS,SAAS,qBAAqB,SAAS,YAAY;CACzE,MAAM,UAAU,IAAI,IAAI,QAAQ,KAAK,SAAS,cAAc,KAAK,KAAK,CAAC,CAAC;CACxE,MAAM,QAAmC,EAAE;AAC3C,MAAK,MAAM,QAAQ,QAAQ,aAAa;EACtC,MAAM,MAAM,cAAc,KAAK,KAAK;AACpC,MAAI,CAAC,QAAQ,IAAI,IAAI,CACnB;EAEF,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,SAAS,KAAK,KAAK,IAAI,EAAE,OAAO;UAC3C;AACN;;AAEF,QAAM,OAAO;GACX,MAAM,iBAAiB,SAAS;GAChC,OAAO,QAAQ,cAAc,SAAS;GACvC;;AAEH,OAAM,eAAe,KAAK;EACxB;EACA,GAAI,sBAAsB,EAAE,sBAAsB,MAAM,GAAG,EAAE;EAC7D,GAAI,OAAO,YAAY,WAAW,EAAE,UAAU,SAAS,GAAG,EAAE;EAC5D,GAAI,YAAY,KAAA,IAAY,EAAE,GAAG,EAAE,SAAS;EAC7C,CAAC;;;AAIJ,SAAS,cAAc,KAAa,MAAsB;AAExD,SADY,WAAW,KAAK,GAAG,SAAS,KAAK,KAAK,GAAG,MAC1C,MAAM,IAAI,CAAC,KAAK,IAAI"}
@@ -1,12 +1,15 @@
1
- import { D as ZitadelError, E as stableStringify, w as isObject } from "./oclif-BRdjVNne.mjs";
2
- import { t as SCHEMAS_DIR } from "./user-schema-DDz5-lX5.mjs";
1
+ import { E as ZitadelError, S as isObject, T as stableStringify } from "./oclif-CL6-M13m.mjs";
2
+ import { a as updateState, n as readState, r as removeFromState, t as SCHEMAS_DIR } from "./user-schema-W1OsXdpc.mjs";
3
+ import { a as toLocalBrandingBody, i as toBrandingWireBody, n as readDescriptorTemplate, r as resolveTemplatePath, t as BRANDING_DIR } from "./branding-BXj028-W.mjs";
3
4
  import { readFile, readdir, writeFile } from "node:fs/promises";
4
- import { join } from "node:path";
5
+ import { basename, join } from "node:path";
5
6
  import { DEFAULT_FLOW_SCHEMA_URI } from "@zitadel/config/defaults";
6
7
  import { consola as consola$1 } from "consola";
7
8
  import { createHash } from "node:crypto";
9
+ import { writeFileSync } from "node:fs";
8
10
  import { normalizeFlowBody, normalizeSchemaBody } from "@zitadel/config/normalize";
9
- import { flowConfigSchema, schemaConfigSchema } from "@zitadel/config/schemas";
11
+ import { brandingConfigSchema, flowConfigSchema, schemaConfigSchema } from "@zitadel/config/schemas";
12
+ import { validateLoginTemplate } from "@zitadel/config/template";
10
13
  import { validateFlowDefinition } from "@zitadel/config/validate";
11
14
  //#region src/lib/flows/env-refs.ts
12
15
  /**
@@ -51,7 +54,11 @@ const FLOWS_DIR = ".zitadel/flows";
51
54
  * sync loop.
52
55
  */
53
56
  function makeSyncers(opts) {
54
- return [new SchemaSyncer(opts.client, opts.projectId, opts.env), new FlowDefinitionSyncer(opts.client, opts.projectId, opts.env)];
57
+ return [
58
+ new SchemaSyncer(opts.client, opts.projectId, opts.env),
59
+ new FlowDefinitionSyncer(opts.client, opts.projectId, opts.env),
60
+ new BrandingSyncer(opts.client, opts.projectId, opts.env, opts.cwd)
61
+ ];
55
62
  }
56
63
  /**
57
64
  * Assert that every env var a resource references — `${VAR}` placeholders and
@@ -181,6 +188,87 @@ var FlowDefinitionSyncer = class {
181
188
  return (await this.client.getFlowDefinition(id, { project_id: this.projectId })).flow_definition;
182
189
  }
183
190
  };
191
+ /**
192
+ * Branding revisions (ADR 040): schema-style immutable semantics — every
193
+ * edit publishes a new revision via `POST /branding`, no update or delete.
194
+ * Unlike schemas, nothing references branding revisions, so a revise never
195
+ * triggers re-pinning. The descriptor keeps the template in a sibling
196
+ * `.liquid` file (`liquid_template_file`); this syncer inlines it for
197
+ * hashing and upload and splits it back out on write-back.
198
+ */
199
+ var BrandingSyncer = class {
200
+ kind = "branding";
201
+ directory = BRANDING_DIR;
202
+ mutable = false;
203
+ revisioned = true;
204
+ /** One project, one branding descriptor — extra .json files fail the scan. */
205
+ singletonFile = "branding.json";
206
+ constructor(client, projectId, env, cwd) {
207
+ this.client = client;
208
+ this.projectId = projectId;
209
+ this.env = env;
210
+ this.cwd = cwd;
211
+ }
212
+ /**
213
+ * The comparison form is the wire body with the template inlined, so an
214
+ * edit to the referenced `.liquid` file changes the state hash and plans
215
+ * a `revise` even though the descriptor JSON is untouched.
216
+ */
217
+ normalize = (data) => toBrandingWireBody(this.cwd, data);
218
+ /**
219
+ * Zod shape + env refs, then the authoritative template validation from
220
+ * `@zitadel/config/template` — the LiquidJS-dialect check the Go server
221
+ * cannot run (its save gate is lexical; see ADR 040).
222
+ */
223
+ validate(data) {
224
+ const result = brandingConfigSchema.safeParse(data);
225
+ if (!result.success) throw new ZitadelError("E_VALIDATION", "Branding file is not a valid branding descriptor", { details: { issues: result.error.issues } });
226
+ assertEnvRefs(data, this.env);
227
+ const template = readDescriptorTemplate(this.cwd, data);
228
+ if (template === void 0) return;
229
+ const issues = validateLoginTemplate(template).filter((issue) => issue.severity === "error");
230
+ if (issues.length > 0) throw new ZitadelError("E_VALIDATION", "Login template failed validation", {
231
+ details: { issues },
232
+ hint: "Fix the template issues; the rules live in docs/design/flowengine/template-security.md."
233
+ });
234
+ }
235
+ /**
236
+ * `POST /branding` publishes a new immutable revision. The canonical body
237
+ * is converted back to descriptor form: the stored template is written to
238
+ * the referenced `.liquid` file (when it differs) and the JSON keeps the
239
+ * file reference, so `writeBackResource` stays pure-JSON.
240
+ */
241
+ async create(data) {
242
+ const wire = toBrandingWireBody(this.cwd, data);
243
+ const result = await this.client.createBranding(wire, { project_id: this.projectId });
244
+ return {
245
+ id: result.id,
246
+ canonical: this.canonicalToLocal(result.branding, data)
247
+ };
248
+ }
249
+ async update(_id, _data) {
250
+ throw new ZitadelError("E_NOT_IMPLEMENTED", "branding is revisioned — edit publishes a new revision, not an update");
251
+ }
252
+ async delete(id) {
253
+ throw new ZitadelError("E_NOT_IMPLEMENTED", `branding delete is not supported (${id})`);
254
+ }
255
+ /** Wire form (template inlined); diffs compare in the normalized form. */
256
+ async fetch(id) {
257
+ return (await this.client.getBrandingById(id, { project_id: this.projectId })).branding;
258
+ }
259
+ canonicalToLocal(canonicalWire, localData) {
260
+ const local = localData;
261
+ const template = canonicalWire.liquid_template;
262
+ if (typeof local.liquid_template_file === "string" && typeof template === "string") {
263
+ const path = resolveTemplatePath(this.cwd, local.liquid_template_file);
264
+ if (readDescriptorTemplate(this.cwd, localData) !== template) {
265
+ writeFileSync(path, template);
266
+ consola$1.info(`Updated ${local.liquid_template_file} from the server's canonical response`);
267
+ }
268
+ }
269
+ return toLocalBrandingBody(canonicalWire, localData);
270
+ }
271
+ };
184
272
  //#endregion
185
273
  //#region src/lib/sync/flow-validation.ts
186
274
  /**
@@ -290,49 +378,6 @@ function resolveSchemaContent(action, scannedContents, stateResources) {
290
378
  consola$1.debug(`flow validation: no local schema for user_schema ${ref}; flow-local rules only`);
291
379
  }
292
380
  //#endregion
293
- //#region src/lib/sync/state.ts
294
- /**
295
- * Read and parse `.zitadel/state.json`. Throws if the file is
296
- * missing or malformed; callers run `zitadel setup` first to bring
297
- * the file into existence.
298
- */
299
- async function readState(cwd) {
300
- const raw = await readFile(join(cwd, ".zitadel/state.json"), "utf8");
301
- return JSON.parse(raw);
302
- }
303
- /**
304
- * Merge an entry into the state file under `key`, preserving any
305
- * fields the caller did not override. Reads the file, writes it back
306
- * with sorted keys disabled (state is engine-managed, not human-
307
- * authored, so deterministic ordering isn't required here).
308
- */
309
- async function updateState(cwd, key, entry) {
310
- const current = await readState(cwd);
311
- const updated = {
312
- ...current,
313
- resources: {
314
- ...current.resources,
315
- [key]: {
316
- ...current.resources[key],
317
- ...entry
318
- }
319
- }
320
- };
321
- await writeFile(join(cwd, ".zitadel/state.json"), JSON.stringify(updated, null, 2));
322
- }
323
- /**
324
- * Remove an entry from the state file. No-op if the key is absent.
325
- */
326
- async function removeFromState(cwd, key) {
327
- const current = await readState(cwd);
328
- const { [key]: _removed, ...rest } = current.resources;
329
- const updated = {
330
- ...current,
331
- resources: rest
332
- };
333
- await writeFile(join(cwd, ".zitadel/state.json"), JSON.stringify(updated, null, 2));
334
- }
335
- //#endregion
336
381
  //#region src/lib/sync/loop.ts
337
382
  /**
338
383
  * Compute the sync plan for `cwd` against the state file and (when
@@ -369,6 +414,10 @@ async function buildSyncPlan(cwd, syncers, fetchOld = false) {
369
414
  const dirPath = join(cwd, syncer.directory);
370
415
  consola$1.debug(`scanning ${syncer.directory}`);
371
416
  const onDisk = await readJsonDir(dirPath);
417
+ if (syncer.singletonFile) for (const absPath of onDisk.keys()) {
418
+ const name = basename(absPath);
419
+ if (name !== syncer.singletonFile) throw new ZitadelError("E_VALIDATION", `unexpected file in ${syncer.directory}: ${name}`, { hint: `${syncer.kind} is a singleton resource — keep exactly one descriptor at ${syncer.directory}/${syncer.singletonFile} and move other .json files out of the directory (each scanned descriptor would publish as its own revision).` });
420
+ }
372
421
  for (const content of onDisk.values()) syncer.validate(content);
373
422
  for (const [filePath, entry] of Object.entries(state.resources)) {
374
423
  if (!filePath.startsWith(syncer.directory)) continue;
@@ -1154,6 +1203,6 @@ function renderWarnings(warnings, blkPad, tty, lines) {
1154
1203
  for (const warning of warnings ?? []) lines.push(paint(`${blkPad}# warning: ${warning.message}`, A.yellow, tty));
1155
1204
  }
1156
1205
  //#endregion
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 };
1206
+ export { buildSyncPlan as a, writeBackResource as c, summarizePlan as i, makeSyncers as l, enumeratePlanResources as n, hashForState as o, renderPlan as r, runSyncLoop as s, collectPlanWarnings as t, FLOWS_DIR as u };
1158
1207
 
1159
- //# sourceMappingURL=sync-Cd3z7Q1R.mjs.map
1208
+ //# sourceMappingURL=sync-Cx2_4rXW.mjs.map