@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 @@
1
+ {"version":3,"file":"dns-records.sync-Bpzz9H0s.mjs","names":["entry: DnsRecordStateEntry"],"sources":["../src/features/dispatch-namespace/dispatch-namespace.sync.ts","../src/features/dns-records/dns-records.sync.ts"],"sourcesContent":["import type { TenantMeta, DispatchNamespaceResourceConfig } from \"../../types.js\";\nimport type { StateManager } from \"../../core/state/StateManager.js\";\nimport type { CFApiClient } from \"../../core/api/CFApiClient.js\";\nimport { effectiveDispatchNamespaceName } from \"./dispatch-namespace.resolve.js\";\n\nexport async function dispatchNamespaceSync(\n resources: DispatchNamespaceResourceConfig[],\n tenant: TenantMeta,\n env: string,\n api: CFApiClient,\n state: StateManager,\n): Promise<void> {\n if (resources.length === 0) return;\n if (env === \"local\") return;\n\n const list = await api.dispatchNamespaceListAll();\n const names = new Set(list.map((n) => n.namespace_name));\n\n for (const config of resources) {\n const resolved = effectiveDispatchNamespaceName(config, env, tenant);\n const key = `dispatch_ns:${resolved}`;\n if (!names.has(resolved)) continue;\n\n state.set(key, {\n type: \"dispatch_namespace\",\n logicalName: config.logicalName,\n derivedName: resolved,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n });\n }\n}\n","import type {\n DnsRecordResourceConfig,\n DnsRecordStateEntry,\n TenantMeta,\n} from \"../../types.js\";\nimport type { CFApiClient } from \"../../core/api/CFApiClient.js\";\nimport type { StateManager } from \"../../core/state/StateManager.js\";\nimport {\n dnsRecordAppliesToEnv,\n dnsRecordCommentMarker,\n dnsRecordStateKey,\n} from \"./dns-records.resolve.js\";\n\n/**\n * Re-adopt every Tamer-attributed DNS record found on the live zones into\n * state. Mirrors the dispatch-namespace sync pattern: Tamer never\n * **destroys** records here, just imports rows that match its\n * attribution comment so a `apply` straight after `sync` is a no-op when\n * Cloudflare and config agree.\n *\n * Records on Cloudflare without Tamer's marker comment are ignored even\n * if they happen to match the declared `(zone, type, name)` — they are\n * presumed to be hand-managed and surface in `tamer drift` as\n * `unrecordedInState` so the operator can decide.\n */\nexport async function dnsRecordSync(\n resources: DnsRecordResourceConfig[],\n tenant: TenantMeta,\n env: string,\n api: CFApiClient,\n state: StateManager,\n): Promise<void> {\n if (resources.length === 0) return;\n const applicable = resources.filter((r) => dnsRecordAppliesToEnv(r, env));\n if (applicable.length === 0) return;\n\n const zoneCache = new Map<\n string,\n Awaited<ReturnType<CFApiClient[\"zoneDnsRecordListAll\"]>>\n >();\n\n for (const config of applicable) {\n let live = zoneCache.get(config.zoneId);\n if (!live) {\n live = await api.zoneDnsRecordListAll(config.zoneId);\n zoneCache.set(config.zoneId, live);\n }\n const marker = dnsRecordCommentMarker(tenant, env, config.logicalName);\n const adopted = live.find(\n (r) =>\n r.type === config.type &&\n typeof r.comment === \"string\" &&\n r.comment.startsWith(marker),\n );\n if (!adopted) continue;\n const stateKey = dnsRecordStateKey(config.zoneId, config.type, config.name);\n const ts = new Date().toISOString();\n const entry: DnsRecordStateEntry = {\n type: \"dns_record\",\n logicalName: config.logicalName,\n zoneId: config.zoneId,\n recordType: config.type,\n name: adopted.name,\n content: adopted.content,\n ttl: adopted.ttl ?? 1,\n proxied: adopted.proxied ?? false,\n priority: adopted.priority,\n comment: adopted.comment ?? marker,\n recordId: adopted.id,\n createdAt: state.get(stateKey)?.type === \"dns_record\"\n ? (state.get(stateKey) as DnsRecordStateEntry).createdAt\n : ts,\n updatedAt: ts,\n };\n state.set(stateKey, entry);\n }\n}\n"],"mappings":";;;;AAKA,eAAsB,sBACpB,WACA,QACA,KACA,KACA,OACe;AACf,KAAI,UAAU,WAAW,EAAG;AAC5B,KAAI,QAAQ,QAAS;CAErB,MAAM,OAAO,MAAM,IAAI,0BAA0B;CACjD,MAAM,QAAQ,IAAI,IAAI,KAAK,KAAK,MAAM,EAAE,eAAe,CAAC;AAExD,MAAK,MAAM,UAAU,WAAW;EAC9B,MAAM,WAAW,+BAA+B,QAAQ,KAAK,OAAO;EACpE,MAAM,MAAM,eAAe;AAC3B,MAAI,CAAC,MAAM,IAAI,SAAS,CAAE;AAE1B,QAAM,IAAI,KAAK;GACb,MAAM;GACN,aAAa,OAAO;GACpB,aAAa;GACb,4BAAW,IAAI,MAAM,EAAC,aAAa;GACnC,4BAAW,IAAI,MAAM,EAAC,aAAa;GACpC,CAAC;;;;;;;;;;;;;;;;;;ACJN,eAAsB,cACpB,WACA,QACA,KACA,KACA,OACe;AACf,KAAI,UAAU,WAAW,EAAG;CAC5B,MAAM,aAAa,UAAU,QAAQ,MAAM,sBAAsB,GAAG,IAAI,CAAC;AACzE,KAAI,WAAW,WAAW,EAAG;CAE7B,MAAM,4BAAY,IAAI,KAGnB;AAEH,MAAK,MAAM,UAAU,YAAY;EAC/B,IAAI,OAAO,UAAU,IAAI,OAAO,OAAO;AACvC,MAAI,CAAC,MAAM;AACT,UAAO,MAAM,IAAI,qBAAqB,OAAO,OAAO;AACpD,aAAU,IAAI,OAAO,QAAQ,KAAK;;EAEpC,MAAM,SAAS,uBAAuB,QAAQ,KAAK,OAAO,YAAY;EACtE,MAAM,UAAU,KAAK,MAClB,MACC,EAAE,SAAS,OAAO,QAClB,OAAO,EAAE,YAAY,YACrB,EAAE,QAAQ,WAAW,OAAO,CAC/B;AACD,MAAI,CAAC,QAAS;EACd,MAAM,WAAW,kBAAkB,OAAO,QAAQ,OAAO,MAAM,OAAO,KAAK;EAC3E,MAAM,sBAAK,IAAI,MAAM,EAAC,aAAa;EACnC,MAAMA,QAA6B;GACjC,MAAM;GACN,aAAa,OAAO;GACpB,QAAQ,OAAO;GACf,YAAY,OAAO;GACnB,MAAM,QAAQ;GACd,SAAS,QAAQ;GACjB,KAAK,QAAQ,OAAO;GACpB,SAAS,QAAQ,WAAW;GAC5B,UAAU,QAAQ;GAClB,SAAS,QAAQ,WAAW;GAC5B,UAAU,QAAQ;GAClB,WAAW,MAAM,IAAI,SAAS,EAAE,SAAS,eACpC,MAAM,IAAI,SAAS,CAAyB,YAC7C;GACJ,WAAW;GACZ;AACD,QAAM,IAAI,UAAU,MAAM"}
@@ -0,0 +1,34 @@
1
+ import { n as cloudflareAccountIdFromEnv, r as cloudflareApiTokenFromEnv, t as CFApiClient } from "./CFApiClient-DhbyyV71.mjs";
2
+
3
+ //#region src/cli/commands/doctor.ts
4
+ async function runDoctor(options) {
5
+ const accountId = cloudflareAccountIdFromEnv();
6
+ const token = cloudflareApiTokenFromEnv();
7
+ const report = {
8
+ ok: false,
9
+ accountIdPresent: !!accountId,
10
+ apiTokenPresent: !!token && token.length > 0,
11
+ accountReadable: false
12
+ };
13
+ if (!accountId || token.length === 0) {
14
+ report.error = "CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN must be set";
15
+ if (options.json) console.log(JSON.stringify(report, null, 2));
16
+ else console.error(report.error);
17
+ return 1;
18
+ }
19
+ try {
20
+ await new CFApiClient(accountId, token).accountRead();
21
+ report.accountReadable = true;
22
+ report.ok = true;
23
+ } catch (err) {
24
+ report.error = err instanceof Error ? err.message : String(err);
25
+ }
26
+ if (options.json) console.log(JSON.stringify(report, null, 2));
27
+ else console.log(report.ok ? "doctor: account token can read Cloudflare account API." : `doctor: failed — ${report.error ?? "unknown"}`);
28
+ if (!report.ok) return 1;
29
+ return 0;
30
+ }
31
+
32
+ //#endregion
33
+ export { runDoctor };
34
+ //# sourceMappingURL=doctor-C_hs7k2D.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor-C_hs7k2D.mjs","names":["report: DoctorReport"],"sources":["../src/cli/commands/doctor.ts"],"sourcesContent":["import {\n cloudflareAccountIdFromEnv,\n cloudflareApiTokenFromEnv,\n} from \"../../core/cloudflareEnv.js\";\nimport { CFApiClient } from \"../../core/api/CFApiClient.js\";\n\nexport interface DoctorReport {\n ok: boolean;\n accountIdPresent: boolean;\n apiTokenPresent: boolean;\n accountReadable: boolean;\n error?: string;\n}\n\nexport async function runDoctor(options: { json?: boolean }): Promise<number> {\n const accountId = cloudflareAccountIdFromEnv();\n const token = cloudflareApiTokenFromEnv();\n const report: DoctorReport = {\n ok: false,\n accountIdPresent: !!accountId,\n apiTokenPresent: !!token && token.length > 0,\n accountReadable: false,\n };\n\n if (!accountId || token.length === 0) {\n report.error = \"CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN must be set\";\n if (options.json) console.log(JSON.stringify(report, null, 2));\n else console.error(report.error);\n return 1;\n }\n\n try {\n const api = new CFApiClient(accountId, token);\n await api.accountRead();\n report.accountReadable = true;\n report.ok = true;\n } catch (err) {\n report.error = err instanceof Error ? err.message : String(err);\n }\n\n if (options.json) {\n console.log(JSON.stringify(report, null, 2));\n } else {\n console.log(\n report.ok\n ? \"doctor: account token can read Cloudflare account API.\"\n : `doctor: failed — ${report.error ?? \"unknown\"}`,\n );\n }\n\n if (!report.ok) {\n return 1;\n }\n return 0;\n}\n"],"mappings":";;;AAcA,eAAsB,UAAU,SAA8C;CAC5E,MAAM,YAAY,4BAA4B;CAC9C,MAAM,QAAQ,2BAA2B;CACzC,MAAMA,SAAuB;EAC3B,IAAI;EACJ,kBAAkB,CAAC,CAAC;EACpB,iBAAiB,CAAC,CAAC,SAAS,MAAM,SAAS;EAC3C,iBAAiB;EAClB;AAED,KAAI,CAAC,aAAa,MAAM,WAAW,GAAG;AACpC,SAAO,QAAQ;AACf,MAAI,QAAQ,KAAM,SAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;MACzD,SAAQ,MAAM,OAAO,MAAM;AAChC,SAAO;;AAGT,KAAI;AAEF,QADY,IAAI,YAAY,WAAW,MAAM,CACnC,aAAa;AACvB,SAAO,kBAAkB;AACzB,SAAO,KAAK;UACL,KAAK;AACZ,SAAO,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;;AAGjE,KAAI,QAAQ,KACV,SAAQ,IAAI,KAAK,UAAU,QAAQ,MAAM,EAAE,CAAC;KAE5C,SAAQ,IACN,OAAO,KACH,2DACA,oBAAoB,OAAO,SAAS,YACzC;AAGH,KAAI,CAAC,OAAO,GACV,QAAO;AAET,QAAO"}
@@ -0,0 +1,10 @@
1
+ import "./loader-DP7yXqT6.mjs";
2
+ import "./fetchStackImports-B4ZJahOt.mjs";
3
+ import "./StateManager-DTqtLLVX.mjs";
4
+ import "./r2S3EmptyBucket-DD81ZWQ7.mjs";
5
+ import { n as runDrift, t as computeDriftReport } from "./drift-D8ZrSgTn.mjs";
6
+ import "./logpush-job-xS7270FZ.mjs";
7
+ import "./worker-route-Be2IvOdr.mjs";
8
+ import "./workers-aGILs77X.mjs";
9
+
10
+ export { runDrift };
@@ -0,0 +1,323 @@
1
+ import { f as getDispatchNamespaces, m as getLogpushJobs, p as getDnsRecords } from "./normalize-Bx0bpFop.mjs";
2
+ import { n as loadConfig, t as getWorkers } from "./loader-DP7yXqT6.mjs";
3
+ import { n as cloudflareAccountIdFromEnv, t as CFApiClient } from "./CFApiClient-DhbyyV71.mjs";
4
+ import { _ as namingFromConfig, a as resourceModules, t as fetchStackImports, u as mergeWorkerConfigForResourcePick } from "./fetchStackImports-B4ZJahOt.mjs";
5
+ import { a as tenantStateKey, f as stackNameForConfig, o as effectiveDispatchNamespaceName, t as StateManager } from "./StateManager-DTqtLLVX.mjs";
6
+ import { n as dnsRecordCommentMarker, r as dnsRecordStateKey, t as dnsRecordAppliesToEnv } from "./dns-records.resolve-DwBR_1WI.mjs";
7
+ import { r as logpushJobDrift } from "./logpush-job-xS7270FZ.mjs";
8
+ import { t as workerRoutesDrift } from "./worker-route-Be2IvOdr.mjs";
9
+ import { t as workersDrift } from "./workers-aGILs77X.mjs";
10
+
11
+ //#region src/features/dispatch-namespace/dispatch-namespace.drift.ts
12
+ function dispatchNamespaceDrift(allDispatch, resources, env, tenant, state) {
13
+ const drift = {
14
+ kind: "dispatch_namespace",
15
+ missingFromCloudflare: [],
16
+ unrecordedInState: [],
17
+ undeployed: []
18
+ };
19
+ const cfNames = new Set(allDispatch.map((d) => d.namespace_name));
20
+ const allState = state.getAll();
21
+ const nsState = Object.values(allState).filter((e) => e.type === "dispatch_namespace");
22
+ for (const config of resources) {
23
+ const derivedName = effectiveDispatchNamespaceName(config, env, tenant);
24
+ const stateEntry = nsState.find((e) => e.logicalName === config.logicalName && e.derivedName === derivedName);
25
+ const onCf = cfNames.has(derivedName);
26
+ if (stateEntry && !onCf) drift.missingFromCloudflare.push({
27
+ logicalName: stateEntry.logicalName,
28
+ derivedName: stateEntry.derivedName
29
+ });
30
+ else if (onCf && !stateEntry) drift.unrecordedInState.push({
31
+ logicalName: config.logicalName,
32
+ derivedName
33
+ });
34
+ else if (!onCf && !stateEntry) drift.undeployed.push({
35
+ logicalName: config.logicalName,
36
+ derivedName
37
+ });
38
+ }
39
+ return drift;
40
+ }
41
+
42
+ //#endregion
43
+ //#region src/features/dns-records/dns-records.drift.ts
44
+ function dnsRecordDrift(byZone, resources, tenant, env, state) {
45
+ const drift = {
46
+ kind: "dns_record",
47
+ missingFromCloudflare: [],
48
+ unrecordedInState: [],
49
+ undeployed: []
50
+ };
51
+ const stateRecords = Object.values(state.getAll()).filter((e) => e.type === "dns_record");
52
+ for (const config of resources) {
53
+ if (!dnsRecordAppliesToEnv(config, env)) continue;
54
+ const live = byZone.get(config.zoneId) ?? [];
55
+ const marker = dnsRecordCommentMarker(tenant, env, config.logicalName);
56
+ const stateKey = dnsRecordStateKey(config.zoneId, config.type, config.name);
57
+ const entry = stateRecords.find((e) => e.zoneId === config.zoneId && e.recordType === config.type && e.logicalName === config.logicalName);
58
+ const onCf = entry ? live.find((r) => r.id === entry.recordId) : live.find((r) => r.type === config.type && typeof r.comment === "string" && r.comment.startsWith(marker));
59
+ if (entry && !onCf) drift.missingFromCloudflare.push({
60
+ logicalName: entry.logicalName,
61
+ derivedName: `${entry.recordType} ${entry.name}`,
62
+ cfId: entry.recordId
63
+ });
64
+ else if (onCf && !entry) drift.unrecordedInState.push({
65
+ logicalName: config.logicalName,
66
+ derivedName: `${config.type} ${onCf.name}`,
67
+ cfId: onCf.id
68
+ });
69
+ else if (!onCf && !entry) drift.undeployed.push({
70
+ logicalName: config.logicalName,
71
+ derivedName: `${config.type} ${config.name}`,
72
+ detail: stateKey
73
+ });
74
+ }
75
+ return drift;
76
+ }
77
+
78
+ //#endregion
79
+ //#region src/core/drift/drift.types.ts
80
+ function resourceDriftIsClean(d) {
81
+ return d.missingFromCloudflare.length === 0 && d.unrecordedInState.length === 0 && d.undeployed.length === 0;
82
+ }
83
+ function reportHasDrift(resources) {
84
+ return resources.some((d) => !resourceDriftIsClean(d));
85
+ }
86
+
87
+ //#endregion
88
+ //#region src/core/drift/tenantDrift.ts
89
+ /**
90
+ * Drift for workspace tenants in {@link CfiState.tenants}: dispatch script and
91
+ * recorded D1 shards must still exist on Cloudflare.
92
+ *
93
+ * `unrecordedInState` / `undeployed` are intentionally empty here — tenant
94
+ * discovery from CF alone is heuristic until product/script naming is fully
95
+ * pinned (`docs/scope-remaining.md` D-1).
96
+ */
97
+ async function tenantDrift(state, api, allD1) {
98
+ const drift = {
99
+ kind: "tenant",
100
+ missingFromCloudflare: [],
101
+ unrecordedInState: [],
102
+ undeployed: []
103
+ };
104
+ const d1ById = new Map(allD1.map((d) => [d.uuid, d.name]));
105
+ const tenants = state.listTenants().filter((t) => t.provisioningStatus !== "tombstoned");
106
+ if (tenants.length === 0) return drift;
107
+ const scriptLists = /* @__PURE__ */ new Map();
108
+ async function scriptsInNs(ns) {
109
+ let set = scriptLists.get(ns);
110
+ if (!set) {
111
+ const list = await api.dispatchNamespaceScriptList(ns);
112
+ set = new Set(list.map((s) => s.id));
113
+ scriptLists.set(ns, set);
114
+ }
115
+ return set;
116
+ }
117
+ for (const t of tenants) {
118
+ const logical = tenantStateKey(t.product, t.workspace);
119
+ try {
120
+ if (!(await scriptsInNs(t.dispatchNamespaceName)).has(t.scriptName)) drift.missingFromCloudflare.push({
121
+ logicalName: logical,
122
+ derivedName: t.scriptName,
123
+ detail: "dispatch_script"
124
+ });
125
+ } catch {
126
+ drift.missingFromCloudflare.push({
127
+ logicalName: logical,
128
+ derivedName: t.dispatchNamespaceName,
129
+ detail: "dispatch_namespace_list_failed"
130
+ });
131
+ }
132
+ for (const shard of t.d1Shards ?? []) if (!d1ById.has(shard.cfId)) drift.missingFromCloudflare.push({
133
+ logicalName: logical,
134
+ derivedName: shard.derivedName,
135
+ cfId: shard.cfId,
136
+ detail: `d1:${shard.role}`
137
+ });
138
+ }
139
+ return drift;
140
+ }
141
+
142
+ //#endregion
143
+ //#region src/cli/commands/drift.ts
144
+ /**
145
+ * Compute a read-only drift report for the given env.
146
+ *
147
+ * Compares Tamer state (D1 `tamer-state-{env}`) against the Cloudflare API
148
+ * and the resources declared in `tamer.config.ts`. Reports three categories:
149
+ *
150
+ * - `missingFromCloudflare` — tracked in state but the CF resource is gone.
151
+ * - `unrecordedInState` — exists on CF and matches a declared resource, but
152
+ * no state entry tracks it (e.g. created out-of-band; run `tamer sync`).
153
+ * - `undeployed` — declared in this stack's config, present in neither
154
+ * state nor CF (run `tamer apply`).
155
+ *
156
+ * Pure: never writes to state. Returns the report so callers can choose how to
157
+ * render or consume it.
158
+ */
159
+ async function computeDriftReport(options) {
160
+ const env = options.env ?? "local";
161
+ const configPath = options.configPath;
162
+ const baseDir = process.cwd();
163
+ const config = await loadConfig(configPath, { env });
164
+ const accountId = config.account_id ?? cloudflareAccountIdFromEnv();
165
+ if (!accountId) throw new Error("account_id required in config or CLOUDFLARE_ACCOUNT_ID env var");
166
+ const api = new CFApiClient(accountId);
167
+ const naming = namingFromConfig(config);
168
+ const state = new StateManager(config.tenant.id, env, stackNameForConfig(config));
169
+ await state.hydrate(api);
170
+ const imports = await fetchStackImports(api, config, env).catch(() => ({}));
171
+ async function safeList(label, fn) {
172
+ try {
173
+ return await fn();
174
+ } catch (err) {
175
+ const msg = err instanceof Error ? err.message : String(err);
176
+ console.warn(`[drift] skipping ${label}: ${msg}`);
177
+ return [];
178
+ }
179
+ }
180
+ const lists = await Promise.all(resourceModules.map((m) => safeList(`${m.label} list`, () => m.fetchAll(api))));
181
+ const allDispatch = getDispatchNamespaces(config).length > 0 ? await safeList("dispatch namespaces", () => api.dispatchNamespaceListAll()) : [];
182
+ const allLogpushJobs = getLogpushJobs(config).length > 0 && env !== "local" ? await safeList("logpush jobs", () => api.logpushAccountJobsList()) : [];
183
+ const workers = await getWorkers(config, baseDir);
184
+ const aggregated = /* @__PURE__ */ new Map();
185
+ function merge(d) {
186
+ const existing = aggregated.get(d.kind);
187
+ if (!existing) {
188
+ aggregated.set(d.kind, d);
189
+ return;
190
+ }
191
+ existing.missingFromCloudflare.push(...d.missingFromCloudflare);
192
+ existing.unrecordedInState.push(...d.unrecordedInState);
193
+ existing.undeployed.push(...d.undeployed);
194
+ }
195
+ for (const [workerKey, workerConfig] of workers) {
196
+ const mergedWorker = mergeWorkerConfigForResourcePick(config, workerKey, workerConfig, env, accountId, naming, state, {
197
+ referencesMode: "tolerant",
198
+ imports
199
+ });
200
+ resourceModules.forEach((mod, i) => {
201
+ const resources = mod.pickResources(mergedWorker);
202
+ if (resources.length === 0) return;
203
+ merge(mod.drift({
204
+ resources,
205
+ all: lists[i],
206
+ tenant: config.tenant,
207
+ env,
208
+ api,
209
+ state,
210
+ naming,
211
+ config,
212
+ baseDir
213
+ }));
214
+ });
215
+ }
216
+ const dispatchResources = getDispatchNamespaces(config);
217
+ if (dispatchResources.length > 0) merge(dispatchNamespaceDrift(allDispatch, dispatchResources, env, config.tenant, state));
218
+ const dnsResources = getDnsRecords(config);
219
+ if (dnsResources.length > 0 && env !== "local") {
220
+ const byZone = /* @__PURE__ */ new Map();
221
+ const zones = Array.from(new Set(dnsResources.map((r) => r.zoneId)));
222
+ for (const zoneId of zones) {
223
+ const live = await safeList(`dns records (zone ${zoneId})`, () => api.zoneDnsRecordListAll(zoneId));
224
+ byZone.set(zoneId, live);
225
+ }
226
+ merge(dnsRecordDrift(byZone, dnsResources, config.tenant, env, state));
227
+ }
228
+ const logpushResources = getLogpushJobs(config);
229
+ if (logpushResources.length > 0 && env !== "local") merge(logpushJobDrift(allLogpushJobs, logpushResources, env, config.tenant, state));
230
+ if (state.listTenants().length > 0) {
231
+ const allD1Idx = resourceModules.findIndex((m) => m.kind === "d1");
232
+ merge(await tenantDrift(state, api, allD1Idx >= 0 ? lists[allD1Idx] : []));
233
+ }
234
+ const workerRouteReport = await workerRoutesDrift(env, config, baseDir, accountId, naming, state, api, { imports });
235
+ if (workerRouteReport) merge(workerRouteReport);
236
+ const workerScriptReport = await workersDrift(env, config, baseDir, accountId, naming, state, api, { imports });
237
+ if (workerScriptReport) merge(workerScriptReport);
238
+ const dedupedResources = Array.from(aggregated.values()).map((d) => ({
239
+ ...d,
240
+ missingFromCloudflare: dedupe(d.missingFromCloudflare),
241
+ unrecordedInState: dedupe(d.unrecordedInState),
242
+ undeployed: dedupe(d.undeployed)
243
+ }));
244
+ return {
245
+ tenantId: config.tenant.id,
246
+ env,
247
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
248
+ resources: dedupedResources,
249
+ hasDrift: reportHasDrift(dedupedResources)
250
+ };
251
+ }
252
+ function dedupe(list) {
253
+ const seen = /* @__PURE__ */ new Set();
254
+ const out = [];
255
+ for (const item of list) {
256
+ const key = `${item.logicalName}::${item.derivedName}`;
257
+ if (seen.has(key)) continue;
258
+ seen.add(key);
259
+ out.push(item);
260
+ }
261
+ return out;
262
+ }
263
+ /**
264
+ * CLI entry point. Prints a human report (or JSON when `--json`) and sets a
265
+ * non-zero process exit code when drift is found.
266
+ */
267
+ async function runDrift(options) {
268
+ const report = await computeDriftReport({
269
+ env: options.env,
270
+ configPath: options.configPath
271
+ });
272
+ if (options.json) console.log(JSON.stringify(report, null, 2));
273
+ else printHumanReport(report);
274
+ return report.hasDrift ? 1 : 0;
275
+ }
276
+ function printHumanReport(report) {
277
+ console.log(`\nDrift report — tenant ${report.tenantId}, env ${report.env}\n`);
278
+ if (report.resources.length === 0) {
279
+ console.log(" (no managed resource kinds in this config)\n");
280
+ return;
281
+ }
282
+ for (const d of report.resources) {
283
+ const total = d.missingFromCloudflare.length + d.unrecordedInState.length + d.undeployed.length;
284
+ console.log(`${labelFor(d.kind)} (${total} drift):`);
285
+ if (total === 0) {
286
+ console.log(" ok");
287
+ continue;
288
+ }
289
+ if (d.missingFromCloudflare.length) {
290
+ console.log(" missing from Cloudflare (state references gone):");
291
+ for (const e of d.missingFromCloudflare) console.log(` - ${e.logicalName} -> ${e.derivedName}${suffix(e.cfId)}`);
292
+ }
293
+ if (d.unrecordedInState.length) {
294
+ console.log(" unrecorded in state (run `tamer sync`):");
295
+ for (const e of d.unrecordedInState) console.log(` - ${e.logicalName} -> ${e.derivedName}${suffix(e.cfId)}`);
296
+ }
297
+ if (d.undeployed.length) {
298
+ console.log(" undeployed (run `tamer apply`):");
299
+ for (const e of d.undeployed) console.log(` - ${e.logicalName} -> ${e.derivedName}`);
300
+ }
301
+ }
302
+ console.log(report.hasDrift ? "\nDrift detected.\n" : "\nNo drift.\n");
303
+ }
304
+ const REGISTRY_LABELS = Object.fromEntries(resourceModules.map((m) => [m.kind, m.label]));
305
+ function labelFor(kind) {
306
+ if (REGISTRY_LABELS[kind]) return REGISTRY_LABELS[kind];
307
+ switch (kind) {
308
+ case "dispatch_namespace": return "Dispatch namespaces";
309
+ case "logpush_job": return "Logpush jobs";
310
+ case "dns_record": return "DNS records";
311
+ case "tenant": return "Workspace tenants";
312
+ case "worker_route": return "HTTP routes (Workers Routes API)";
313
+ case "worker_script": return "Worker scripts";
314
+ default: return kind;
315
+ }
316
+ }
317
+ function suffix(cfId) {
318
+ return cfId ? ` [${cfId}]` : "";
319
+ }
320
+
321
+ //#endregion
322
+ export { runDrift as n, computeDriftReport as t };
323
+ //# sourceMappingURL=drift-D8ZrSgTn.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drift-D8ZrSgTn.mjs","names":["drift: ResourceDrift","drift: ResourceDrift","drift: ResourceDrift","byZone: DnsRecordsByZone","out: T[]","REGISTRY_LABELS: Record<string, string>"],"sources":["../src/features/dispatch-namespace/dispatch-namespace.drift.ts","../src/features/dns-records/dns-records.drift.ts","../src/core/drift/drift.types.ts","../src/core/drift/tenantDrift.ts","../src/cli/commands/drift.ts"],"sourcesContent":["import type {\n DispatchNamespaceResourceConfig,\n DispatchNamespaceStateEntry,\n TenantMeta,\n} from \"../../types.js\";\nimport type { StateManager } from \"../../core/state/StateManager.js\";\nimport type { ResourceDrift } from \"../../core/drift/drift.types.js\";\nimport { effectiveDispatchNamespaceName } from \"./dispatch-namespace.resolve.js\";\n\ninterface CFDispatchNamespace {\n namespace_name: string;\n}\n\nexport function dispatchNamespaceDrift(\n allDispatch: CFDispatchNamespace[],\n resources: DispatchNamespaceResourceConfig[],\n env: string,\n tenant: TenantMeta,\n state: StateManager,\n): ResourceDrift {\n const drift: ResourceDrift = {\n kind: \"dispatch_namespace\",\n missingFromCloudflare: [],\n unrecordedInState: [],\n undeployed: [],\n };\n\n const cfNames = new Set(allDispatch.map((d) => d.namespace_name));\n const allState = state.getAll();\n const nsState = Object.values(allState).filter(\n (e): e is DispatchNamespaceStateEntry => e.type === \"dispatch_namespace\",\n );\n\n for (const config of resources) {\n const derivedName = effectiveDispatchNamespaceName(config, env, tenant);\n const stateEntry = nsState.find(\n (e) => e.logicalName === config.logicalName && e.derivedName === derivedName,\n );\n const onCf = cfNames.has(derivedName);\n\n if (stateEntry && !onCf) {\n drift.missingFromCloudflare.push({\n logicalName: stateEntry.logicalName,\n derivedName: stateEntry.derivedName,\n });\n } else if (onCf && !stateEntry) {\n drift.unrecordedInState.push({\n logicalName: config.logicalName,\n derivedName,\n });\n } else if (!onCf && !stateEntry) {\n drift.undeployed.push({\n logicalName: config.logicalName,\n derivedName,\n });\n }\n }\n\n return drift;\n}\n","import type {\n DnsRecordResourceConfig,\n DnsRecordStateEntry,\n TenantMeta,\n} from \"../../types.js\";\nimport type { StateManager } from \"../../core/state/StateManager.js\";\nimport type { ResourceDrift } from \"../../core/drift/drift.types.js\";\nimport {\n dnsRecordAppliesToEnv,\n dnsRecordCommentMarker,\n dnsRecordStateKey,\n} from \"./dns-records.resolve.js\";\n\ninterface CFDnsRecord {\n id: string;\n type: string;\n name: string;\n content: string;\n ttl?: number;\n proxied?: boolean;\n priority?: number;\n comment?: string | null;\n}\n\n/**\n * Map of `zoneId → live records`. The drift caller pre-fetches because\n * other resource modules also iterate zones (worker routes), so we\n * accept the snapshot rather than refetching here.\n */\nexport type DnsRecordsByZone = Map<string, CFDnsRecord[]>;\n\nexport function dnsRecordDrift(\n byZone: DnsRecordsByZone,\n resources: DnsRecordResourceConfig[],\n tenant: TenantMeta,\n env: string,\n state: StateManager,\n): ResourceDrift {\n const drift: ResourceDrift = {\n kind: \"dns_record\",\n missingFromCloudflare: [],\n unrecordedInState: [],\n undeployed: [],\n };\n\n const stateRecords = Object.values(state.getAll()).filter(\n (e): e is DnsRecordStateEntry => e.type === \"dns_record\",\n );\n\n for (const config of resources) {\n if (!dnsRecordAppliesToEnv(config, env)) continue;\n const live = byZone.get(config.zoneId) ?? [];\n const marker = dnsRecordCommentMarker(tenant, env, config.logicalName);\n const stateKey = dnsRecordStateKey(config.zoneId, config.type, config.name);\n const entry = stateRecords.find(\n (e) =>\n e.zoneId === config.zoneId &&\n e.recordType === config.type &&\n e.logicalName === config.logicalName,\n );\n const onCf = entry\n ? live.find((r) => r.id === entry.recordId)\n : live.find(\n (r) =>\n r.type === config.type &&\n typeof r.comment === \"string\" &&\n r.comment.startsWith(marker),\n );\n\n if (entry && !onCf) {\n drift.missingFromCloudflare.push({\n logicalName: entry.logicalName,\n derivedName: `${entry.recordType} ${entry.name}`,\n cfId: entry.recordId,\n });\n } else if (onCf && !entry) {\n drift.unrecordedInState.push({\n logicalName: config.logicalName,\n derivedName: `${config.type} ${onCf.name}`,\n cfId: onCf.id,\n });\n } else if (!onCf && !entry) {\n drift.undeployed.push({\n logicalName: config.logicalName,\n derivedName: `${config.type} ${config.name}`,\n detail: stateKey,\n });\n }\n }\n\n return drift;\n}\n","/**\n * Read-only drift report comparing recorded state vs. Cloudflare reality vs.\n * the current `tamer.config.ts`.\n */\n\nexport type DriftKind =\n | \"d1\"\n | \"r2\"\n | \"kv\"\n | \"queue\"\n | \"hyperdrive\"\n | \"vectorize\"\n | \"ai_gateway\"\n | \"pipeline\"\n | \"workflow\"\n | \"secret_store\"\n | \"dns_record\"\n | \"dispatch_namespace\"\n | \"logpush_job\"\n | \"tenant\"\n | \"worker_route\"\n | \"worker_script\";\n\nexport interface DriftEntry {\n /** Logical resource name from `tamer.config.ts`. */\n logicalName: string;\n /** Cloudflare-side name (or `(unknown)` when no CF or state side knows it). */\n derivedName: string;\n /** Cloudflare resource ID, when known (D1 uuid, KV id). */\n cfId?: string;\n /** Optional human-readable detail (e.g. shard date). */\n detail?: string;\n}\n\nexport interface ResourceDrift {\n kind: DriftKind;\n /** Tracked in state but no longer present on Cloudflare. */\n missingFromCloudflare: DriftEntry[];\n /**\n * Present on Cloudflare and matches a declared resource in this config,\n * but no state entry tracks it (e.g. created out-of-band).\n */\n unrecordedInState: DriftEntry[];\n /**\n * Declared in this stack's config but neither tracked in state nor present\n * on Cloudflare (run `tamer apply`).\n */\n undeployed: DriftEntry[];\n}\n\nexport interface DriftReport {\n tenantId: string;\n env: string;\n generatedAt: string;\n resources: ResourceDrift[];\n /** True iff any of the three categories has at least one entry. */\n hasDrift: boolean;\n}\n\nexport function resourceDriftIsClean(d: ResourceDrift): boolean {\n return (\n d.missingFromCloudflare.length === 0 &&\n d.unrecordedInState.length === 0 &&\n d.undeployed.length === 0\n );\n}\n\nexport function reportHasDrift(resources: ResourceDrift[]): boolean {\n return resources.some((d) => !resourceDriftIsClean(d));\n}\n","import type { CFApiClient } from \"../api/CFApiClient.js\";\nimport type { StateManager } from \"../state/StateManager.js\";\nimport type { ResourceDrift } from \"./drift.types.js\";\nimport { tenantStateKey } from \"../tenant/tenantKeys.js\";\n\ninterface CFD1 {\n uuid: string;\n name: string;\n}\n\n/**\n * Drift for workspace tenants in {@link CfiState.tenants}: dispatch script and\n * recorded D1 shards must still exist on Cloudflare.\n *\n * `unrecordedInState` / `undeployed` are intentionally empty here — tenant\n * discovery from CF alone is heuristic until product/script naming is fully\n * pinned (`docs/scope-remaining.md` D-1).\n */\nexport async function tenantDrift(\n state: StateManager,\n api: CFApiClient,\n allD1: CFD1[],\n): Promise<ResourceDrift> {\n const drift: ResourceDrift = {\n kind: \"tenant\",\n missingFromCloudflare: [],\n unrecordedInState: [],\n undeployed: [],\n };\n\n const d1ById = new Map(allD1.map((d) => [d.uuid, d.name]));\n const tenants = state\n .listTenants()\n .filter((t) => t.provisioningStatus !== \"tombstoned\");\n if (tenants.length === 0) return drift;\n\n const scriptLists = new Map<string, Set<string>>();\n async function scriptsInNs(ns: string): Promise<Set<string>> {\n let set = scriptLists.get(ns);\n if (!set) {\n const list = await api.dispatchNamespaceScriptList(ns);\n set = new Set(list.map((s) => s.id));\n scriptLists.set(ns, set);\n }\n return set;\n }\n\n for (const t of tenants) {\n const logical = tenantStateKey(t.product, t.workspace);\n try {\n const ids = await scriptsInNs(t.dispatchNamespaceName);\n if (!ids.has(t.scriptName)) {\n drift.missingFromCloudflare.push({\n logicalName: logical,\n derivedName: t.scriptName,\n detail: \"dispatch_script\",\n });\n }\n } catch {\n drift.missingFromCloudflare.push({\n logicalName: logical,\n derivedName: t.dispatchNamespaceName,\n detail: \"dispatch_namespace_list_failed\",\n });\n }\n\n for (const shard of t.d1Shards ?? []) {\n if (!d1ById.has(shard.cfId)) {\n drift.missingFromCloudflare.push({\n logicalName: logical,\n derivedName: shard.derivedName,\n cfId: shard.cfId,\n detail: `d1:${shard.role}`,\n });\n }\n }\n }\n\n return drift;\n}\n","import { loadConfig, getWorkers } from \"../../core/config/loader.js\";\nimport { cloudflareAccountIdFromEnv } from \"../../core/cloudflareEnv.js\";\nimport { namingFromConfig } from \"../../core/config/namingFromConfig.js\";\nimport { StateManager } from \"../../core/state/StateManager.js\";\nimport { stackNameForConfig } from \"../../core/state/stackName.js\";\nimport { CFApiClient } from \"../../core/api/CFApiClient.js\";\nimport { dispatchNamespaceDrift } from \"../../features/dispatch-namespace/index.js\";\nimport {\n dnsRecordDrift,\n type DnsRecordsByZone,\n} from \"../../features/dns-records/index.js\";\nimport { getDispatchNamespaces, getDnsRecords, getLogpushJobs } from \"../../types.js\";\nimport { logpushJobDrift } from \"../../features/logpush-job/index.js\";\nimport type {\n DriftReport,\n ResourceDrift,\n} from \"../../core/drift/drift.types.js\";\nimport { reportHasDrift } from \"../../core/drift/drift.types.js\";\nimport { tenantDrift } from \"../../core/drift/tenantDrift.js\";\nimport { workerRoutesDrift } from \"../../features/worker-route/index.js\";\nimport { workersDrift } from \"../../features/workers/index.js\";\nimport { resourceModules } from \"../../core/registry/registry.js\";\nimport { fetchStackImports } from \"../../core/imports/fetchStackImports.js\";\nimport { mergeWorkerConfigForResourcePick } from \"../../core/config/resolver.js\";\n\n/**\n * Compute a read-only drift report for the given env.\n *\n * Compares Tamer state (D1 `tamer-state-{env}`) against the Cloudflare API\n * and the resources declared in `tamer.config.ts`. Reports three categories:\n *\n * - `missingFromCloudflare` — tracked in state but the CF resource is gone.\n * - `unrecordedInState` — exists on CF and matches a declared resource, but\n * no state entry tracks it (e.g. created out-of-band; run `tamer sync`).\n * - `undeployed` — declared in this stack's config, present in neither\n * state nor CF (run `tamer apply`).\n *\n * Pure: never writes to state. Returns the report so callers can choose how to\n * render or consume it.\n */\nexport async function computeDriftReport(options: {\n env?: string;\n configPath?: string;\n}): Promise<DriftReport> {\n const env = options.env ?? \"local\";\n const configPath = options.configPath;\n const baseDir = process.cwd();\n\n const config = await loadConfig(configPath, { env });\n const accountId = 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 naming = namingFromConfig(config);\n const state = new StateManager(\n config.tenant.id,\n env,\n stackNameForConfig(config),\n );\n await state.hydrate(api);\n // Tolerant pre-fetch keeps drift accurate when worker `tamerRoutes`\n // depend on sibling-stack outputs (otherwise the placeholder pattern\n // would never match anything CF returned).\n const imports = await fetchStackImports(api, config, env).catch(() => ({}));\n\n async function safeList<T>(\n label: string,\n fn: () => Promise<T[]>,\n ): Promise<T[]> {\n try {\n return await fn();\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[drift] skipping ${label}: ${msg}`);\n return [];\n }\n }\n\n const lists = await Promise.all(\n resourceModules.map((m) =>\n safeList(`${m.label} list`, () => m.fetchAll(api)),\n ),\n );\n\n const allDispatch =\n getDispatchNamespaces(config).length > 0\n ? await safeList(\"dispatch namespaces\", () =>\n api.dispatchNamespaceListAll(),\n )\n : [];\n\n const allLogpushJobs =\n getLogpushJobs(config).length > 0 && env !== \"local\"\n ? await safeList(\"logpush jobs\", () => api.logpushAccountJobsList())\n : [];\n\n const workers = await getWorkers(config, baseDir);\n\n const aggregated = new Map<string, ResourceDrift>();\n function merge(d: ResourceDrift): void {\n const existing = aggregated.get(d.kind);\n if (!existing) {\n aggregated.set(d.kind, d);\n return;\n }\n existing.missingFromCloudflare.push(...d.missingFromCloudflare);\n existing.unrecordedInState.push(...d.unrecordedInState);\n existing.undeployed.push(...d.undeployed);\n }\n\n for (const [workerKey, workerConfig] of workers) {\n const mergedWorker = mergeWorkerConfigForResourcePick(\n config,\n workerKey,\n workerConfig,\n env,\n accountId,\n naming,\n state,\n { referencesMode: \"tolerant\", imports },\n );\n resourceModules.forEach((mod, i) => {\n const resources = mod.pickResources(mergedWorker);\n if (resources.length === 0) return;\n merge(\n mod.drift({\n resources,\n all: lists[i],\n tenant: config.tenant,\n env,\n api,\n state,\n naming,\n config,\n baseDir,\n }),\n );\n });\n }\n\n const dispatchResources = getDispatchNamespaces(config);\n if (dispatchResources.length > 0) {\n merge(\n dispatchNamespaceDrift(\n allDispatch,\n dispatchResources,\n env,\n config.tenant,\n state,\n ),\n );\n }\n\n const dnsResources = getDnsRecords(config);\n if (dnsResources.length > 0 && env !== \"local\") {\n const byZone: DnsRecordsByZone = new Map();\n const zones = Array.from(new Set(dnsResources.map((r) => r.zoneId)));\n for (const zoneId of zones) {\n const live = await safeList(`dns records (zone ${zoneId})`, () =>\n api.zoneDnsRecordListAll(zoneId),\n );\n byZone.set(zoneId, live);\n }\n merge(dnsRecordDrift(byZone, dnsResources, config.tenant, env, state));\n }\n\n const logpushResources = getLogpushJobs(config);\n if (logpushResources.length > 0 && env !== \"local\") {\n merge(\n logpushJobDrift(\n allLogpushJobs,\n logpushResources,\n env,\n config.tenant,\n state,\n ),\n );\n }\n\n if (state.listTenants().length > 0) {\n const allD1Idx = resourceModules.findIndex((m) => m.kind === \"d1\");\n const allD1 =\n allD1Idx >= 0\n ? (lists[allD1Idx] as Array<{ uuid: string; name: string }>)\n : [];\n merge(await tenantDrift(state, api, allD1));\n }\n\n const workerRouteReport = await workerRoutesDrift(\n env,\n config,\n baseDir,\n accountId,\n naming,\n state,\n api,\n { imports },\n );\n if (workerRouteReport) merge(workerRouteReport);\n\n const workerScriptReport = await workersDrift(\n env,\n config,\n baseDir,\n accountId,\n naming,\n state,\n api,\n { imports },\n );\n if (workerScriptReport) merge(workerScriptReport);\n\n const dedupedResources = Array.from(aggregated.values()).map((d) => ({\n ...d,\n missingFromCloudflare: dedupe(d.missingFromCloudflare),\n unrecordedInState: dedupe(d.unrecordedInState),\n undeployed: dedupe(d.undeployed),\n }));\n\n return {\n tenantId: config.tenant.id,\n env,\n generatedAt: new Date().toISOString(),\n resources: dedupedResources,\n hasDrift: reportHasDrift(dedupedResources),\n };\n}\n\nfunction dedupe<T extends { logicalName: string; derivedName: string }>(\n list: T[],\n): T[] {\n const seen = new Set<string>();\n const out: T[] = [];\n for (const item of list) {\n const key = `${item.logicalName}::${item.derivedName}`;\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(item);\n }\n return out;\n}\n\n/**\n * CLI entry point. Prints a human report (or JSON when `--json`) and sets a\n * non-zero process exit code when drift is found.\n */\nexport async function runDrift(options: {\n env?: string;\n configPath?: string;\n json?: boolean;\n}): Promise<number> {\n const report = await computeDriftReport({\n env: options.env,\n configPath: options.configPath,\n });\n\n if (options.json) {\n console.log(JSON.stringify(report, null, 2));\n } else {\n printHumanReport(report);\n }\n\n return report.hasDrift ? 1 : 0;\n}\n\nfunction printHumanReport(report: DriftReport): void {\n console.log(\n `\\nDrift report — tenant ${report.tenantId}, env ${report.env}\\n`,\n );\n if (report.resources.length === 0) {\n console.log(\" (no managed resource kinds in this config)\\n\");\n return;\n }\n for (const d of report.resources) {\n const total =\n d.missingFromCloudflare.length +\n d.unrecordedInState.length +\n d.undeployed.length;\n console.log(`${labelFor(d.kind)} (${total} drift):`);\n if (total === 0) {\n console.log(\" ok\");\n continue;\n }\n if (d.missingFromCloudflare.length) {\n console.log(\" missing from Cloudflare (state references gone):\");\n for (const e of d.missingFromCloudflare) {\n console.log(` - ${e.logicalName} -> ${e.derivedName}${suffix(e.cfId)}`);\n }\n }\n if (d.unrecordedInState.length) {\n console.log(\" unrecorded in state (run `tamer sync`):\");\n for (const e of d.unrecordedInState) {\n console.log(` - ${e.logicalName} -> ${e.derivedName}${suffix(e.cfId)}`);\n }\n }\n if (d.undeployed.length) {\n console.log(\" undeployed (run `tamer apply`):\");\n for (const e of d.undeployed) {\n console.log(` - ${e.logicalName} -> ${e.derivedName}`);\n }\n }\n }\n console.log(report.hasDrift ? \"\\nDrift detected.\\n\" : \"\\nNo drift.\\n\");\n}\n\nconst REGISTRY_LABELS: Record<string, string> = Object.fromEntries(\n resourceModules.map((m) => [m.kind, m.label]),\n);\n\nfunction labelFor(kind: ResourceDrift[\"kind\"]): string {\n if (REGISTRY_LABELS[kind]) return REGISTRY_LABELS[kind];\n switch (kind) {\n case \"dispatch_namespace\":\n return \"Dispatch namespaces\";\n case \"logpush_job\":\n return \"Logpush jobs\";\n case \"dns_record\":\n return \"DNS records\";\n case \"tenant\":\n return \"Workspace tenants\";\n case \"worker_route\":\n return \"HTTP routes (Workers Routes API)\";\n case \"worker_script\":\n return \"Worker scripts\";\n default:\n return kind;\n }\n}\n\nfunction suffix(cfId?: string): string {\n return cfId ? ` [${cfId}]` : \"\";\n}\n"],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ import { n as loadConfig } from "./loader-DP7yXqT6.mjs";
2
+ import { n as cloudflareAccountIdFromEnv, t as CFApiClient } from "./CFApiClient-DhbyyV71.mjs";
3
+ import { f as stackNameForConfig, t as StateManager } from "./StateManager-DTqtLLVX.mjs";
4
+
5
+ //#region src/cli/commands/events.ts
6
+ function formatDuration(startedAt, completedAt) {
7
+ if (!completedAt) return void 0;
8
+ const a = Date.parse(startedAt);
9
+ const b = Date.parse(completedAt);
10
+ if (!Number.isFinite(a) || !Number.isFinite(b) || b < a) return void 0;
11
+ const ms = b - a;
12
+ if (ms < 1e3) return `${ms}ms`;
13
+ return `${(ms / 1e3).toFixed(1)}s`;
14
+ }
15
+ function printOp(label, op) {
16
+ const dur = formatDuration(op.startedAt, op.completedAt);
17
+ console.log(` [${label}] ${op.command}`);
18
+ console.log(` started: ${op.startedAt}`);
19
+ if (op.completedAt) console.log(` completed: ${op.completedAt}`);
20
+ if (dur) console.log(` duration: ${dur}`);
21
+ if (op.detail) console.log(` detail: ${op.detail}`);
22
+ if (op.errorMessage) console.log(` error: ${op.errorMessage}`);
23
+ console.log();
24
+ }
25
+ /**
26
+ * Read-only: print recent completed stack operations from D1 state
27
+ * (`operationHistory`) plus any in-progress marker (`lastOperation`).
28
+ */
29
+ async function runEvents(options) {
30
+ const env = options.env ?? "local";
31
+ const config = await loadConfig(options.configPath, { env });
32
+ const accountId = config.account_id ?? cloudflareAccountIdFromEnv();
33
+ if (!accountId) throw new Error("account_id required in config or CLOUDFLARE_ACCOUNT_ID env var");
34
+ const stackName = stackNameForConfig(config);
35
+ const api = new CFApiClient(accountId);
36
+ const state = new StateManager(config.tenant.id, env, stackName);
37
+ await state.hydrate(api);
38
+ const last = state.getLastOperation();
39
+ const inProgress = last?.status === "in_progress" ? last : void 0;
40
+ let history = state.getOperationHistory();
41
+ if (options.limit !== void 0) history = history.slice(0, options.limit);
42
+ if (options.json) {
43
+ const out = {
44
+ stackName,
45
+ env,
46
+ tenantId: config.tenant.id,
47
+ inProgress: inProgress ?? null,
48
+ history
49
+ };
50
+ console.log(JSON.stringify(out, null, 2));
51
+ return;
52
+ }
53
+ console.log(`\nStack events — ${config.tenant.name} (${config.tenant.slug}) — stack "${stackName}" env ${env}\n`);
54
+ if (inProgress) {
55
+ console.log("In progress:\n");
56
+ printOp("in_progress", inProgress);
57
+ }
58
+ if (history.length === 0) {
59
+ console.log(inProgress ? "(no completed operations in history yet)\n" : "No completed operations recorded in state yet.\n");
60
+ return;
61
+ }
62
+ console.log("Recent completed operations (newest first):\n");
63
+ for (const op of history) printOp(op.status, op);
64
+ }
65
+
66
+ //#endregion
67
+ export { runEvents };
68
+ //# sourceMappingURL=events-BSwGdkGj.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-BSwGdkGj.mjs","names":[],"sources":["../src/cli/commands/events.ts"],"sourcesContent":["import { loadConfig } from \"../../core/config/loader.js\";\nimport { cloudflareAccountIdFromEnv } from \"../../core/cloudflareEnv.js\";\nimport { StateManager } from \"../../core/state/StateManager.js\";\nimport { stackNameForConfig } from \"../../core/state/stackName.js\";\nimport { CFApiClient } from \"../../core/api/CFApiClient.js\";\nimport type { CfiOperationRecord } from \"../../types.js\";\n\nfunction formatDuration(\n startedAt: string,\n completedAt: string | undefined,\n): string | undefined {\n if (!completedAt) return undefined;\n const a = Date.parse(startedAt);\n const b = Date.parse(completedAt);\n if (!Number.isFinite(a) || !Number.isFinite(b) || b < a) return undefined;\n const ms = b - a;\n if (ms < 1000) return `${ms}ms`;\n return `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction printOp(label: string, op: CfiOperationRecord): void {\n const dur = formatDuration(op.startedAt, op.completedAt);\n console.log(` [${label}] ${op.command}`);\n console.log(` started: ${op.startedAt}`);\n if (op.completedAt) {\n console.log(` completed: ${op.completedAt}`);\n }\n if (dur) console.log(` duration: ${dur}`);\n if (op.detail) console.log(` detail: ${op.detail}`);\n if (op.errorMessage) console.log(` error: ${op.errorMessage}`);\n console.log();\n}\n\n/**\n * Read-only: print recent completed stack operations from D1 state\n * (`operationHistory`) plus any in-progress marker (`lastOperation`).\n */\nexport async function runEvents(options: {\n env?: string;\n configPath?: string;\n limit?: number;\n json?: boolean;\n}): Promise<void> {\n const env = options.env ?? \"local\";\n const config = await loadConfig(options.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 const stackName = stackNameForConfig(config);\n const api = new CFApiClient(accountId);\n const state = new StateManager(config.tenant.id, env, stackName);\n await state.hydrate(api);\n\n const last = state.getLastOperation();\n const inProgress =\n last?.status === \"in_progress\" ? last : undefined;\n let history = state.getOperationHistory();\n if (options.limit !== undefined) {\n history = history.slice(0, options.limit);\n }\n\n if (options.json) {\n const out = {\n stackName,\n env,\n tenantId: config.tenant.id,\n inProgress: inProgress ?? null,\n history,\n };\n console.log(JSON.stringify(out, null, 2));\n return;\n }\n\n console.log(\n `\\nStack events — ${config.tenant.name} (${config.tenant.slug}) — stack \"${stackName}\" env ${env}\\n`,\n );\n\n if (inProgress) {\n console.log(\"In progress:\\n\");\n printOp(\"in_progress\", inProgress);\n }\n\n if (history.length === 0) {\n console.log(\n inProgress\n ? \"(no completed operations in history yet)\\n\"\n : \"No completed operations recorded in state yet.\\n\",\n );\n return;\n }\n\n console.log(\"Recent completed operations (newest first):\\n\");\n for (const op of history) {\n printOp(op.status, op);\n }\n}\n"],"mappings":";;;;;AAOA,SAAS,eACP,WACA,aACoB;AACpB,KAAI,CAAC,YAAa,QAAO;CACzB,MAAM,IAAI,KAAK,MAAM,UAAU;CAC/B,MAAM,IAAI,KAAK,MAAM,YAAY;AACjC,KAAI,CAAC,OAAO,SAAS,EAAE,IAAI,CAAC,OAAO,SAAS,EAAE,IAAI,IAAI,EAAG,QAAO;CAChE,MAAM,KAAK,IAAI;AACf,KAAI,KAAK,IAAM,QAAO,GAAG,GAAG;AAC5B,QAAO,IAAI,KAAK,KAAM,QAAQ,EAAE,CAAC;;AAGnC,SAAS,QAAQ,OAAe,IAA8B;CAC5D,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,YAAY;AACxD,SAAQ,IAAI,MAAM,MAAM,IAAI,GAAG,UAAU;AACzC,SAAQ,IAAI,kBAAkB,GAAG,YAAY;AAC7C,KAAI,GAAG,YACL,SAAQ,IAAI,kBAAkB,GAAG,cAAc;AAEjD,KAAI,IAAK,SAAQ,IAAI,kBAAkB,MAAM;AAC7C,KAAI,GAAG,OAAQ,SAAQ,IAAI,kBAAkB,GAAG,SAAS;AACzD,KAAI,GAAG,aAAc,SAAQ,IAAI,kBAAkB,GAAG,eAAe;AACrE,SAAQ,KAAK;;;;;;AAOf,eAAsB,UAAU,SAKd;CAChB,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,SAAS,MAAM,WAAW,QAAQ,YAAY,EAAE,KAAK,CAAC;CAC5D,MAAM,YACJ,OAAO,cAAc,4BAA4B;AACnD,KAAI,CAAC,UACH,OAAM,IAAI,MACR,iEACD;CAEH,MAAM,YAAY,mBAAmB,OAAO;CAC5C,MAAM,MAAM,IAAI,YAAY,UAAU;CACtC,MAAM,QAAQ,IAAI,aAAa,OAAO,OAAO,IAAI,KAAK,UAAU;AAChE,OAAM,MAAM,QAAQ,IAAI;CAExB,MAAM,OAAO,MAAM,kBAAkB;CACrC,MAAM,aACJ,MAAM,WAAW,gBAAgB,OAAO;CAC1C,IAAI,UAAU,MAAM,qBAAqB;AACzC,KAAI,QAAQ,UAAU,OACpB,WAAU,QAAQ,MAAM,GAAG,QAAQ,MAAM;AAG3C,KAAI,QAAQ,MAAM;EAChB,MAAM,MAAM;GACV;GACA;GACA,UAAU,OAAO,OAAO;GACxB,YAAY,cAAc;GAC1B;GACD;AACD,UAAQ,IAAI,KAAK,UAAU,KAAK,MAAM,EAAE,CAAC;AACzC;;AAGF,SAAQ,IACN,oBAAoB,OAAO,OAAO,KAAK,IAAI,OAAO,OAAO,KAAK,aAAa,UAAU,QAAQ,IAAI,IAClG;AAED,KAAI,YAAY;AACd,UAAQ,IAAI,iBAAiB;AAC7B,UAAQ,eAAe,WAAW;;AAGpC,KAAI,QAAQ,WAAW,GAAG;AACxB,UAAQ,IACN,aACI,+CACA,mDACL;AACD;;AAGF,SAAQ,IAAI,gDAAgD;AAC5D,MAAK,MAAM,MAAM,QACf,SAAQ,GAAG,QAAQ,GAAG"}