@zitadel/cli 0.1.0-alpha.9 → 1.0.0-alpha.21

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 (66) hide show
  1. package/README.md +250 -62
  2. package/SKILLS.md +221 -37
  3. package/dist/branding-cygSBPkV.mjs +79 -0
  4. package/dist/branding-cygSBPkV.mjs.map +1 -0
  5. package/dist/claim-state-DlfD6Res.mjs +66 -0
  6. package/dist/claim-state-DlfD6Res.mjs.map +1 -0
  7. package/dist/commands/apply.mjs +27 -8
  8. package/dist/commands/apply.mjs.map +1 -1
  9. package/dist/commands/branding/eject.mjs +104 -0
  10. package/dist/commands/branding/eject.mjs.map +1 -0
  11. package/dist/commands/claim.mjs +415 -0
  12. package/dist/commands/claim.mjs.map +1 -0
  13. package/dist/commands/doctor.mjs +594 -41
  14. package/dist/commands/doctor.mjs.map +1 -1
  15. package/dist/commands/eject.mjs +18 -6
  16. package/dist/commands/eject.mjs.map +1 -1
  17. package/dist/commands/logs.mjs +2 -2
  18. package/dist/commands/plan.mjs +19 -6
  19. package/dist/commands/plan.mjs.map +1 -1
  20. package/dist/commands/reset.mjs +2 -2
  21. package/dist/commands/schemas/list.mjs +146 -0
  22. package/dist/commands/schemas/list.mjs.map +1 -0
  23. package/dist/commands/setup.mjs +629 -214
  24. package/dist/commands/setup.mjs.map +1 -1
  25. package/dist/commands/start.mjs +4 -4
  26. package/dist/commands/start.mjs.map +1 -1
  27. package/dist/commands/status.mjs +52 -13
  28. package/dist/commands/status.mjs.map +1 -1
  29. package/dist/commands/stop.mjs +3 -3
  30. package/dist/commands/stop.mjs.map +1 -1
  31. package/dist/designs-Ckz18Dpo.mjs +38 -0
  32. package/dist/designs-Ckz18Dpo.mjs.map +1 -0
  33. package/dist/{docker-CnGQK3ZK.mjs → docker-Ev1Ggt9l.mjs} +10 -6
  34. package/dist/docker-Ev1Ggt9l.mjs.map +1 -0
  35. package/dist/environment-rjRVkJjW.mjs +17 -0
  36. package/dist/environment-rjRVkJjW.mjs.map +1 -0
  37. package/dist/journey-guidance-BGrOX_gT.mjs +40 -0
  38. package/dist/journey-guidance-BGrOX_gT.mjs.map +1 -0
  39. package/dist/oclif-CanO3zdt.mjs +1769 -0
  40. package/dist/oclif-CanO3zdt.mjs.map +1 -0
  41. package/dist/{orca-U142Wrau.mjs → orca-66vAxeIe.mjs} +1666 -305
  42. package/dist/orca-66vAxeIe.mjs.map +1 -0
  43. package/dist/package-manager-DFdLcDx1.mjs +194 -0
  44. package/dist/package-manager-DFdLcDx1.mjs.map +1 -0
  45. package/dist/{ports-B09RjuHx.mjs → ports-BM20XIZb.mjs} +25 -20
  46. package/dist/ports-BM20XIZb.mjs.map +1 -0
  47. package/dist/{processes-Cw8TO1SY.mjs → processes-DKHPkU8O.mjs} +1 -1
  48. package/dist/{processes-Cw8TO1SY.mjs.map → processes-DKHPkU8O.mjs.map} +1 -1
  49. package/dist/{project-Cd0L3PtM.mjs → project-CHhot85s.mjs} +57 -4
  50. package/dist/project-CHhot85s.mjs.map +1 -0
  51. package/dist/sync-BBlACKzT.mjs +1633 -0
  52. package/dist/sync-BBlACKzT.mjs.map +1 -0
  53. package/dist/user-schema-DTuOsdKE.mjs +91 -0
  54. package/dist/user-schema-DTuOsdKE.mjs.map +1 -0
  55. package/oclif.manifest.json +406 -5
  56. package/package.json +10 -5
  57. package/dist/docker-CnGQK3ZK.mjs.map +0 -1
  58. package/dist/docker-guidance-ypN3IM3o.mjs +0 -21
  59. package/dist/docker-guidance-ypN3IM3o.mjs.map +0 -1
  60. package/dist/oclif-B7lBzh3R.mjs +0 -808
  61. package/dist/oclif-B7lBzh3R.mjs.map +0 -1
  62. package/dist/orca-U142Wrau.mjs.map +0 -1
  63. package/dist/ports-B09RjuHx.mjs.map +0 -1
  64. package/dist/project-Cd0L3PtM.mjs.map +0 -1
  65. package/dist/sync-BojoQm2P.mjs +0 -733
  66. package/dist/sync-BojoQm2P.mjs.map +0 -1
@@ -1,733 +0,0 @@
1
- import { C as isObject, E as ZitadelError } from "./oclif-B7lBzh3R.mjs";
2
- import { consola as consola$1 } from "consola";
3
- import { join } from "node:path";
4
- import { readFile, readdir, writeFile } from "node:fs/promises";
5
- import { createHash } from "node:crypto";
6
- import { z } from "zod";
7
- import { CreateFlowDefinitionBody, CreateSchemaBody } from "@zitadel/api/generated/endpoints/zitadelNextGen.zod";
8
- //#region src/lib/environment.ts
9
- /**
10
- * CLI-side deployment environment. Not an API model — it gates which
11
- * `zitadel.json` environment block and server the commands target.
12
- * Project request/response shapes live in `@zitadel/api`
13
- * (generated from the OpenAPI spec).
14
- */
15
- const environmentSchema = z.enum([
16
- "development",
17
- "preview",
18
- "production"
19
- ]);
20
- //#endregion
21
- //#region src/lib/flows/validate.ts
22
- /**
23
- * The generated `CreateFlowDefinitionBody` Zod schema describes the
24
- * full envelope (`{project_id, flow_definition, schema_uri?}`); the
25
- * on-disk flow body is just the inner `flow_definition` shape. Pull
26
- * that out via `.shape` so on-disk validation runs against exactly the
27
- * same schema the wire request validates against.
28
- */
29
- const flowDefinitionBodySchema = CreateFlowDefinitionBody.shape.flow_definition;
30
- /**
31
- * Validate raw JSON bodies against the generated flow-definition Zod
32
- * schema (the orval-emitted equivalent of
33
- * `api/openapi/components/flows/flow-definition.yaml`). Errors from
34
- * every input are collected and rethrown as a single `E_VALIDATION`
35
- * `ZitadelError` so callers see the full picture at once rather than
36
- * failing on the first malformed entry.
37
- *
38
- * Pure: does not touch the filesystem or network. The input array
39
- * is read-only; the returned array is freshly allocated.
40
- *
41
- * @param flows - Raw values to validate. Unknown-typed so callers
42
- * can pass freshly-parsed JSON without first asserting a shape.
43
- */
44
- function validateFlows(flows) {
45
- const issues = [];
46
- const parsed = [];
47
- for (let i = 0; i < flows.length; i += 1) {
48
- const result = flowDefinitionBodySchema.safeParse(flows[i]);
49
- if (!result.success) {
50
- issues.push({
51
- index: i,
52
- issues: result.error.issues
53
- });
54
- continue;
55
- }
56
- parsed.push(result.data);
57
- }
58
- if (issues.length > 0) throw new ZitadelError("E_VALIDATION", "One or more flow definitions are invalid", { details: { issues } });
59
- return parsed;
60
- }
61
- //#endregion
62
- //#region src/lib/flows/env-refs.ts
63
- /**
64
- * Collects the environment variables a flows document depends on, sorted and
65
- * de-duplicated. Recognises two reference styles: inline `${VAR}` interpolations
66
- * inside string values, and keys ending in `_env` whose value names a single
67
- * variable. `apply`/`plan` use this to fail before contacting the platform when
68
- * a required variable is absent.
69
- */
70
- function flowEnvRefs(value) {
71
- const refs = /* @__PURE__ */ new Set();
72
- const visit = (node) => {
73
- if (typeof node === "string") for (const match of node.matchAll(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g)) {
74
- const ref = match[1];
75
- if (ref) refs.add(ref);
76
- }
77
- else if (Array.isArray(node)) node.forEach(visit);
78
- else if (isObject(node)) for (const [key, child] of Object.entries(node)) if (key.endsWith("_env") && typeof child === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(child)) refs.add(child);
79
- else visit(child);
80
- };
81
- visit(value);
82
- return [...refs].sort();
83
- }
84
- //#endregion
85
- //#region src/lib/flows/index.ts
86
- /**
87
- * Relative directory (from the project root) where local flow files
88
- * live. Owned here so callers (`commands/*`, `sync/syncers.ts`) and
89
- * tests share a single source of truth for the path; the runtime
90
- * never depends on it directly because `lib/flows` does not touch
91
- * the filesystem.
92
- */
93
- const FLOWS_DIR = ".zitadel/flows";
94
- //#endregion
95
- //#region src/lib/user-schema/index.ts
96
- /**
97
- * Relative directory (from the project root) where local user-schema
98
- * files live. Owned here so callers (`commands/*`, `sync/syncers.ts`)
99
- * and tests share a single source of truth for the path; the runtime
100
- * never depends on it directly because `lib/user-schema` does not touch
101
- * the filesystem. The counterpart of `lib/flows`' `FLOWS_DIR`.
102
- */
103
- const SCHEMAS_DIR = ".zitadel/schemas";
104
- //#endregion
105
- //#region src/lib/sync/syncers.ts
106
- /**
107
- * Build the syncer list with the context every syncer needs: the
108
- * `project_id` flow creates carry, and the runtime `env` against which
109
- * each file's `${VAR}` / `*_env` references are checked. Callers
110
- * (apply / plan / setup) read `project_id` from `.zitadel/secret` and
111
- * pass the process environment. The returned array is treated as
112
- * read-only by the sync loop.
113
- */
114
- function makeSyncers(opts) {
115
- return [new SchemaSyncer(opts.client, opts.projectId, opts.env), new FlowDefinitionSyncer(opts.client, opts.projectId, opts.env)];
116
- }
117
- /**
118
- * Assert that every env var a resource references — `${VAR}` placeholders and
119
- * the `*_env` convention — is present in `env`, throwing `E_VALIDATION` listing
120
- * the missing names. Shared by every syncer so the check is identical for
121
- * schemas and flows, and runs in the sync engine before any platform call.
122
- */
123
- function assertEnvRefs(data, env) {
124
- const missing = flowEnvRefs(data).filter((name) => !env[name]);
125
- if (missing.length > 0) throw new ZitadelError("E_VALIDATION", `Missing environment variables: ${missing.join(", ")}`);
126
- }
127
- var SchemaSyncer = class {
128
- kind = "schema";
129
- directory = SCHEMAS_DIR;
130
- mutable = false;
131
- constructor(client, projectId, env) {
132
- this.client = client;
133
- this.projectId = projectId;
134
- this.env = env;
135
- }
136
- /**
137
- * Parse against the generated `CreateSchemaBody` Zod (the orval-emitted
138
- * equivalent of `api/openapi/endpoints/schemas/user-schema.yaml`). The
139
- * generated schema is a union of `user-schema` and `schema-url`
140
- * discriminated on `kind`; both are valid on-disk bodies.
141
- */
142
- validate(data) {
143
- const result = CreateSchemaBody.safeParse(data);
144
- if (!result.success) throw new ZitadelError("E_VALIDATION", "Schema file is not a valid Zitadel schema body", { details: { issues: result.error.issues } });
145
- assertEnvRefs(data, this.env);
146
- }
147
- async create(data) {
148
- return (await this.client.createSchema(data, { project_id: this.projectId })).id;
149
- }
150
- /** Never called — schemas are immutable on the platform, so `mutable = false`. */
151
- async update(_id, _data) {}
152
- async delete(id) {
153
- throw new ZitadelError("E_NOT_IMPLEMENTED", `schema delete is not supported (${id})`);
154
- }
155
- async fetch(id) {
156
- return await this.client.getSchemaById(id, { project_id: this.projectId });
157
- }
158
- };
159
- var FlowDefinitionSyncer = class {
160
- kind = "flow";
161
- directory = FLOWS_DIR;
162
- mutable = true;
163
- constructor(client, projectId, env) {
164
- this.client = client;
165
- this.projectId = projectId;
166
- this.env = env;
167
- }
168
- /**
169
- * Validates one flow file. `validateFlows` takes a batch and throws
170
- * `E_VALIDATION` on the first invalid entry; passing a single-element array
171
- * lets us reuse the batch validator for one file.
172
- */
173
- validate(data) {
174
- validateFlows([data]);
175
- assertEnvRefs(data, this.env);
176
- }
177
- /**
178
- * Wraps the bare on-disk flow body in the spec's create-envelope
179
- * (`api/openapi/components/flows/flow-definition-create-request.yaml`)
180
- * before sending. The file on disk stays bare so it is human-editable;
181
- * only the wire request carries `project_id` and the surrounding
182
- * envelope.
183
- */
184
- async create(data) {
185
- return (await this.client.createFlowDefinition({
186
- project_id: this.projectId,
187
- flow_definition: data
188
- })).id;
189
- }
190
- /** PATCH body is the bare partial flow per `flow-definition-update-request` — no envelope. */
191
- async update(id, data) {
192
- await this.client.updateFlowDefinition(id, data);
193
- }
194
- async delete(id) {
195
- await this.client.deleteFlowDefinition(id);
196
- }
197
- /**
198
- * `GET /flow_definitions/:id` wraps the bare flow body in a detail envelope
199
- * (`id`, `project_id`, `schema_uri`, `status`, `created_at`, `updated_at`).
200
- * Strip those envelope fields here so the diff renderer compares
201
- * apples-to-apples against the on-disk file, which stores only the bare
202
- * body.
203
- */
204
- async fetch(id) {
205
- const { id: _id, project_id: _projectId, schema_uri: _schemaUri, status: _status, created_at: _createdAt, updated_at: _updatedAt, ...body } = await this.client.getFlowDefinition(id);
206
- return body;
207
- }
208
- };
209
- //#endregion
210
- //#region src/lib/sync/state.ts
211
- /**
212
- * Read and parse `.zitadel/state.json`. Throws if the file is
213
- * missing or malformed; callers run `zitadel setup` first to bring
214
- * the file into existence.
215
- */
216
- async function readState(cwd) {
217
- const raw = await readFile(join(cwd, ".zitadel/state.json"), "utf8");
218
- return JSON.parse(raw);
219
- }
220
- /**
221
- * Merge an entry into the state file under `key`, preserving any
222
- * fields the caller did not override. Reads the file, writes it back
223
- * with sorted keys disabled (state is engine-managed, not human-
224
- * authored, so deterministic ordering isn't required here).
225
- */
226
- async function updateState(cwd, key, entry) {
227
- const current = await readState(cwd);
228
- const updated = {
229
- ...current,
230
- resources: {
231
- ...current.resources,
232
- [key]: {
233
- ...current.resources[key],
234
- ...entry
235
- }
236
- }
237
- };
238
- await writeFile(join(cwd, ".zitadel/state.json"), JSON.stringify(updated, null, 2));
239
- }
240
- /**
241
- * Remove an entry from the state file. No-op if the key is absent.
242
- */
243
- async function removeFromState(cwd, key) {
244
- const current = await readState(cwd);
245
- const { [key]: _removed, ...rest } = current.resources;
246
- const updated = {
247
- ...current,
248
- resources: rest
249
- };
250
- await writeFile(join(cwd, ".zitadel/state.json"), JSON.stringify(updated, null, 2));
251
- }
252
- //#endregion
253
- //#region src/lib/sync/loop.ts
254
- /**
255
- * Compute the sync plan for `cwd` against the state file and (when
256
- * `fetchOld` is true) the platform API. The plan is read-only: it
257
- * decides what create/update/delete operations need to happen but
258
- * performs none of them. Pass it to {@link runSyncLoop} to execute.
259
- *
260
- * Validates every on-disk file (via `syncer.validate`) before planning any
261
- * work — a single malformed schema or flow aborts the whole run with
262
- * `E_VALIDATION` before any platform mutation. Both `plan` and `apply`
263
- * reach this code path.
264
- *
265
- * Bearer auth + base URL live in the api package's runtime registries
266
- * (`runtime/{auth,base-url}`). Callers set them once at command boot;
267
- * the sync engine doesn't carry a client.
268
- *
269
- * @param cwd - Project root.
270
- * @param syncers - Per-resource adapters. Order is preserved in the output.
271
- * @param fetchOld - When true, the planner fetches each delete/update target
272
- * from the platform to populate `oldContent` for diff rendering.
273
- */
274
- async function buildSyncPlan(cwd, syncers, fetchOld = false) {
275
- const state = await readState(cwd);
276
- const actions = [];
277
- for (const syncer of syncers) {
278
- const dirPath = join(cwd, syncer.directory);
279
- consola$1.debug(`scanning ${syncer.directory}`);
280
- const onDisk = await readJsonDir(dirPath);
281
- for (const content of onDisk.values()) syncer.validate(content);
282
- for (const [filePath, entry] of Object.entries(state.resources)) {
283
- if (!filePath.startsWith(syncer.directory)) continue;
284
- if (onDisk.has(join(cwd, filePath)) || !entry.id) continue;
285
- let oldContent = null;
286
- if (fetchOld && syncer.fetch) try {
287
- oldContent = await syncer.fetch(entry.id);
288
- } catch (err) {
289
- consola$1.debug(`fetch ${syncer.kind} ${entry.id} failed:`, err);
290
- }
291
- actions.push({
292
- kind: "delete",
293
- path: filePath,
294
- syncer,
295
- id: entry.id,
296
- oldContent
297
- });
298
- }
299
- for (const [absPath, content] of onDisk.entries()) {
300
- const relPath = absPath.slice(cwd.length + 1);
301
- const entry = state.resources[relPath];
302
- const hash = sha256(content);
303
- if (!entry?.id) {
304
- actions.push({
305
- kind: "create",
306
- path: relPath,
307
- syncer,
308
- content,
309
- hash
310
- });
311
- continue;
312
- }
313
- if (!syncer.mutable) {
314
- actions.push({
315
- kind: "skip",
316
- path: relPath,
317
- reason: "immutable"
318
- });
319
- continue;
320
- }
321
- if (entry.hash === hash) {
322
- actions.push({
323
- kind: "skip",
324
- path: relPath,
325
- reason: "no-change"
326
- });
327
- continue;
328
- }
329
- let oldContent = null;
330
- if (fetchOld && syncer.fetch) try {
331
- oldContent = await syncer.fetch(entry.id);
332
- } catch (err) {
333
- consola$1.debug(`fetch ${syncer.kind} ${entry.id} failed:`, err);
334
- }
335
- actions.push({
336
- kind: "update",
337
- path: relPath,
338
- syncer,
339
- id: entry.id,
340
- content,
341
- hash,
342
- oldContent
343
- });
344
- }
345
- }
346
- return actions;
347
- }
348
- /**
349
- * Execute every action returned by {@link buildSyncPlan} against the
350
- * platform. Updates the local state file (`.zitadel/state.json`) as
351
- * each action completes so an interrupted run can resume.
352
- *
353
- * The platform target (base URL + bearer auth) lives in the api
354
- * package's runtime registries; callers set them before invoking this.
355
- *
356
- * @param cwd - Project root.
357
- * @param syncers - Per-resource adapters; same list passed to
358
- * `buildSyncPlan`.
359
- */
360
- async function runSyncLoop(cwd, syncers) {
361
- const actions = await buildSyncPlan(cwd, syncers);
362
- for (const action of actions) switch (action.kind) {
363
- case "create": {
364
- const id = await action.syncer.create(action.content);
365
- await updateState(cwd, action.path, {
366
- id,
367
- hash: action.hash
368
- });
369
- consola$1.info(`Created a new ${action.syncer.kind} on Zitadel from ${action.path} (id ${id})`);
370
- break;
371
- }
372
- case "update":
373
- await action.syncer.update(action.id, action.content);
374
- await updateState(cwd, action.path, { hash: action.hash });
375
- consola$1.info(`Updated the ${action.syncer.kind} on Zitadel from ${action.path}`);
376
- break;
377
- case "delete":
378
- await action.syncer.delete(action.id);
379
- await removeFromState(cwd, action.path);
380
- consola$1.info(`Deleted the ${action.syncer.kind} on Zitadel because ${action.path} was removed locally`);
381
- break;
382
- case "skip":
383
- consola$1.debug(`Skipped ${action.path} (${action.reason})`);
384
- break;
385
- }
386
- }
387
- async function readJsonDir(dirPath) {
388
- const result = /* @__PURE__ */ new Map();
389
- let entries;
390
- try {
391
- entries = await readdir(dirPath);
392
- } catch (err) {
393
- if (typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT") return result;
394
- throw err;
395
- }
396
- for (const entry of entries.filter((e) => e.endsWith(".json"))) {
397
- const filePath = join(dirPath, entry);
398
- const raw = await readFile(filePath, "utf8");
399
- result.set(filePath, JSON.parse(raw));
400
- }
401
- return result;
402
- }
403
- function sha256(data) {
404
- return createHash("sha256").update(JSON.stringify(data)).digest("hex");
405
- }
406
- //#endregion
407
- //#region src/lib/sync/plan-renderer.ts
408
- /**
409
- * Count the non-`skip` actions in a {@link buildSyncPlan} result. Pure; the
410
- * single source of truth for the plan counts shared by the `plan` /
411
- * `apply --dry-run` JSON payload and {@link renderPlan}'s summary line.
412
- */
413
- function summarizePlan(actions) {
414
- const active = actions.filter((a) => a.kind !== "skip");
415
- return {
416
- creates: active.filter((a) => a.kind === "create").length,
417
- updates: active.filter((a) => a.kind === "update").length,
418
- deletes: active.filter((a) => a.kind === "delete").length,
419
- total: active.length
420
- };
421
- }
422
- /**
423
- * Render a {@link buildSyncPlan} result as a human-readable Terraform-style
424
- * plan. TTY-aware: colors and bold are emitted only when `tty` is true.
425
- * Returns the empty-state message when every action is `skip`.
426
- *
427
- * @param actions - The action list produced by `buildSyncPlan`. Read-only;
428
- * the function never mutates the input.
429
- * @param tty - True when stdout is a TTY; controls ANSI emission.
430
- */
431
- function renderPlan(actions, tty) {
432
- const active = actions.filter((a) => a.kind !== "skip");
433
- if (active.length === 0) return paint("No changes. Your Zitadel configuration matches the current state.", A.bold, tty);
434
- const out = [];
435
- out.push(paint("Zitadel will perform the following actions:", A.bold, tty));
436
- for (const action of active) {
437
- out.push("");
438
- out.push(...renderBlock(action, tty));
439
- }
440
- out.push("");
441
- const { creates, updates, deletes } = summarizePlan(actions);
442
- const parts = [];
443
- if (creates > 0) parts.push(`${creates} to add`);
444
- if (updates > 0) parts.push(`${updates} to change`);
445
- if (deletes > 0) parts.push(`${deletes} to destroy`);
446
- out.push(paint(`Plan: ${parts.join(", ")}.`, A.bold, tty));
447
- return out.join("\n");
448
- }
449
- const A = {
450
- reset: "\x1B[0m",
451
- bold: "\x1B[1m",
452
- green: "\x1B[32m",
453
- red: "\x1B[31m",
454
- yellow: "\x1B[33m"
455
- };
456
- function paint(text, code, tty) {
457
- return tty ? `${code}${text}${A.reset}` : text;
458
- }
459
- function isPrimitive(v) {
460
- return v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean";
461
- }
462
- function isPlainObject(v) {
463
- return typeof v === "object" && v !== null && !Array.isArray(v);
464
- }
465
- const KNOWN_AFTER_APPLY = "(known after apply)";
466
- function escapeString(s) {
467
- return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
468
- }
469
- function fmtPrimitive(v) {
470
- if (v === null) return "null";
471
- if (typeof v === "string" && v === KNOWN_AFTER_APPLY) return KNOWN_AFTER_APPLY;
472
- if (typeof v === "string") return `"${escapeString(v)}"`;
473
- return String(v);
474
- }
475
- function prefixAnsi(p) {
476
- if (p === "+") return A.green;
477
- if (p === "-") return A.red;
478
- if (p === "~") return A.yellow;
479
- return "";
480
- }
481
- function renderFields(obj, prefix, prefixCol, ctx, lines) {
482
- const pad = " ".repeat(prefixCol);
483
- const ansi = prefixAnsi(prefix);
484
- const col = (s) => paint(s, ansi, ctx.tty);
485
- const keys = Object.keys(obj).sort();
486
- const maxLen = keys.reduce((m, k) => Math.max(m, k.length), 0);
487
- for (const key of keys) {
488
- const val = obj[key];
489
- const pk = key.padEnd(maxLen);
490
- if (isPrimitive(val)) {
491
- const formatted = fmtPrimitive(val);
492
- const suffix = ctx.deleteMode ? " -> null" : "";
493
- lines.push(col(`${pad}${prefix} ${pk} = ${formatted}${suffix}`));
494
- } else if (Array.isArray(val)) if (val.length === 0) lines.push(col(`${pad}${prefix} ${pk} = []`));
495
- else {
496
- lines.push(col(`${pad}${prefix} ${pk} = [`));
497
- renderArrayItems(val, prefix, prefixCol + 4, ctx, lines);
498
- lines.push(col(`${" ".repeat(prefixCol + 2)}]`));
499
- }
500
- else if (isPlainObject(val)) if (Object.keys(val).length === 0) lines.push(col(`${pad}${prefix} ${pk} = {}`));
501
- else {
502
- lines.push(col(`${pad}${prefix} ${pk} = {`));
503
- renderFields(val, prefix, prefixCol + 4, ctx, lines);
504
- lines.push(col(`${" ".repeat(prefixCol + 2)}}`));
505
- }
506
- }
507
- }
508
- /**
509
- * Renders the items of an array. Unlike {@link renderFields}, primitive
510
- * elements never get a trailing ` -> null` suffix even under `deleteMode` —
511
- * Terraform only annotates scalar object-field removals that way, not array
512
- * items.
513
- */
514
- function renderArrayItems(arr, prefix, prefixCol, ctx, lines) {
515
- const pad = " ".repeat(prefixCol);
516
- const ansi = prefixAnsi(prefix);
517
- const col = (s) => paint(s, ansi, ctx.tty);
518
- for (const item of arr) if (isPrimitive(item)) {
519
- const formatted = fmtPrimitive(item);
520
- lines.push(col(`${pad}${prefix} ${formatted},`));
521
- } else if (Array.isArray(item)) if (item.length === 0) lines.push(col(`${pad}${prefix} [],`));
522
- else {
523
- lines.push(col(`${pad}${prefix} [`));
524
- renderArrayItems(item, prefix, prefixCol + 4, ctx, lines);
525
- lines.push(col(`${" ".repeat(prefixCol + 2)}],`));
526
- }
527
- else if (isPlainObject(item)) if (Object.keys(item).length === 0) lines.push(col(`${pad}${prefix} {},`));
528
- else {
529
- lines.push(col(`${pad}${prefix} {`));
530
- renderFields(item, prefix, prefixCol + 4, ctx, lines);
531
- lines.push(col(`${" ".repeat(prefixCol + 2)}},`));
532
- }
533
- }
534
- /**
535
- * Walks both old and new objects, emitting Terraform-style change lines.
536
- * Returns true if any actual change line (+ / - / ~) was emitted.
537
- *
538
- * Edge cases:
539
- * - Changed arrays render as a full remove + full add (no LCS diff).
540
- * - Nested objects recurse, and the outer key is only marked `~` if a child
541
- * actually changed; unchanged children render with the neutral prefix.
542
- * - A value whose type changed (e.g. string → object) also renders as a
543
- * remove + add pair.
544
- */
545
- function renderDiff(oldObj, newObj, prefixCol, tty, lines) {
546
- const allKeys = [...new Set([...Object.keys(oldObj), ...Object.keys(newObj)])].sort();
547
- const maxLen = allKeys.reduce((m, k) => Math.max(m, k.length), 0);
548
- const pad = " ".repeat(prefixCol);
549
- let hasChanges = false;
550
- for (const key of allKeys) {
551
- const pk = key.padEnd(maxLen);
552
- const hasOld = Object.prototype.hasOwnProperty.call(oldObj, key);
553
- const hasNew = Object.prototype.hasOwnProperty.call(newObj, key);
554
- const oldVal = oldObj[key];
555
- const newVal = newObj[key];
556
- if (!hasOld) {
557
- hasChanges = true;
558
- const col = (s) => paint(s, A.green, tty);
559
- if (isPrimitive(newVal)) lines.push(col(`${pad}+ ${pk} = ${fmtPrimitive(newVal)}`));
560
- else if (Array.isArray(newVal)) {
561
- lines.push(col(`${pad}+ ${pk} = [`));
562
- renderArrayItems(newVal, "+", prefixCol + 4, {
563
- tty,
564
- deleteMode: false
565
- }, lines);
566
- lines.push(col(`${" ".repeat(prefixCol + 2)}]`));
567
- } else if (isPlainObject(newVal)) {
568
- lines.push(col(`${pad}+ ${pk} = {`));
569
- renderFields(newVal, "+", prefixCol + 4, {
570
- tty,
571
- deleteMode: false
572
- }, lines);
573
- lines.push(col(`${" ".repeat(prefixCol + 2)}}`));
574
- }
575
- } else if (!hasNew) {
576
- hasChanges = true;
577
- const col = (s) => paint(s, A.red, tty);
578
- if (isPrimitive(oldVal)) lines.push(col(`${pad}- ${pk} = ${fmtPrimitive(oldVal)} -> null`));
579
- else if (Array.isArray(oldVal)) {
580
- lines.push(col(`${pad}- ${pk} = [`));
581
- renderArrayItems(oldVal, "-", prefixCol + 4, {
582
- tty,
583
- deleteMode: false
584
- }, lines);
585
- lines.push(col(`${" ".repeat(prefixCol + 2)}]`));
586
- } else if (isPlainObject(oldVal)) {
587
- lines.push(col(`${pad}- ${pk} = {`));
588
- renderFields(oldVal, "-", prefixCol + 4, {
589
- tty,
590
- deleteMode: true
591
- }, lines);
592
- lines.push(col(`${" ".repeat(prefixCol + 2)}}`));
593
- }
594
- } else if (isPrimitive(oldVal) && isPrimitive(newVal)) if (oldVal === newVal) lines.push(`${pad} ${pk} = ${fmtPrimitive(newVal)}`);
595
- else {
596
- hasChanges = true;
597
- const col = (s) => paint(s, A.yellow, tty);
598
- lines.push(col(`${pad}~ ${pk} = ${fmtPrimitive(oldVal)} -> ${fmtPrimitive(newVal)}`));
599
- }
600
- else if (Array.isArray(oldVal) && Array.isArray(newVal)) if (JSON.stringify(oldVal) === JSON.stringify(newVal)) if (newVal.length === 0) lines.push(`${pad} ${pk} = []`);
601
- else {
602
- lines.push(`${pad} ${pk} = [`);
603
- renderArrayItems(newVal, " ", prefixCol + 4, {
604
- tty,
605
- deleteMode: false
606
- }, lines);
607
- lines.push(`${" ".repeat(prefixCol + 2)}]`);
608
- }
609
- else {
610
- hasChanges = true;
611
- const colR = (s) => paint(s, A.red, tty);
612
- const colA = (s) => paint(s, A.green, tty);
613
- if (oldVal.length === 0) lines.push(colR(`${pad}- ${pk} = []`));
614
- else {
615
- lines.push(colR(`${pad}- ${pk} = [`));
616
- renderArrayItems(oldVal, "-", prefixCol + 4, {
617
- tty,
618
- deleteMode: false
619
- }, lines);
620
- lines.push(colR(`${" ".repeat(prefixCol + 2)}]`));
621
- }
622
- if (newVal.length === 0) lines.push(colA(`${pad}+ ${pk} = []`));
623
- else {
624
- lines.push(colA(`${pad}+ ${pk} = [`));
625
- renderArrayItems(newVal, "+", prefixCol + 4, {
626
- tty,
627
- deleteMode: false
628
- }, lines);
629
- lines.push(colA(`${" ".repeat(prefixCol + 2)}]`));
630
- }
631
- }
632
- else if (isPlainObject(oldVal) && isPlainObject(newVal)) {
633
- const childLines = [];
634
- if (renderDiff(oldVal, newVal, prefixCol + 4, tty, childLines)) {
635
- hasChanges = true;
636
- const col = (s) => paint(s, A.yellow, tty);
637
- lines.push(col(`${pad}~ ${pk} = {`));
638
- lines.push(...childLines);
639
- lines.push(col(`${" ".repeat(prefixCol + 2)}}`));
640
- } else if (childLines.length > 0) {
641
- lines.push(`${pad} ${pk} = {`);
642
- lines.push(...childLines);
643
- lines.push(`${" ".repeat(prefixCol + 2)}}`);
644
- } else lines.push(`${pad} ${pk} = {}`);
645
- } else {
646
- hasChanges = true;
647
- const colR = (s) => paint(s, A.red, tty);
648
- const colA = (s) => paint(s, A.green, tty);
649
- if (isPrimitive(oldVal)) lines.push(colR(`${pad}- ${pk} = ${fmtPrimitive(oldVal)} -> null`));
650
- if (isPrimitive(newVal)) lines.push(colA(`${pad}+ ${pk} = ${fmtPrimitive(newVal)}`));
651
- }
652
- }
653
- return hasChanges;
654
- }
655
- /**
656
- * Column layout (matches Terraform's per-block format):
657
- * BLOCK_COL = 2 — where the +/-/~ sits on the resource opening line
658
- * FIELD_COL = 6 — where the +/-/~ sits on first-level field lines
659
- * closing } — at BLOCK_COL + 2 = 4, no prefix
660
- */
661
- const BLOCK_COL = 2;
662
- const FIELD_COL = 6;
663
- function resourceName(path) {
664
- return path.split("/").pop() ?? path;
665
- }
666
- /**
667
- * Renders one Terraform-style resource block for a single `SyncAction`.
668
- *
669
- * Per-case notes:
670
- * - **create**: a synthetic `id = (known after apply)` is injected into the
671
- * rendered fields so it sorts alphabetically alongside the real keys.
672
- * - **delete**: when `oldContent` is null (the fetch failed), the body
673
- * collapses to a single `- id = "<id>" -> null` line.
674
- * - **update**: when `oldContent` is null (no read endpoint for this
675
- * resource kind), the field diff is replaced with a placeholder
676
- * "field diff unavailable" line.
677
- * - **skip**: omitted from the output entirely, matching Terraform's
678
- * default of not showing no-change resources.
679
- */
680
- function renderBlock(action, tty) {
681
- const lines = [];
682
- const blkPad = " ".repeat(BLOCK_COL);
683
- const closePad = " ".repeat(BLOCK_COL + 2);
684
- switch (action.kind) {
685
- case "create": {
686
- const header = `${blkPad}# ${action.path} will be created`;
687
- const opening = `${blkPad}+ resource "${action.syncer.kind}" "${resourceName(action.path)}" {`;
688
- lines.push(paint(header, A.bold, tty));
689
- lines.push(paint(opening, A.green, tty));
690
- renderFields({
691
- id: KNOWN_AFTER_APPLY,
692
- ...action.content
693
- }, "+", FIELD_COL, {
694
- tty,
695
- deleteMode: false
696
- }, lines);
697
- lines.push(`${closePad}}`);
698
- break;
699
- }
700
- case "delete": {
701
- const header = `${blkPad}# ${action.path} will be destroyed`;
702
- const opening = `${blkPad}- resource "${action.syncer.kind}" "${resourceName(action.path)}" {`;
703
- lines.push(paint(header, A.bold, tty));
704
- lines.push(paint(opening, A.red, tty));
705
- if (action.oldContent) renderFields({
706
- id: action.id,
707
- ...action.oldContent
708
- }, "-", FIELD_COL, {
709
- tty,
710
- deleteMode: true
711
- }, lines);
712
- else lines.push(paint(`${" ".repeat(FIELD_COL)}- id = "${action.id}" -> null`, A.red, tty));
713
- lines.push(`${closePad}}`);
714
- break;
715
- }
716
- case "update": {
717
- const header = `${blkPad}# ${action.path} will be updated in-place`;
718
- const opening = `${blkPad}~ resource "${action.syncer.kind}" "${resourceName(action.path)}" {`;
719
- lines.push(paint(header, A.bold, tty));
720
- lines.push(paint(opening, A.yellow, tty));
721
- if (action.oldContent) renderDiff(action.oldContent, action.content, FIELD_COL, tty, lines);
722
- else lines.push(`${" ".repeat(FIELD_COL)} # (field diff unavailable — no read endpoint for ${action.syncer.kind})`);
723
- lines.push(`${closePad}}`);
724
- break;
725
- }
726
- case "skip": break;
727
- }
728
- return lines;
729
- }
730
- //#endregion
731
- export { makeSyncers as a, runSyncLoop as i, summarizePlan as n, environmentSchema as o, buildSyncPlan as r, renderPlan as t };
732
-
733
- //# sourceMappingURL=sync-BojoQm2P.mjs.map