@dragonmastery/tamer 0.1.1 → 0.28.0

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 (89) hide show
  1. package/README.md +569 -18
  2. package/dist/CFApiClient-DhbyyV71.mjs +868 -0
  3. package/dist/CFApiClient-DhbyyV71.mjs.map +1 -0
  4. package/dist/StateManager-DTqtLLVX.mjs +760 -0
  5. package/dist/StateManager-DTqtLLVX.mjs.map +1 -0
  6. package/dist/apply-B0b_jjGv.mjs +423 -0
  7. package/dist/apply-B0b_jjGv.mjs.map +1 -0
  8. package/dist/applyTarget-BetDYdeS.mjs +152 -0
  9. package/dist/applyTarget-BetDYdeS.mjs.map +1 -0
  10. package/dist/bootstrap-CBzPilB1.mjs +33 -0
  11. package/dist/bootstrap-CBzPilB1.mjs.map +1 -0
  12. package/dist/buildDispatchUploadForm-BoUB93b3.mjs +38 -0
  13. package/dist/buildDispatchUploadForm-BoUB93b3.mjs.map +1 -0
  14. package/dist/cloudflareSnapshot-B4FOaNr0.mjs +163 -0
  15. package/dist/cloudflareSnapshot-B4FOaNr0.mjs.map +1 -0
  16. package/dist/deploy-gHEQxhmx.mjs +119 -0
  17. package/dist/deploy-gHEQxhmx.mjs.map +1 -0
  18. package/dist/destroy-B21f3wgq.mjs +215 -0
  19. package/dist/destroy-B21f3wgq.mjs.map +1 -0
  20. package/dist/destroy-tenant-BW2nasnK.mjs +103 -0
  21. package/dist/destroy-tenant-BW2nasnK.mjs.map +1 -0
  22. package/dist/dev-Dt26nzMJ.mjs +103 -0
  23. package/dist/dev-Dt26nzMJ.mjs.map +1 -0
  24. package/dist/dns-records.resolve-C2T0m4NG.mjs +3 -0
  25. package/dist/dns-records.resolve-DwBR_1WI.mjs +47 -0
  26. package/dist/dns-records.resolve-DwBR_1WI.mjs.map +1 -0
  27. package/dist/dns-records.sync-Bpzz9H0s.mjs +75 -0
  28. package/dist/dns-records.sync-Bpzz9H0s.mjs.map +1 -0
  29. package/dist/doctor-C_hs7k2D.mjs +34 -0
  30. package/dist/doctor-C_hs7k2D.mjs.map +1 -0
  31. package/dist/drift-D5qzCTft.mjs +10 -0
  32. package/dist/drift-D8ZrSgTn.mjs +323 -0
  33. package/dist/drift-D8ZrSgTn.mjs.map +1 -0
  34. package/dist/events-BSwGdkGj.mjs +68 -0
  35. package/dist/events-BSwGdkGj.mjs.map +1 -0
  36. package/dist/fetchStackImports-B4ZJahOt.mjs +3817 -0
  37. package/dist/fetchStackImports-B4ZJahOt.mjs.map +1 -0
  38. package/dist/generator-CIMbcPzv.mjs +77 -0
  39. package/dist/generator-CIMbcPzv.mjs.map +1 -0
  40. package/dist/import-BrduwA9Z.mjs +164 -0
  41. package/dist/import-BrduwA9Z.mjs.map +1 -0
  42. package/dist/index.d.mts +6592 -56
  43. package/dist/index.d.mts.map +1 -1
  44. package/dist/index.mjs +18 -1
  45. package/dist/index.mjs.map +1 -0
  46. package/dist/loader-DP7yXqT6.mjs +518 -0
  47. package/dist/loader-DP7yXqT6.mjs.map +1 -0
  48. package/dist/logpush-job-xS7270FZ.mjs +1106 -0
  49. package/dist/logpush-job-xS7270FZ.mjs.map +1 -0
  50. package/dist/migrate-CahG6BYV.mjs +87 -0
  51. package/dist/migrate-CahG6BYV.mjs.map +1 -0
  52. package/dist/normalize-Bx0bpFop.mjs +236 -0
  53. package/dist/normalize-Bx0bpFop.mjs.map +1 -0
  54. package/dist/plan-DWvsvy1U.mjs +453 -0
  55. package/dist/plan-DWvsvy1U.mjs.map +1 -0
  56. package/dist/planFormat-CJw8Kq2s.mjs +119 -0
  57. package/dist/planFormat-CJw8Kq2s.mjs.map +1 -0
  58. package/dist/provision-tenant-WTKo93Y0.mjs +192 -0
  59. package/dist/provision-tenant-WTKo93Y0.mjs.map +1 -0
  60. package/dist/r2S3EmptyBucket-DD81ZWQ7.mjs +92 -0
  61. package/dist/r2S3EmptyBucket-DD81ZWQ7.mjs.map +1 -0
  62. package/dist/stackOutputs-W9mnnJuj.mjs +69 -0
  63. package/dist/stackOutputs-W9mnnJuj.mjs.map +1 -0
  64. package/dist/status-DLwREPjb.mjs +198 -0
  65. package/dist/status-DLwREPjb.mjs.map +1 -0
  66. package/dist/sync-f2K2blwm.mjs +90 -0
  67. package/dist/sync-f2K2blwm.mjs.map +1 -0
  68. package/dist/tamer.d.mts +1 -0
  69. package/dist/tamer.mjs +4553 -0
  70. package/dist/tamer.mjs.map +1 -0
  71. package/dist/tamerArtifactsR2-Ccgplu2Q.mjs +52 -0
  72. package/dist/tamerArtifactsR2-Ccgplu2Q.mjs.map +1 -0
  73. package/dist/types-CqxqYnrT.mjs +44 -0
  74. package/dist/types-CqxqYnrT.mjs.map +1 -0
  75. package/dist/verifyPlanFile-c16z1AMH.mjs +33 -0
  76. package/dist/verifyPlanFile-c16z1AMH.mjs.map +1 -0
  77. package/dist/wfp-delete-DysvX1u7.mjs +36 -0
  78. package/dist/wfp-delete-DysvX1u7.mjs.map +1 -0
  79. package/dist/wfp-put-jaVd_LjO.mjs +52 -0
  80. package/dist/wfp-put-jaVd_LjO.mjs.map +1 -0
  81. package/dist/worker-route-Be2IvOdr.mjs +263 -0
  82. package/dist/worker-route-Be2IvOdr.mjs.map +1 -0
  83. package/dist/workers-aGILs77X.mjs +87 -0
  84. package/dist/workers-aGILs77X.mjs.map +1 -0
  85. package/dist/wranglerSpawn-DmEz0ldT.mjs +24 -0
  86. package/dist/wranglerSpawn-DmEz0ldT.mjs.map +1 -0
  87. package/dist/zoneResolver-VoxLHM4N.mjs +32 -0
  88. package/dist/zoneResolver-VoxLHM4N.mjs.map +1 -0
  89. package/package.json +42 -4
@@ -0,0 +1,152 @@
1
+ import { f as getDispatchNamespaces, m as getLogpushJobs, p as getDnsRecords } from "./normalize-Bx0bpFop.mjs";
2
+ import { a as resourceModules, o as logicalNamesForResourceKind } from "./fetchStackImports-B4ZJahOt.mjs";
3
+ import { a as effectiveDnsRecordProxied, i as effectiveDnsRecordComment, o as effectiveDnsRecordTtl, r as dnsRecordStateKey, t as dnsRecordAppliesToEnv } from "./dns-records.resolve-DwBR_1WI.mjs";
4
+
5
+ //#region src/features/dns-records/dns-records.diff.ts
6
+ function dnsRecordDiffPlanItems(args) {
7
+ const { resources, tenant, env, state } = args;
8
+ const items = [];
9
+ for (const config of resources) {
10
+ if (!dnsRecordAppliesToEnv(config, env)) continue;
11
+ const key = dnsRecordStateKey(config.zoneId, config.type, config.name);
12
+ const entry = state.get(key);
13
+ if (!entry || entry.type !== "dns_record") continue;
14
+ const stateEntry = entry;
15
+ if (stateEntry.recordType !== config.type) {
16
+ items.push({
17
+ kind: "dns_record",
18
+ action: "replace",
19
+ logicalName: config.logicalName,
20
+ derivedName: `${config.type} ${config.name}`,
21
+ detail: `type ${stateEntry.recordType} -> ${config.type} (delete + recreate)`,
22
+ changes: [{
23
+ field: "type",
24
+ from: stateEntry.recordType,
25
+ to: config.type,
26
+ kind: "immutable"
27
+ }]
28
+ });
29
+ continue;
30
+ }
31
+ const changes = computeMutableChanges(stateEntry, config, tenant, env);
32
+ if (changes.length === 0) continue;
33
+ items.push({
34
+ kind: "dns_record",
35
+ action: "update",
36
+ logicalName: config.logicalName,
37
+ derivedName: `${config.type} ${config.name}`,
38
+ detail: changes.map((c) => `${c.field}: ${formatVal(c.from)} -> ${formatVal(c.to)}`).join(", "),
39
+ changes
40
+ });
41
+ }
42
+ return items;
43
+ }
44
+ /**
45
+ * Pure comparison shared with `dnsRecordApply` so plan and apply agree
46
+ * on what counts as drift on the mutable record fields. Excludes the
47
+ * `type` change — that's an immutable replace handled separately.
48
+ */
49
+ function computeDnsRecordMutableChanges(state, config, tenant, env) {
50
+ return computeMutableChanges(state, config, tenant, env);
51
+ }
52
+ function computeMutableChanges(state, config, tenant, env) {
53
+ const expectedTtl = effectiveDnsRecordTtl(config);
54
+ const expectedProxied = effectiveDnsRecordProxied(config);
55
+ const expectedComment = effectiveDnsRecordComment(config, tenant, env);
56
+ const changes = [];
57
+ if (state.content !== config.content) changes.push({
58
+ field: "content",
59
+ from: state.content,
60
+ to: config.content,
61
+ kind: "mutable"
62
+ });
63
+ if (state.ttl !== expectedTtl) changes.push({
64
+ field: "ttl",
65
+ from: state.ttl,
66
+ to: expectedTtl,
67
+ kind: "mutable"
68
+ });
69
+ if (state.proxied !== expectedProxied) changes.push({
70
+ field: "proxied",
71
+ from: state.proxied,
72
+ to: expectedProxied,
73
+ kind: "mutable"
74
+ });
75
+ if (config.priority !== void 0 && state.priority !== config.priority) changes.push({
76
+ field: "priority",
77
+ from: state.priority,
78
+ to: config.priority,
79
+ kind: "mutable"
80
+ });
81
+ if (state.comment !== expectedComment) changes.push({
82
+ field: "comment",
83
+ from: state.comment,
84
+ to: expectedComment,
85
+ kind: "mutable"
86
+ });
87
+ return changes;
88
+ }
89
+ function formatVal(v) {
90
+ if (v === void 0) return "(unset)";
91
+ if (typeof v === "string") return v.length > 32 ? `${v.slice(0, 29)}...` : v;
92
+ return String(v);
93
+ }
94
+
95
+ //#endregion
96
+ //#region src/core/apply/applyTarget.ts
97
+ const APPLY_TARGET_KINDS = new Set([
98
+ ...resourceModules.map((m) => m.kind),
99
+ "dispatch_namespace",
100
+ "dns_record",
101
+ "logpush_job"
102
+ ]);
103
+ /**
104
+ * Parse `tamer apply --target` / `tamer plan --target` (`d1:settings`,
105
+ * `dispatch_namespace:workspace`, …). Not valid for `worker_route` /
106
+ * `worker_script` (routes deploy with `tamer deploy`; scripts with wrangler).
107
+ */
108
+ function parseApplyTarget(raw) {
109
+ const idx = raw.indexOf(":");
110
+ if (idx < 1 || idx === raw.length - 1) throw new Error(`Invalid --target "${raw}": expected <kind>:<logicalName> (e.g. d1:settings, dns_record:apex_txt)`);
111
+ const kind = raw.slice(0, idx);
112
+ const logical = raw.slice(idx + 1);
113
+ if (!logical.trim()) throw new Error(`Invalid --target "${raw}": missing logical name after ":"`);
114
+ if (kind === "worker_route" || kind === "worker_script") throw new Error(`--target ${kind}:… is not supported here (worker routes are applied on \`tamer deploy\`; scripts via wrangler). Omit --target or pass a resource kind.`);
115
+ if (!APPLY_TARGET_KINDS.has(kind)) {
116
+ const list = [...APPLY_TARGET_KINDS].sort().join(", ");
117
+ throw new Error(`Unknown resource kind in --target: "${kind}". Expected one of: ${list}`);
118
+ }
119
+ return {
120
+ kind,
121
+ logical
122
+ };
123
+ }
124
+ async function assertApplyTargetDeclared(config, baseDir, target) {
125
+ if (target.kind === "dispatch_namespace") {
126
+ const names = getDispatchNamespaces(config).map((n) => n.logicalName);
127
+ if (!names.includes(target.logical)) throw new Error(`No dispatch_namespace "${target.logical}" in the Tamer project config. Declared: ${names.length ? names.join(", ") : "(none)"}`);
128
+ return;
129
+ }
130
+ if (target.kind === "dns_record") {
131
+ const names = getDnsRecords(config).map((d) => d.logicalName);
132
+ if (!names.includes(target.logical)) throw new Error(`No dns_record "${target.logical}" in the Tamer project config. Declared: ${names.length ? names.join(", ") : "(none)"}`);
133
+ return;
134
+ }
135
+ if (target.kind === "logpush_job") {
136
+ const names = getLogpushJobs(config).map((j) => j.logicalName);
137
+ if (!names.includes(target.logical)) throw new Error(`No logpush_job "${target.logical}" in the Tamer project config. Declared: ${names.length ? names.join(", ") : "(none)"}`);
138
+ return;
139
+ }
140
+ const set = await logicalNamesForResourceKind(config, baseDir, target.kind);
141
+ if (!set.has(target.logical)) {
142
+ const arr = [...set].sort();
143
+ throw new Error(`No ${target.kind} resource "${target.logical}" in the Tamer project config for this stack. Declared ${target.kind}: ${arr.length ? arr.join(", ") : "(none)"}`);
144
+ }
145
+ }
146
+ function filterPlanItemsForTarget(items, target) {
147
+ return items.filter((it) => it.kind === target.kind && it.logicalName === target.logical);
148
+ }
149
+
150
+ //#endregion
151
+ export { dnsRecordDiffPlanItems as a, computeDnsRecordMutableChanges as i, filterPlanItemsForTarget as n, parseApplyTarget as r, assertApplyTargetDeclared as t };
152
+ //# sourceMappingURL=applyTarget-BetDYdeS.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applyTarget-BetDYdeS.mjs","names":["items: PlanItem[]","changes: PlanFieldChange[]","APPLY_TARGET_KINDS: ReadonlySet<string>"],"sources":["../src/features/dns-records/dns-records.diff.ts","../src/core/apply/applyTarget.ts"],"sourcesContent":["/**\n * Plan-time field-level diff for declared DNS records. Pure: takes the\n * already-recorded {@link DnsRecordStateEntry} and the {@link\n * DnsRecordResourceConfig} the user wrote, and returns the\n * `update` / `replace` items `tamer plan` should emit.\n *\n * Mirrors the patch + delete-and-recreate decision tree in\n * {@link dnsRecordApply}:\n * - `type` change → `replace` (Cloudflare's API rejects PATCH on type per\n * https://developers.cloudflare.com/fundamentals/api/reference/deprecations/).\n * - any of `content` / `ttl` / `proxied` / `priority` / `comment` differ →\n * `update` (in-place PATCH).\n *\n * Records that don't yet have a state row are reported by `dnsRecordDrift`\n * as `undeployed` instead — those become `create` items. Records that\n * `apply` would skip (env-skipped, missing zone) are excluded here.\n */\n\nimport type {\n DnsRecordResourceConfig,\n DnsRecordStateEntry,\n TenantMeta,\n} from \"../../types.js\";\nimport type { StateManager } from \"../../core/state/StateManager.js\";\nimport type { PlanFieldChange, PlanItem } from \"../../core/plan/plan.types.js\";\nimport {\n dnsRecordAppliesToEnv,\n dnsRecordStateKey,\n effectiveDnsRecordComment,\n effectiveDnsRecordProxied,\n effectiveDnsRecordTtl,\n} from \"./dns-records.resolve.js\";\n\nexport function dnsRecordDiffPlanItems(args: {\n resources: DnsRecordResourceConfig[];\n tenant: TenantMeta;\n env: string;\n state: StateManager;\n}): PlanItem[] {\n const { resources, tenant, env, state } = args;\n const items: PlanItem[] = [];\n\n for (const config of resources) {\n if (!dnsRecordAppliesToEnv(config, env)) continue;\n const key = dnsRecordStateKey(config.zoneId, config.type, config.name);\n const entry = state.get(key);\n if (!entry || entry.type !== \"dns_record\") continue;\n\n const stateEntry = entry as DnsRecordStateEntry;\n if (stateEntry.recordType !== config.type) {\n items.push({\n kind: \"dns_record\",\n action: \"replace\",\n logicalName: config.logicalName,\n derivedName: `${config.type} ${config.name}`,\n detail: `type ${stateEntry.recordType} -> ${config.type} (delete + recreate)`,\n changes: [\n {\n field: \"type\",\n from: stateEntry.recordType,\n to: config.type,\n kind: \"immutable\",\n },\n ],\n });\n continue;\n }\n\n const changes = computeMutableChanges(stateEntry, config, tenant, env);\n if (changes.length === 0) continue;\n items.push({\n kind: \"dns_record\",\n action: \"update\",\n logicalName: config.logicalName,\n derivedName: `${config.type} ${config.name}`,\n detail: changes\n .map((c) => `${c.field}: ${formatVal(c.from)} -> ${formatVal(c.to)}`)\n .join(\", \"),\n changes,\n });\n }\n\n return items;\n}\n\n/**\n * Pure comparison shared with `dnsRecordApply` so plan and apply agree\n * on what counts as drift on the mutable record fields. Excludes the\n * `type` change — that's an immutable replace handled separately.\n */\nexport function computeDnsRecordMutableChanges(\n state: DnsRecordStateEntry,\n config: DnsRecordResourceConfig,\n tenant: TenantMeta,\n env: string,\n): PlanFieldChange[] {\n return computeMutableChanges(state, config, tenant, env);\n}\n\nfunction computeMutableChanges(\n state: DnsRecordStateEntry,\n config: DnsRecordResourceConfig,\n tenant: TenantMeta,\n env: string,\n): PlanFieldChange[] {\n const expectedTtl = effectiveDnsRecordTtl(config);\n const expectedProxied = effectiveDnsRecordProxied(config);\n const expectedComment = effectiveDnsRecordComment(\n config,\n tenant,\n env,\n );\n\n const changes: PlanFieldChange[] = [];\n if (state.content !== config.content) {\n changes.push({\n field: \"content\",\n from: state.content,\n to: config.content,\n kind: \"mutable\",\n });\n }\n if (state.ttl !== expectedTtl) {\n changes.push({\n field: \"ttl\",\n from: state.ttl,\n to: expectedTtl,\n kind: \"mutable\",\n });\n }\n if (state.proxied !== expectedProxied) {\n changes.push({\n field: \"proxied\",\n from: state.proxied,\n to: expectedProxied,\n kind: \"mutable\",\n });\n }\n if (\n config.priority !== undefined &&\n state.priority !== config.priority\n ) {\n changes.push({\n field: \"priority\",\n from: state.priority,\n to: config.priority,\n kind: \"mutable\",\n });\n }\n if (state.comment !== expectedComment) {\n changes.push({\n field: \"comment\",\n from: state.comment,\n to: expectedComment,\n kind: \"mutable\",\n });\n }\n return changes;\n}\n\nfunction formatVal(v: unknown): string {\n if (v === undefined) return \"(unset)\";\n if (typeof v === \"string\") return v.length > 32 ? `${v.slice(0, 29)}...` : v;\n return String(v);\n}\n","import type { CfiConfig } from \"../../types.js\";\nimport {\n getDispatchNamespaces,\n getDnsRecords,\n getLogpushJobs,\n} from \"../../types.js\";\nimport { logicalNamesForResourceKind } from \"../config/resourcesFromConfig.js\";\nimport type { ResourceKind } from \"../registry/registry.js\";\nimport { resourceModules } from \"../registry/registry.js\";\nimport type { PlanItem } from \"../plan/plan.types.js\";\n\nconst APPLY_TARGET_KINDS: ReadonlySet<string> = new Set([\n ...resourceModules.map((m) => m.kind),\n \"dispatch_namespace\",\n \"dns_record\",\n \"logpush_job\",\n]);\n\nexport type ApplyTargetKind =\n | ResourceKind\n | \"dispatch_namespace\"\n | \"dns_record\"\n | \"logpush_job\";\n\nexport interface ApplyTarget {\n kind: ApplyTargetKind;\n logical: string;\n}\n\n/**\n * Parse `tamer apply --target` / `tamer plan --target` (`d1:settings`,\n * `dispatch_namespace:workspace`, …). Not valid for `worker_route` /\n * `worker_script` (routes deploy with `tamer deploy`; scripts with wrangler).\n */\nexport function parseApplyTarget(raw: string): ApplyTarget {\n const idx = raw.indexOf(\":\");\n if (idx < 1 || idx === raw.length - 1) {\n throw new Error(\n `Invalid --target \"${raw}\": expected <kind>:<logicalName> (e.g. d1:settings, dns_record:apex_txt)`,\n );\n }\n const kind = raw.slice(0, idx);\n const logical = raw.slice(idx + 1);\n if (!logical.trim()) {\n throw new Error(\n `Invalid --target \"${raw}\": missing logical name after \":\"`,\n );\n }\n if (kind === \"worker_route\" || kind === \"worker_script\") {\n throw new Error(\n `--target ${kind}:… is not supported here (worker routes are applied on \\`tamer deploy\\`; scripts via wrangler). Omit --target or pass a resource kind.`,\n );\n }\n if (!APPLY_TARGET_KINDS.has(kind)) {\n const list = [...APPLY_TARGET_KINDS].sort().join(\", \");\n throw new Error(\n `Unknown resource kind in --target: \"${kind}\". Expected one of: ${list}`,\n );\n }\n return { kind: kind as ApplyTargetKind, logical };\n}\n\nexport async function assertApplyTargetDeclared(\n config: CfiConfig,\n baseDir: string,\n target: ApplyTarget,\n): Promise<void> {\n if (target.kind === \"dispatch_namespace\") {\n const names = getDispatchNamespaces(config).map((n) => n.logicalName);\n if (!names.includes(target.logical)) {\n throw new Error(\n `No dispatch_namespace \"${target.logical}\" in the Tamer project config. Declared: ${names.length ? names.join(\", \") : \"(none)\"}`,\n );\n }\n return;\n }\n if (target.kind === \"dns_record\") {\n const names = getDnsRecords(config).map((d) => d.logicalName);\n if (!names.includes(target.logical)) {\n throw new Error(\n `No dns_record \"${target.logical}\" in the Tamer project config. Declared: ${names.length ? names.join(\", \") : \"(none)\"}`,\n );\n }\n return;\n }\n if (target.kind === \"logpush_job\") {\n const names = getLogpushJobs(config).map((j) => j.logicalName);\n if (!names.includes(target.logical)) {\n throw new Error(\n `No logpush_job \"${target.logical}\" in the Tamer project config. Declared: ${names.length ? names.join(\", \") : \"(none)\"}`,\n );\n }\n return;\n }\n const set = await logicalNamesForResourceKind(config, baseDir, target.kind);\n if (!set.has(target.logical)) {\n const arr = [...set].sort();\n throw new Error(\n `No ${target.kind} resource \"${target.logical}\" in the Tamer project config for this stack. Declared ${target.kind}: ${arr.length ? arr.join(\", \") : \"(none)\"}`,\n );\n }\n}\n\nexport function filterPlanItemsForTarget(\n items: PlanItem[],\n target: ApplyTarget,\n): PlanItem[] {\n return items.filter(\n (it) => it.kind === target.kind && it.logicalName === target.logical,\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import { n as loadConfig } from "./loader-DP7yXqT6.mjs";
2
+ import { n as cloudflareAccountIdFromEnv, t as CFApiClient } from "./CFApiClient-DhbyyV71.mjs";
3
+ import { d as tamerStateDatabaseName, f as stackNameForConfig, t as StateManager, u as ensureTamerStateDatabase } from "./StateManager-DTqtLLVX.mjs";
4
+ import "./r2S3EmptyBucket-DD81ZWQ7.mjs";
5
+ import { n as ensureTamerArtifactsBucket } from "./tamerArtifactsR2-Ccgplu2Q.mjs";
6
+
7
+ //#region src/cli/commands/bootstrap.ts
8
+ async function runBootstrap(options) {
9
+ const { env, configPath } = options;
10
+ if (env === "local") {
11
+ console.log("bootstrap: env=local uses in-memory state only; no D1/R2 metadata is created.");
12
+ return;
13
+ }
14
+ const config = await loadConfig(configPath, { env });
15
+ const accountId = config.account_id ?? cloudflareAccountIdFromEnv();
16
+ if (!accountId) throw new Error("account_id required in config or CLOUDFLARE_ACCOUNT_ID env var");
17
+ const api = new CFApiClient(accountId);
18
+ const stackName = stackNameForConfig(config);
19
+ const uuid = await ensureTamerStateDatabase(api, config.tenant.id, env, stackName);
20
+ console.log(`Tamer state ready: D1 uuid=${uuid} name=${tamerStateDatabaseName(env)} (stack=${stackName})`);
21
+ const bucketName = await ensureTamerArtifactsBucket(api, env);
22
+ console.log(`Tamer artifacts ready: R2 bucket=${bucketName}`);
23
+ const state = new StateManager(config.tenant.id, env, stackName);
24
+ await state.hydrate(api);
25
+ state.beginOperation("bootstrap");
26
+ if (!state.getStackMeta()?.name) state.setStackMeta({ name: stackName });
27
+ state.finishOperation("ready");
28
+ await state.persist(api);
29
+ }
30
+
31
+ //#endregion
32
+ export { runBootstrap };
33
+ //# sourceMappingURL=bootstrap-CBzPilB1.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap-CBzPilB1.mjs","names":[],"sources":["../src/cli/commands/bootstrap.ts"],"sourcesContent":["import { loadConfig } from \"../../core/config/loader.js\";\nimport { cloudflareAccountIdFromEnv } from \"../../core/cloudflareEnv.js\";\nimport { CFApiClient } from \"../../core/api/CFApiClient.js\";\nimport {\n ensureTamerStateDatabase,\n tamerStateDatabaseName,\n} from \"../../core/state/tamerStateDb.js\";\nimport {\n ensureTamerArtifactsBucket,\n} from \"../../core/state/tamerArtifactsR2.js\";\nimport { StateManager } from \"../../core/state/StateManager.js\";\nimport { stackNameForConfig } from \"../../core/state/stackName.js\";\n\nexport async function runBootstrap(options: {\n env: string;\n configPath?: string;\n}): Promise<void> {\n const { env, configPath } = options;\n if (env === \"local\") {\n console.log(\n \"bootstrap: env=local uses in-memory state only; no D1/R2 metadata is created.\",\n );\n return;\n }\n\n const config = await loadConfig(configPath, { env });\n const accountId =\n config.account_id ?? cloudflareAccountIdFromEnv();\n if (!accountId) {\n throw new Error(\n \"account_id required in config or CLOUDFLARE_ACCOUNT_ID env var\",\n );\n }\n\n const api = new CFApiClient(accountId);\n const stackName = stackNameForConfig(config);\n const uuid = await ensureTamerStateDatabase(\n api,\n config.tenant.id,\n env,\n stackName,\n );\n console.log(\n `Tamer state ready: D1 uuid=${uuid} name=${tamerStateDatabaseName(env)} (stack=${stackName})`,\n );\n\n const bucketName = await ensureTamerArtifactsBucket(api, env);\n console.log(`Tamer artifacts ready: R2 bucket=${bucketName}`);\n\n const state = new StateManager(config.tenant.id, env, stackName);\n await state.hydrate(api);\n state.beginOperation(\"bootstrap\");\n if (!state.getStackMeta()?.name) {\n state.setStackMeta({ name: stackName });\n }\n state.finishOperation(\"ready\");\n await state.persist(api);\n}\n"],"mappings":";;;;;;;AAaA,eAAsB,aAAa,SAGjB;CAChB,MAAM,EAAE,KAAK,eAAe;AAC5B,KAAI,QAAQ,SAAS;AACnB,UAAQ,IACN,gFACD;AACD;;CAGF,MAAM,SAAS,MAAM,WAAW,YAAY,EAAE,KAAK,CAAC;CACpD,MAAM,YACJ,OAAO,cAAc,4BAA4B;AACnD,KAAI,CAAC,UACH,OAAM,IAAI,MACR,iEACD;CAGH,MAAM,MAAM,IAAI,YAAY,UAAU;CACtC,MAAM,YAAY,mBAAmB,OAAO;CAC5C,MAAM,OAAO,MAAM,yBACjB,KACA,OAAO,OAAO,IACd,KACA,UACD;AACD,SAAQ,IACN,8BAA8B,KAAK,QAAQ,uBAAuB,IAAI,CAAC,UAAU,UAAU,GAC5F;CAED,MAAM,aAAa,MAAM,2BAA2B,KAAK,IAAI;AAC7D,SAAQ,IAAI,oCAAoC,aAAa;CAE7D,MAAM,QAAQ,IAAI,aAAa,OAAO,OAAO,IAAI,KAAK,UAAU;AAChE,OAAM,MAAM,QAAQ,IAAI;AACxB,OAAM,eAAe,YAAY;AACjC,KAAI,CAAC,MAAM,cAAc,EAAE,KACzB,OAAM,aAAa,EAAE,MAAM,WAAW,CAAC;AAEzC,OAAM,gBAAgB,QAAQ;AAC9B,OAAM,MAAM,QAAQ,IAAI"}
@@ -0,0 +1,38 @@
1
+ import { basename } from "path";
2
+ import { readFileSync } from "fs";
3
+
4
+ //#region src/core/wfp/buildDispatchUploadForm.ts
5
+ /**
6
+ * Build multipart body for PUT .../dispatch/namespaces/{ns}/scripts/{script}
7
+ * with a single Worker module file.
8
+ */
9
+ function buildSingleModuleDispatchForm(mainFilePath, opts) {
10
+ const moduleName = basename(mainFilePath);
11
+ const body = readFileSync(mainFilePath);
12
+ const metadata = {
13
+ main_module: moduleName,
14
+ compatibility_date: opts.compatibility_date
15
+ };
16
+ if (opts.compatibility_flags?.length) metadata.compatibility_flags = opts.compatibility_flags;
17
+ const form = new FormData();
18
+ form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
19
+ form.append(moduleName, new Blob([body], { type: "application/javascript+module" }), moduleName);
20
+ return form;
21
+ }
22
+ /** Same as {@link buildSingleModuleDispatchForm} but from in-memory bytes (e.g. R2 download). */
23
+ function buildSingleModuleDispatchFormFromBuffer(moduleName, body, opts) {
24
+ const bytes = body instanceof ArrayBuffer ? new Uint8Array(body) : body;
25
+ const metadata = {
26
+ main_module: moduleName,
27
+ compatibility_date: opts.compatibility_date
28
+ };
29
+ if (opts.compatibility_flags?.length) metadata.compatibility_flags = opts.compatibility_flags;
30
+ const form = new FormData();
31
+ form.append("metadata", new Blob([JSON.stringify(metadata)], { type: "application/json" }));
32
+ form.append(moduleName, new Blob([bytes], { type: "application/javascript+module" }), moduleName);
33
+ return form;
34
+ }
35
+
36
+ //#endregion
37
+ export { buildSingleModuleDispatchFormFromBuffer as n, buildSingleModuleDispatchForm as t };
38
+ //# sourceMappingURL=buildDispatchUploadForm-BoUB93b3.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildDispatchUploadForm-BoUB93b3.mjs","names":["metadata: Record<string, unknown>"],"sources":["../src/core/wfp/buildDispatchUploadForm.ts"],"sourcesContent":["import { basename } from \"path\";\nimport { readFileSync } from \"fs\";\n\n/**\n * Build multipart body for PUT .../dispatch/namespaces/{ns}/scripts/{script}\n * with a single Worker module file.\n */\nexport function buildSingleModuleDispatchForm(\n mainFilePath: string,\n opts: {\n compatibility_date: string;\n compatibility_flags?: string[];\n },\n): FormData {\n const moduleName = basename(mainFilePath);\n const body = readFileSync(mainFilePath);\n const metadata: Record<string, unknown> = {\n main_module: moduleName,\n compatibility_date: opts.compatibility_date,\n };\n if (opts.compatibility_flags?.length) {\n metadata.compatibility_flags = opts.compatibility_flags;\n }\n const form = new FormData();\n form.append(\n \"metadata\",\n new Blob([JSON.stringify(metadata)], { type: \"application/json\" }),\n );\n // CRITICAL: Cloudflare reads `Content-Type: application/javascript+module`\n // off the module part to decide whether to treat it as ESM. Without this it\n // errors with `Main module must be an ES module.` even though metadata uses\n // `main_module`.\n form.append(\n moduleName,\n new Blob([body], { type: \"application/javascript+module\" }),\n moduleName,\n );\n return form;\n}\n\n/** Same as {@link buildSingleModuleDispatchForm} but from in-memory bytes (e.g. R2 download). */\nexport function buildSingleModuleDispatchFormFromBuffer(\n moduleName: string,\n body: Uint8Array | ArrayBuffer,\n opts: {\n compatibility_date: string;\n compatibility_flags?: string[];\n },\n): FormData {\n const bytes = body instanceof ArrayBuffer ? new Uint8Array(body) : body;\n const metadata: Record<string, unknown> = {\n main_module: moduleName,\n compatibility_date: opts.compatibility_date,\n };\n if (opts.compatibility_flags?.length) {\n metadata.compatibility_flags = opts.compatibility_flags;\n }\n const form = new FormData();\n form.append(\n \"metadata\",\n new Blob([JSON.stringify(metadata)], { type: \"application/json\" }),\n );\n form.append(\n moduleName,\n new Blob([bytes], { type: \"application/javascript+module\" }),\n moduleName,\n );\n return form;\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,8BACd,cACA,MAIU;CACV,MAAM,aAAa,SAAS,aAAa;CACzC,MAAM,OAAO,aAAa,aAAa;CACvC,MAAMA,WAAoC;EACxC,aAAa;EACb,oBAAoB,KAAK;EAC1B;AACD,KAAI,KAAK,qBAAqB,OAC5B,UAAS,sBAAsB,KAAK;CAEtC,MAAM,OAAO,IAAI,UAAU;AAC3B,MAAK,OACH,YACA,IAAI,KAAK,CAAC,KAAK,UAAU,SAAS,CAAC,EAAE,EAAE,MAAM,oBAAoB,CAAC,CACnE;AAKD,MAAK,OACH,YACA,IAAI,KAAK,CAAC,KAAK,EAAE,EAAE,MAAM,iCAAiC,CAAC,EAC3D,WACD;AACD,QAAO;;;AAIT,SAAgB,wCACd,YACA,MACA,MAIU;CACV,MAAM,QAAQ,gBAAgB,cAAc,IAAI,WAAW,KAAK,GAAG;CACnE,MAAMA,WAAoC;EACxC,aAAa;EACb,oBAAoB,KAAK;EAC1B;AACD,KAAI,KAAK,qBAAqB,OAC5B,UAAS,sBAAsB,KAAK;CAEtC,MAAM,OAAO,IAAI,UAAU;AAC3B,MAAK,OACH,YACA,IAAI,KAAK,CAAC,KAAK,UAAU,SAAS,CAAC,EAAE,EAAE,MAAM,oBAAoB,CAAC,CACnE;AACD,MAAK,OACH,YACA,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,MAAM,iCAAiC,CAAC,EAC5D,WACD;AACD,QAAO"}
@@ -0,0 +1,163 @@
1
+ import { f as getDispatchNamespaces, m as getLogpushJobs, p as getDnsRecords } from "./normalize-Bx0bpFop.mjs";
2
+ import { t as getWorkers } from "./loader-DP7yXqT6.mjs";
3
+ import { n as cloudflareAccountIdFromEnv } from "./CFApiClient-DhbyyV71.mjs";
4
+ import { _ as namingFromConfig, a as resourceModules, t as fetchStackImports, u as mergeWorkerConfigForResourcePick } from "./fetchStackImports-B4ZJahOt.mjs";
5
+ import { f as stackNameForConfig, t as StateManager } from "./StateManager-DTqtLLVX.mjs";
6
+ import { n as dispatchNamespaceSync, t as dnsRecordSync } from "./dns-records.sync-Bpzz9H0s.mjs";
7
+ import { i as logpushJobSync } from "./logpush-job-xS7270FZ.mjs";
8
+ import { readFileSync, writeFileSync } from "fs";
9
+ import { createHash } from "crypto";
10
+
11
+ //#region src/core/plan/planFile.ts
12
+ /** Stable on-disk format identifier for `tamer plan --out`. */
13
+ const PLAN_FILE_FORMAT = "tamer-plan/v1";
14
+ /**
15
+ * Stable JSON stringify: object keys are sorted recursively. The hash must
16
+ * be deterministic across machines, so insertion order can't matter.
17
+ */
18
+ function canonicalJson(value) {
19
+ return stableStringify(value);
20
+ }
21
+ function stableStringify(value) {
22
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
23
+ if (Array.isArray(value)) return `[${value.map((v) => stableStringify(v)).join(",")}]`;
24
+ const obj = value;
25
+ return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
26
+ }
27
+ function sha256Hex(input) {
28
+ return `sha256:${createHash("sha256").update(input).digest("hex")}`;
29
+ }
30
+ /**
31
+ * Compute the attestation hashes for `(config, state)` exactly as the
32
+ * apply-side will recompute them. Keep both sides in sync — the canonical
33
+ * JSON algorithm above is the contract.
34
+ */
35
+ function computeAttestation(config, state, cloudflareSnapshot) {
36
+ const att = {
37
+ configHash: sha256Hex(canonicalJson(config)),
38
+ stateHash: sha256Hex(canonicalJson(stateForHash(state))),
39
+ stateRevision: state.revision
40
+ };
41
+ if (cloudflareSnapshot) att.cloudflareHash = sha256Hex(canonicalJson(cloudflareSnapshot));
42
+ return att;
43
+ }
44
+ /**
45
+ * Stable hash of a Cloudflare snapshot; same algorithm as
46
+ * `attestation.cloudflareHash` so apply can recompute and compare.
47
+ */
48
+ function hashCloudflareSnapshot(snapshot) {
49
+ return sha256Hex(canonicalJson(snapshot));
50
+ }
51
+ /**
52
+ * State view used for hashing — strips fields that legitimately drift
53
+ * between consecutive reads (timestamps, revision counter) so the hash
54
+ * only changes when *resources* / *tenants* / *stack* change.
55
+ */
56
+ function stateForHash(state) {
57
+ const { resources, tenants, stack } = state;
58
+ return {
59
+ resources: resources ?? {},
60
+ tenants: tenants ?? {},
61
+ stack
62
+ };
63
+ }
64
+ function writePlanFile(path, file) {
65
+ writeFileSync(path, JSON.stringify(file, null, 2), "utf-8");
66
+ }
67
+ function readPlanFile(path) {
68
+ let raw;
69
+ try {
70
+ raw = readFileSync(path, "utf-8");
71
+ } catch (err) {
72
+ const msg = err instanceof Error ? err.message : String(err);
73
+ throw new Error(`--plan: cannot read plan file "${path}": ${msg}`);
74
+ }
75
+ let parsed;
76
+ try {
77
+ parsed = JSON.parse(raw);
78
+ } catch (err) {
79
+ const msg = err instanceof Error ? err.message : String(err);
80
+ throw new Error(`--plan: plan file "${path}" is not valid JSON: ${msg}`);
81
+ }
82
+ if (!isPlanFile(parsed)) throw new Error(`--plan: plan file "${path}" is missing required fields or has wrong format (expected ${PLAN_FILE_FORMAT})`);
83
+ if (parsed.format !== PLAN_FILE_FORMAT) throw new Error(`--plan: plan file format "${parsed.format}" is not supported by this Tamer build (expected ${PLAN_FILE_FORMAT})`);
84
+ return parsed;
85
+ }
86
+ function isPlanFile(v) {
87
+ if (!v || typeof v !== "object") return false;
88
+ const o = v;
89
+ return typeof o.format === "string" && typeof o.tenantId === "string" && typeof o.env === "string" && typeof o.generatedAt === "string" && !!o.report && typeof o.report === "object" && !!o.attestation && typeof o.attestation === "object";
90
+ }
91
+
92
+ //#endregion
93
+ //#region src/core/plan/cloudflareSnapshot.ts
94
+ /**
95
+ * Drift-aware Cloudflare snapshot used by `tamer plan --out` and
96
+ * `tamer apply --plan`.
97
+ *
98
+ * Drives the registry's `sync` hook for every kind against a **fresh
99
+ * in-memory state** so the result reflects only what currently exists on
100
+ * Cloudflare (no carryover from D1). The shape mirrors the post-sync
101
+ * resource map but strips volatile fields (timestamps) so consecutive
102
+ * snapshots of an unchanged account hash to the same value.
103
+ *
104
+ * Apply recomputes this against live Cloudflare and refuses to proceed if
105
+ * `cloudflareHash` differs from the plan's pinned hash — that's how a
106
+ * stale plan against drifted infra is detected.
107
+ */
108
+ async function buildCloudflareSnapshot(args) {
109
+ const { config, env, api, baseDir } = args;
110
+ const naming = namingFromConfig(config);
111
+ const state = new StateManager(config.tenant.id, env, stackNameForConfig(config));
112
+ state.hydrateInMemory();
113
+ const accountId = config.account_id ?? cloudflareAccountIdFromEnv() ?? "";
114
+ const imports = env !== "local" ? await fetchStackImports(api, config, env).catch(() => ({})) : {};
115
+ const workers = await getWorkers(config, baseDir);
116
+ for (const mod of resourceModules) {
117
+ const all = await mod.fetchAll(api);
118
+ for (const [workerKey, wc] of workers) {
119
+ const mergedWorker = env !== "local" && accountId ? mergeWorkerConfigForResourcePick(config, workerKey, wc, env, accountId, naming, state, {
120
+ referencesMode: "tolerant",
121
+ imports
122
+ }) : wc;
123
+ const resources = mod.pickResources(mergedWorker);
124
+ if (resources.length === 0) continue;
125
+ mod.sync({
126
+ all,
127
+ resources,
128
+ tenant: config.tenant,
129
+ env,
130
+ api,
131
+ state,
132
+ naming,
133
+ config,
134
+ baseDir
135
+ });
136
+ }
137
+ }
138
+ if (getDispatchNamespaces(config).length > 0 && env !== "local") await dispatchNamespaceSync(getDispatchNamespaces(config), config.tenant, env, api, state);
139
+ if (getDnsRecords(config).length > 0 && env !== "local") await dnsRecordSync(getDnsRecords(config), config.tenant, env, api, state);
140
+ if (getLogpushJobs(config).length > 0 && env !== "local") await logpushJobSync(getLogpushJobs(config), config.tenant, env, api, state);
141
+ const entries = state.getAll();
142
+ const stable = { entries: {} };
143
+ for (const key of Object.keys(entries).sort()) stable.entries[key] = stripVolatile(entries[key]);
144
+ return stable;
145
+ }
146
+ /**
147
+ * Per-entry projection used in {@link CloudflareSnapshot}. Drops fields
148
+ * that legitimately move between two reads of the same Cloudflare resource
149
+ * (timestamps); keeps anything that, if it changed, would mean the plan is
150
+ * no longer accurate (cfId, derivedName, kind-specific identity bits).
151
+ */
152
+ function stripVolatile(entry) {
153
+ const out = {};
154
+ for (const [k, v] of Object.entries(entry)) {
155
+ if (k === "createdAt" || k === "updatedAt") continue;
156
+ out[k] = v;
157
+ }
158
+ return out;
159
+ }
160
+
161
+ //#endregion
162
+ export { readPlanFile as a, hashCloudflareSnapshot as i, PLAN_FILE_FORMAT as n, writePlanFile as o, computeAttestation as r, buildCloudflareSnapshot as t };
163
+ //# sourceMappingURL=cloudflareSnapshot-B4FOaNr0.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflareSnapshot-B4FOaNr0.mjs","names":["att: PlanAttestation","raw: string","parsed: unknown","stable: CloudflareSnapshot","out: Record<string, unknown>"],"sources":["../src/core/plan/planFile.ts","../src/core/plan/cloudflareSnapshot.ts"],"sourcesContent":["import { createHash } from \"crypto\";\nimport { readFileSync, writeFileSync } from \"fs\";\nimport type { CfiConfig, CfiState } from \"../../types.js\";\nimport type { PlanReport } from \"./plan.types.js\";\nimport type { CloudflareSnapshot } from \"./cloudflareSnapshot.js\";\n\n/** Stable on-disk format identifier for `tamer plan --out`. */\nexport const PLAN_FILE_FORMAT = \"tamer-plan/v1\" as const;\n\n/**\n * Serialized `tamer plan` output, suitable for `tamer apply --plan`.\n *\n * The {@link attestation} hashes pin the `(config, state)` pair the plan\n * was generated against. Apply recomputes the same hashes at run time and\n * refuses to proceed if either drifted, so a stale plan can never silently\n * apply against a different reality. Use `--allow-stale` only when you\n * understand the risk.\n */\nexport interface PlanFile {\n format: typeof PLAN_FILE_FORMAT;\n tenantId: string;\n env: string;\n generatedAt: string;\n /** The plan as it would be printed by `tamer plan` (kept for review). */\n report: PlanReport;\n attestation: PlanAttestation;\n}\n\nexport interface PlanAttestation {\n /** SHA-256 over the canonical JSON of the parsed `tamer.config.ts`. */\n configHash: string;\n /** SHA-256 over the canonical JSON of `state.load()` at plan time. */\n stateHash: string;\n /** Revision at plan time (informational; primary check is stateHash). */\n stateRevision?: number;\n /**\n * Drift-aware refresh: SHA-256 over the normalized **Cloudflare-side**\n * snapshot at plan time (per-kind `sync()` against a fresh in-memory\n * state, with timestamps stripped). Apply re-fetches and recomputes; a\n * mismatch means infrastructure drifted out-of-band between plan and\n * apply, so the plan is no longer accurate. Omitted for `env: local`.\n */\n cloudflareHash?: string;\n}\n\n/**\n * Stable JSON stringify: object keys are sorted recursively. The hash must\n * be deterministic across machines, so insertion order can't matter.\n */\nexport function canonicalJson(value: unknown): string {\n return stableStringify(value);\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value);\n }\n if (Array.isArray(value)) {\n return `[${value.map((v) => stableStringify(v)).join(\",\")}]`;\n }\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n const body = keys\n .map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)\n .join(\",\");\n return `{${body}}`;\n}\n\nexport function sha256Hex(input: string): string {\n return `sha256:${createHash(\"sha256\").update(input).digest(\"hex\")}`;\n}\n\n/**\n * Compute the attestation hashes for `(config, state)` exactly as the\n * apply-side will recompute them. Keep both sides in sync — the canonical\n * JSON algorithm above is the contract.\n */\nexport function computeAttestation(\n config: CfiConfig,\n state: CfiState,\n cloudflareSnapshot?: CloudflareSnapshot,\n): PlanAttestation {\n const att: PlanAttestation = {\n configHash: sha256Hex(canonicalJson(config)),\n stateHash: sha256Hex(canonicalJson(stateForHash(state))),\n stateRevision: state.revision,\n };\n if (cloudflareSnapshot) {\n att.cloudflareHash = sha256Hex(canonicalJson(cloudflareSnapshot));\n }\n return att;\n}\n\n/**\n * Stable hash of a Cloudflare snapshot; same algorithm as\n * `attestation.cloudflareHash` so apply can recompute and compare.\n */\nexport function hashCloudflareSnapshot(\n snapshot: CloudflareSnapshot,\n): string {\n return sha256Hex(canonicalJson(snapshot));\n}\n\n/**\n * State view used for hashing — strips fields that legitimately drift\n * between consecutive reads (timestamps, revision counter) so the hash\n * only changes when *resources* / *tenants* / *stack* change.\n */\nfunction stateForHash(state: CfiState): unknown {\n const { resources, tenants, stack } = state;\n return { resources: resources ?? {}, tenants: tenants ?? {}, stack };\n}\n\nexport function writePlanFile(path: string, file: PlanFile): void {\n writeFileSync(path, JSON.stringify(file, null, 2), \"utf-8\");\n}\n\nexport function readPlanFile(path: string): PlanFile {\n let raw: string;\n try {\n raw = readFileSync(path, \"utf-8\");\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`--plan: cannot read plan file \"${path}\": ${msg}`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`--plan: plan file \"${path}\" is not valid JSON: ${msg}`);\n }\n if (!isPlanFile(parsed)) {\n throw new Error(\n `--plan: plan file \"${path}\" is missing required fields or has wrong format ` +\n `(expected ${PLAN_FILE_FORMAT})`,\n );\n }\n if (parsed.format !== PLAN_FILE_FORMAT) {\n throw new Error(\n `--plan: plan file format \"${parsed.format}\" is not supported by this Tamer build (expected ${PLAN_FILE_FORMAT})`,\n );\n }\n return parsed;\n}\n\nfunction isPlanFile(v: unknown): v is PlanFile {\n if (!v || typeof v !== \"object\") return false;\n const o = v as Record<string, unknown>;\n return (\n typeof o.format === \"string\" &&\n typeof o.tenantId === \"string\" &&\n typeof o.env === \"string\" &&\n typeof o.generatedAt === \"string\" &&\n !!o.report &&\n typeof o.report === \"object\" &&\n !!o.attestation &&\n typeof o.attestation === \"object\"\n );\n}\n","import type { CfiConfig, StateEntry } from \"../../types.js\";\nimport type { CFApiClient } from \"../api/CFApiClient.js\";\nimport { StateManager } from \"../state/StateManager.js\";\nimport { stackNameForConfig } from \"../state/stackName.js\";\nimport { resourceModules } from \"../registry/registry.js\";\nimport { namingFromConfig } from \"../config/namingFromConfig.js\";\nimport { getWorkers } from \"../config/loader.js\";\nimport { dispatchNamespaceSync } from \"../../features/dispatch-namespace/index.js\";\nimport { dnsRecordSync } from \"../../features/dns-records/index.js\";\nimport { getDispatchNamespaces, getDnsRecords, getLogpushJobs } from \"../../types.js\";\nimport { logpushJobSync } from \"../../features/logpush-job/index.js\";\nimport { fetchStackImports } from \"../imports/fetchStackImports.js\";\nimport { cloudflareAccountIdFromEnv } from \"../cloudflareEnv.js\";\nimport { mergeWorkerConfigForResourcePick } from \"../config/resolver.js\";\n\n/**\n * Drift-aware Cloudflare snapshot used by `tamer plan --out` and\n * `tamer apply --plan`.\n *\n * Drives the registry's `sync` hook for every kind against a **fresh\n * in-memory state** so the result reflects only what currently exists on\n * Cloudflare (no carryover from D1). The shape mirrors the post-sync\n * resource map but strips volatile fields (timestamps) so consecutive\n * snapshots of an unchanged account hash to the same value.\n *\n * Apply recomputes this against live Cloudflare and refuses to proceed if\n * `cloudflareHash` differs from the plan's pinned hash — that's how a\n * stale plan against drifted infra is detected.\n */\nexport async function buildCloudflareSnapshot(args: {\n config: CfiConfig;\n env: string;\n api: CFApiClient;\n baseDir: string;\n}): Promise<CloudflareSnapshot> {\n const { config, env, api, baseDir } = args;\n const naming = namingFromConfig(config);\n // In-memory snapshot — never round-trips to D1, so stack name only\n // matters for diagnostics; passing it keeps logs consistent with the\n // owning stack.\n const state = new StateManager(\n config.tenant.id,\n env,\n stackNameForConfig(config),\n );\n state.hydrateInMemory();\n\n const accountId = config.account_id ?? cloudflareAccountIdFromEnv() ?? \"\";\n const imports =\n env !== \"local\"\n ? await fetchStackImports(api, config, env).catch(\n () => ({} as Record<string, Record<string, string>>),\n )\n : ({} as Record<string, Record<string, string>>);\n\n const workers = await getWorkers(config, baseDir);\n\n for (const mod of resourceModules) {\n const all = await mod.fetchAll(api);\n for (const [workerKey, wc] of workers) {\n const mergedWorker =\n env !== \"local\" && accountId\n ? mergeWorkerConfigForResourcePick(\n config,\n workerKey,\n wc,\n env,\n accountId,\n naming,\n state,\n { referencesMode: \"tolerant\", imports },\n )\n : wc;\n const resources = mod.pickResources(mergedWorker);\n if (resources.length === 0) continue;\n mod.sync({\n all,\n resources,\n tenant: config.tenant,\n env,\n api,\n state,\n naming,\n config,\n baseDir,\n });\n }\n }\n\n if (getDispatchNamespaces(config).length > 0 && env !== \"local\") {\n await dispatchNamespaceSync(\n getDispatchNamespaces(config),\n config.tenant,\n env,\n api,\n state,\n );\n }\n\n if (getDnsRecords(config).length > 0 && env !== \"local\") {\n await dnsRecordSync(\n getDnsRecords(config),\n config.tenant,\n env,\n api,\n state,\n );\n }\n\n if (getLogpushJobs(config).length > 0 && env !== \"local\") {\n await logpushJobSync(\n getLogpushJobs(config),\n config.tenant,\n env,\n api,\n state,\n );\n }\n\n const entries = state.getAll();\n const stable: CloudflareSnapshot = { entries: {} };\n for (const key of Object.keys(entries).sort()) {\n stable.entries[key] = stripVolatile(entries[key]!);\n }\n return stable;\n}\n\n/**\n * Per-entry projection used in {@link CloudflareSnapshot}. Drops fields\n * that legitimately move between two reads of the same Cloudflare resource\n * (timestamps); keeps anything that, if it changed, would mean the plan is\n * no longer accurate (cfId, derivedName, kind-specific identity bits).\n */\nfunction stripVolatile(entry: StateEntry): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(entry)) {\n if (k === \"createdAt\" || k === \"updatedAt\") continue;\n out[k] = v;\n }\n return out;\n}\n\nexport interface CloudflareSnapshot {\n /** Sorted by key (== `derivedName`) for deterministic hashing. */\n entries: Record<string, Record<string, unknown>>;\n}\n"],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ import { n as loadConfig, t as getWorkers } from "./loader-DP7yXqT6.mjs";
2
+ import { n as cloudflareAccountIdFromEnv, t as CFApiClient } from "./CFApiClient-DhbyyV71.mjs";
3
+ import { _ as namingFromConfig, d as mergedWorkerConfigForEnv, f as resolveDeployedWorkerName, g as wranglerConfigCliArgs, l as buildIntraStackScriptNameMap, m as rewriteIntraStackServiceTargets, p as resolveWorkerConfig, t as fetchStackImports } from "./fetchStackImports-B4ZJahOt.mjs";
4
+ import { f as stackNameForConfig, t as StateManager } from "./StateManager-DTqtLLVX.mjs";
5
+ import "./r2S3EmptyBucket-DD81ZWQ7.mjs";
6
+ import { n as writeWranglerJson, t as generateWranglerConfig } from "./generator-CIMbcPzv.mjs";
7
+ import "./logpush-job-xS7270FZ.mjs";
8
+ import { r as workerRoutesApply } from "./worker-route-Be2IvOdr.mjs";
9
+ import { runSync } from "./sync-f2K2blwm.mjs";
10
+ import { t as spawnWranglerSync } from "./wranglerSpawn-DmEz0ldT.mjs";
11
+
12
+ //#region src/cli/commands/deploy.ts
13
+ /**
14
+ * Topologically sort workers by `services[].service` so that dependencies
15
+ * deploy before dependents. Cloudflare rejects deploys that reference
16
+ * service bindings to workers that don't yet exist on the account.
17
+ * Cross-config dependencies (services not in this monorepo) are ignored.
18
+ */
19
+ function topoSortWorkersByServiceBindings(workers, config, naming, env) {
20
+ const intraMap = buildIntraStackScriptNameMap(config, env, naming);
21
+ const scriptNameToKey = /* @__PURE__ */ new Map();
22
+ for (const [key, cfg] of workers) {
23
+ const name = resolveDeployedWorkerName(config, key, cfg, env, naming);
24
+ scriptNameToKey.set(name, key);
25
+ }
26
+ const byKey = new Map(workers);
27
+ const visited = /* @__PURE__ */ new Set();
28
+ const visiting = /* @__PURE__ */ new Set();
29
+ const out = [];
30
+ function visit(key) {
31
+ if (visited.has(key)) return;
32
+ if (visiting.has(key)) return;
33
+ visiting.add(key);
34
+ const cfg = byKey.get(key);
35
+ if (cfg) {
36
+ const deps = rewriteIntraStackServiceTargets(mergedWorkerConfigForEnv(cfg, env, config.tenant), intraMap).services?.map((s) => s.service) ?? [];
37
+ for (const dep of deps) {
38
+ const depKey = scriptNameToKey.get(dep);
39
+ if (depKey && depKey !== key) visit(depKey);
40
+ }
41
+ visiting.delete(key);
42
+ visited.add(key);
43
+ out.push([key, cfg]);
44
+ }
45
+ }
46
+ for (const [key] of workers) visit(key);
47
+ return out;
48
+ }
49
+ async function runDeploy(options) {
50
+ const workerFilter = options.worker;
51
+ const env = options.env;
52
+ if (!env) throw new Error("deploy: --env is required (e.g. --env staging). Tamer no longer defaults to prod — pass the env explicitly.");
53
+ const configPath = options.configPath;
54
+ const baseDir = process.cwd();
55
+ const config = await loadConfig(configPath, { env });
56
+ const accountId = config.account_id ?? cloudflareAccountIdFromEnv();
57
+ if (!accountId) throw new Error("account_id required in config or CLOUDFLARE_ACCOUNT_ID env var");
58
+ console.log(`Syncing state for env: ${env}...`);
59
+ await runSync({
60
+ env,
61
+ configPath
62
+ });
63
+ const naming = namingFromConfig(config);
64
+ const api = new CFApiClient(accountId);
65
+ const state = new StateManager(config.tenant.id, env, stackNameForConfig(config));
66
+ await state.hydrate(api);
67
+ const imports = await fetchStackImports(api, config, env);
68
+ state.beginOperation("deploy", workerFilter ? `worker:${workerFilter}` : void 0);
69
+ try {
70
+ const workers = await getWorkers(config, baseDir);
71
+ const toDeploy = workerFilter ? workers.filter(([k]) => k === workerFilter) : workers;
72
+ if (toDeploy.length === 0) throw new Error(workerFilter ? `Worker "${workerFilter}" not found` : "No workers configured");
73
+ const ordered = topoSortWorkersByServiceBindings(toDeploy, config, naming, env);
74
+ for (const [workerKey, workerConfig] of ordered) {
75
+ const resolved = await resolveWorkerConfig(config, workerKey, workerConfig, env, baseDir, accountId, naming, state, { imports });
76
+ const wranglerConfig = generateWranglerConfig(resolved, state, naming);
77
+ writeWranglerJson(resolved.workerDir, wranglerConfig, resolved.wranglerOutFile);
78
+ if (spawnWranglerSync([
79
+ "wrangler",
80
+ ...wranglerConfigCliArgs(resolved.wranglerOutFile),
81
+ "types"
82
+ ], {
83
+ cwd: resolved.workerDir,
84
+ stdio: "inherit"
85
+ }).status !== 0) throw new Error(`wrangler types failed for ${workerKey}`);
86
+ const dispatchNs = resolved.dispatchNamespace ?? options.dispatchNamespace;
87
+ const deployArgs = [
88
+ "wrangler",
89
+ ...wranglerConfigCliArgs(resolved.wranglerOutFile),
90
+ "deploy"
91
+ ];
92
+ if (dispatchNs) deployArgs.push("--dispatch-namespace", dispatchNs);
93
+ if (spawnWranglerSync(deployArgs, {
94
+ cwd: resolved.workerDir,
95
+ stdio: "inherit"
96
+ }).status !== 0) throw new Error(`wrangler deploy failed for ${workerKey}`);
97
+ console.log(`Deployed ${workerKey}`);
98
+ }
99
+ if (env !== "local") try {
100
+ await workerRoutesApply(env, config, baseDir, accountId, naming, state, api, { imports });
101
+ } catch (err) {
102
+ const msg = err instanceof Error ? err.message : String(err);
103
+ throw new Error(`worker routes apply failed after deploy: ${msg}`);
104
+ }
105
+ state.finishOperation();
106
+ await state.persist(api);
107
+ console.log(`Deploy complete for env: ${env}`);
108
+ } catch (err) {
109
+ state.failOperation(err instanceof Error ? err.message : String(err));
110
+ try {
111
+ await state.persist(api);
112
+ } catch {}
113
+ throw err;
114
+ }
115
+ }
116
+
117
+ //#endregion
118
+ export { runDeploy };
119
+ //# sourceMappingURL=deploy-gHEQxhmx.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy-gHEQxhmx.mjs","names":["out: WorkerEntry[]"],"sources":["../src/cli/commands/deploy.ts"],"sourcesContent":["import type { CfiConfig, WorkerConfig } from \"../../types.js\";\nimport { loadConfig, getWorkers } from \"../../core/config/loader.js\";\nimport { cloudflareAccountIdFromEnv } from \"../../core/cloudflareEnv.js\";\nimport { namingFromConfig } from \"../../core/config/namingFromConfig.js\";\nimport type { NamingEngine } from \"../../core/naming/NamingEngine.js\";\nimport { wranglerConfigCliArgs } from \"../../core/wrangler/wranglerOutFile.js\";\nimport { spawnWranglerSync } from \"../../core/wrangler/wranglerSpawn.js\";\nimport { StateManager } from \"../../core/state/StateManager.js\";\nimport { stackNameForConfig } from \"../../core/state/stackName.js\";\nimport { CFApiClient } from \"../../core/api/CFApiClient.js\";\nimport {\n buildIntraStackScriptNameMap,\n mergedWorkerConfigForEnv,\n resolveDeployedWorkerName,\n resolveWorkerConfig,\n rewriteIntraStackServiceTargets,\n} from \"../../core/config/resolver.js\";\nimport {\n generateWranglerConfig,\n writeWranglerJson,\n} from \"../../core/wrangler/generator.js\";\nimport { runSync } from \"./sync.js\";\nimport { workerRoutesApply } from \"../../features/worker-route/index.js\";\nimport { fetchStackImports } from \"../../core/imports/fetchStackImports.js\";\n\ntype WorkerEntry = [string, WorkerConfig];\n\n/**\n * Topologically sort workers by `services[].service` so that dependencies\n * deploy before dependents. Cloudflare rejects deploys that reference\n * service bindings to workers that don't yet exist on the account.\n * Cross-config dependencies (services not in this monorepo) are ignored.\n */\nexport function topoSortWorkersByServiceBindings(\n workers: WorkerEntry[],\n config: CfiConfig,\n naming: NamingEngine,\n env: string,\n): WorkerEntry[] {\n const intraMap = buildIntraStackScriptNameMap(config, env, naming);\n const scriptNameToKey = new Map<string, string>();\n for (const [key, cfg] of workers) {\n const name = resolveDeployedWorkerName(config, key, cfg, env, naming);\n scriptNameToKey.set(name, key);\n }\n\n const byKey = new Map(workers);\n const visited = new Set<string>();\n const visiting = new Set<string>();\n const out: WorkerEntry[] = [];\n\n function visit(key: string): void {\n if (visited.has(key)) return;\n if (visiting.has(key)) return;\n visiting.add(key);\n const cfg = byKey.get(key);\n if (cfg) {\n const merged = rewriteIntraStackServiceTargets(\n mergedWorkerConfigForEnv(cfg, env, config.tenant),\n intraMap,\n );\n const deps = merged.services?.map((s) => s.service) ?? [];\n for (const dep of deps) {\n const depKey = scriptNameToKey.get(dep);\n if (depKey && depKey !== key) visit(depKey);\n }\n visiting.delete(key);\n visited.add(key);\n out.push([key, cfg]);\n }\n }\n\n for (const [key] of workers) visit(key);\n return out;\n}\n\nexport async function runDeploy(options: {\n worker?: string;\n env?: string;\n configPath?: string;\n /** Passed to `wrangler deploy --dispatch-namespace` when the worker has no `dispatchNamespace` in config. */\n dispatchNamespace?: string;\n}): Promise<void> {\n const workerFilter = options.worker;\n // `--env` is intentionally required (no silent default to \"prod\") to\n // match every other Tamer command and to avoid the classic \"ran\n // `tamer deploy` from the wrong shell, shipped a half-tested branch\n // to production\" footgun.\n const env = options.env;\n if (!env) {\n throw new Error(\n \"deploy: --env is required (e.g. --env staging). \" +\n \"Tamer no longer defaults to prod — pass the env explicitly.\",\n );\n }\n const configPath = options.configPath;\n const baseDir = process.cwd();\n\n const config = await loadConfig(configPath, { env });\n const accountId =\n config.account_id ?? cloudflareAccountIdFromEnv();\n if (!accountId) {\n throw new Error(\n \"account_id required in config or CLOUDFLARE_ACCOUNT_ID env var\",\n );\n }\n\n console.log(`Syncing state for env: ${env}...`);\n await runSync({ env, configPath });\n\n const naming = namingFromConfig(config);\n const api = new CFApiClient(accountId);\n const state = new StateManager(\n config.tenant.id,\n env,\n stackNameForConfig(config),\n );\n await state.hydrate(api);\n const imports = await fetchStackImports(api, config, env);\n state.beginOperation(\"deploy\", workerFilter ? `worker:${workerFilter}` : undefined);\n\n try {\n\n const workers = await getWorkers(config, baseDir);\n const toDeploy = workerFilter\n ? workers.filter(([k]) => k === workerFilter)\n : workers;\n\n if (toDeploy.length === 0) {\n throw new Error(\n workerFilter\n ? `Worker \"${workerFilter}\" not found`\n : \"No workers configured\",\n );\n }\n\n const ordered = topoSortWorkersByServiceBindings(\n toDeploy,\n config,\n naming,\n env,\n );\n\n for (const [workerKey, workerConfig] of ordered) {\n const resolved = await resolveWorkerConfig(\n config,\n workerKey,\n workerConfig,\n env,\n baseDir,\n accountId,\n naming,\n state,\n { imports },\n );\n const wranglerConfig = generateWranglerConfig(resolved, state, naming);\n writeWranglerJson(resolved.workerDir, wranglerConfig, resolved.wranglerOutFile);\n\n const typesArgs = [\n \"wrangler\",\n ...wranglerConfigCliArgs(resolved.wranglerOutFile),\n \"types\",\n ];\n const typesResult = spawnWranglerSync(typesArgs, {\n cwd: resolved.workerDir,\n stdio: \"inherit\",\n });\n if (typesResult.status !== 0) {\n throw new Error(`wrangler types failed for ${workerKey}`);\n }\n\n const dispatchNs =\n resolved.dispatchNamespace ?? options.dispatchNamespace;\n const deployArgs = [\n \"wrangler\",\n ...wranglerConfigCliArgs(resolved.wranglerOutFile),\n \"deploy\",\n ];\n if (dispatchNs) {\n deployArgs.push(\"--dispatch-namespace\", dispatchNs);\n }\n\n const deployResult = spawnWranglerSync(deployArgs, {\n cwd: resolved.workerDir,\n stdio: \"inherit\",\n });\n if (deployResult.status !== 0) {\n throw new Error(`wrangler deploy failed for ${workerKey}`);\n }\n console.log(`Deployed ${workerKey}`);\n }\n\n if (env !== \"local\") {\n try {\n await workerRoutesApply(\n env,\n config,\n baseDir,\n accountId,\n naming,\n state,\n api,\n { imports },\n );\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`worker routes apply failed after deploy: ${msg}`);\n }\n }\n\n state.finishOperation();\n await state.persist(api);\n console.log(`Deploy complete for env: ${env}`);\n } catch (err) {\n state.failOperation(err instanceof Error ? err.message : String(err));\n try {\n await state.persist(api);\n } catch {\n /* swallow secondary persist failure */\n }\n throw err;\n }\n}\n"],"mappings":"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"}