@reddoorla/maintenance 0.82.0 → 0.83.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 (129) hide show
  1. package/dist/airtable-5ZPU3MT4.js +9 -0
  2. package/dist/{announce-7QHQGJPV.js → announce-4H64L2Z6.js} +4 -4
  3. package/dist/{blux-EATHOHKK.js → blux-ZGDSHI3U.js} +5 -5
  4. package/dist/{bump-deps-ZREAMK7M.js → bump-deps-2OL6PGTM.js} +5 -5
  5. package/dist/{chunk-OIDDJLS7.js → chunk-2WN4SNBQ.js} +2 -2
  6. package/dist/{chunk-HVOOCK6L.js → chunk-4DHQCN5H.js} +2 -1
  7. package/dist/{chunk-5E4L7WKE.js → chunk-5P2HVNLZ.js} +3 -3
  8. package/dist/{chunk-ZXRRKKMR.js → chunk-6V7565O2.js} +8 -8
  9. package/dist/{chunk-YRM6EED7.js → chunk-CLWNAMUX.js} +28 -2
  10. package/dist/chunk-CLWNAMUX.js.map +1 -0
  11. package/dist/{chunk-VXXF4Y25.js → chunk-FSRJAF2Q.js} +60 -3
  12. package/dist/chunk-FSRJAF2Q.js.map +1 -0
  13. package/dist/{chunk-B5ZUQZFL.js → chunk-ICXV25N7.js} +2 -2
  14. package/dist/{chunk-UH5YPYS4.js → chunk-JVWJJ36K.js} +5 -4
  15. package/dist/chunk-JVWJJ36K.js.map +1 -0
  16. package/dist/{chunk-4PW6ISX7.js → chunk-KV7DIRRO.js} +3 -3
  17. package/dist/{chunk-ODC4PXZL.js → chunk-MQUOUVQP.js} +2 -2
  18. package/dist/{chunk-LMCO7CYQ.js → chunk-OQFOMKBP.js} +2 -2
  19. package/dist/{chunk-FFI7P74K.js → chunk-PBCGXXG6.js} +2 -2
  20. package/dist/{chunk-T4FQ6TZT.js → chunk-QRDB4NLB.js} +6 -6
  21. package/dist/{chunk-K5JZ5KLO.js → chunk-R4AB6GCT.js} +3 -3
  22. package/dist/chunk-R53VL3RI.js +32 -0
  23. package/dist/chunk-R53VL3RI.js.map +1 -0
  24. package/dist/{chunk-FX7RAMGC.js → chunk-R6FPFMOS.js} +3 -3
  25. package/dist/{chunk-MGJ72BA4.js → chunk-RJB3QUSP.js} +2 -2
  26. package/dist/{chunk-ATJ6ZA2V.js → chunk-SDUJIANJ.js} +2 -2
  27. package/dist/{chunk-7ADQL5Y3.js → chunk-THEFTZ7F.js} +2 -2
  28. package/dist/{chunk-NT4WIP7I.js → chunk-UYCAF77E.js} +2 -2
  29. package/dist/chunk-VNB5XBQ4.js +699 -0
  30. package/dist/chunk-VNB5XBQ4.js.map +1 -0
  31. package/dist/{chunk-YAMRVXOD.js → chunk-VRWUL4TR.js} +2 -2
  32. package/dist/{chunk-2BHSGLWR.js → chunk-XDCRVYFL.js} +3 -3
  33. package/dist/chunk-XDCRVYFL.js.map +1 -0
  34. package/dist/{chunk-4NAOR44V.js → chunk-ZMIPG6TR.js} +2 -2
  35. package/dist/cli/bin.js +51 -23
  36. package/dist/cli/bin.js.map +1 -1
  37. package/dist/cli/commands/audit.d.ts +1 -1
  38. package/dist/cli/commands/audit.js +10 -10
  39. package/dist/{convert-to-pnpm-UT2R6EOW.js → convert-to-pnpm-KCXOYOHW.js} +5 -5
  40. package/dist/{digest-CLFUWW62.js → digest-WGEPAWMP.js} +12 -7
  41. package/dist/digest-WGEPAWMP.js.map +1 -0
  42. package/dist/{ensure-site-LLPOXLWP.js → ensure-site-HYAIVZY2.js} +2 -2
  43. package/dist/{forms-notify-target-PN6P3WPW.js → forms-notify-target-HSL4ZMT5.js} +2 -2
  44. package/dist/{github-signals-HTH7474O.js → github-signals-I7XACMCE.js} +6 -6
  45. package/dist/{header-image-WMHZFU7B.js → header-image-ADYPTGVY.js} +2 -2
  46. package/dist/{health-endpoint-4E53ZMLE.js → health-endpoint-JNCUILHO.js} +6 -6
  47. package/dist/index.d.ts +65 -3
  48. package/dist/index.js +32 -31
  49. package/dist/index.js.map +1 -1
  50. package/dist/{init-3ZJ7Z5NG.js → init-FLGLJCMJ.js} +13 -13
  51. package/dist/{launch-FRCDOB56.js → launch-AVKH2JVF.js} +10 -10
  52. package/dist/{lighthouse-airtable-I46QCEIS.js → lighthouse-airtable-CHANWVO6.js} +3 -3
  53. package/dist/{onboard-45NPDMMW.js → onboard-HF4FTJDE.js} +5 -5
  54. package/dist/{orchestrate-34JYNIXT.js → orchestrate-5FMNYJEA.js} +3 -3
  55. package/dist/{preflight-WVD32COX.js → preflight-HTWIX3MN.js} +4 -4
  56. package/dist/prismic-ci-BH225Q4L.js +363 -0
  57. package/dist/prismic-ci-BH225Q4L.js.map +1 -0
  58. package/dist/prismic-models-HXCAM4ZL.js +1020 -0
  59. package/dist/prismic-models-HXCAM4ZL.js.map +1 -0
  60. package/dist/{protection-audit-2UR45SOC.js → protection-audit-64PLLBLI.js} +3 -3
  61. package/dist/recipes/sync-configs.d.ts +1 -1
  62. package/dist/{renovate-dispatch-LOOCNZTW.js → renovate-dispatch-HBZG2UEU.js} +4 -4
  63. package/dist/{report-CLF2753O.js → report-LV7NY5W3.js} +20 -13
  64. package/dist/report-LV7NY5W3.js.map +1 -0
  65. package/dist/{self-updating-5TJ2NB26.js → self-updating-4Q65OVXY.js} +6 -6
  66. package/dist/{selftest-BTCZLCTL.js → selftest-K5XZKVTE.js} +7 -7
  67. package/dist/{smoke-suite-GHKXMKIR.js → smoke-suite-J3LL3JZJ.js} +6 -6
  68. package/dist/{submissions-KUXLKCSQ.js → submissions-GMBUP4LV.js} +2 -2
  69. package/dist/{svelte-codemods-YTCTLK7G.js → svelte-codemods-JTMX4BSX.js} +4 -4
  70. package/dist/{sync-configs-KSSQZU5L.js → sync-configs-5DXSL64Q.js} +4 -4
  71. package/dist/{types-DnEL3NRn.d.ts → types-OIQZ5hWN.d.ts} +1 -1
  72. package/dist/{upgrade-UDAPY3SN.js → upgrade-HCR25P25.js} +5 -5
  73. package/dist/{websites-JXG3WWAU.js → websites-VITQ3WXN.js} +6 -2
  74. package/dist/{write-audits-to-airtable-7D3LV4KP.js → write-audits-to-airtable-3SOXLKJK.js} +4 -4
  75. package/package.json +1 -1
  76. package/dist/airtable-ZDHLKK5K.js +0 -9
  77. package/dist/chunk-2BHSGLWR.js.map +0 -1
  78. package/dist/chunk-KRDLHAKK.js +0 -17
  79. package/dist/chunk-KRDLHAKK.js.map +0 -1
  80. package/dist/chunk-UH5YPYS4.js.map +0 -1
  81. package/dist/chunk-VXXF4Y25.js.map +0 -1
  82. package/dist/chunk-YRM6EED7.js.map +0 -1
  83. package/dist/digest-CLFUWW62.js.map +0 -1
  84. package/dist/report-CLF2753O.js.map +0 -1
  85. /package/dist/{airtable-ZDHLKK5K.js.map → airtable-5ZPU3MT4.js.map} +0 -0
  86. /package/dist/{announce-7QHQGJPV.js.map → announce-4H64L2Z6.js.map} +0 -0
  87. /package/dist/{blux-EATHOHKK.js.map → blux-ZGDSHI3U.js.map} +0 -0
  88. /package/dist/{bump-deps-ZREAMK7M.js.map → bump-deps-2OL6PGTM.js.map} +0 -0
  89. /package/dist/{chunk-OIDDJLS7.js.map → chunk-2WN4SNBQ.js.map} +0 -0
  90. /package/dist/{chunk-HVOOCK6L.js.map → chunk-4DHQCN5H.js.map} +0 -0
  91. /package/dist/{chunk-5E4L7WKE.js.map → chunk-5P2HVNLZ.js.map} +0 -0
  92. /package/dist/{chunk-ZXRRKKMR.js.map → chunk-6V7565O2.js.map} +0 -0
  93. /package/dist/{chunk-B5ZUQZFL.js.map → chunk-ICXV25N7.js.map} +0 -0
  94. /package/dist/{chunk-4PW6ISX7.js.map → chunk-KV7DIRRO.js.map} +0 -0
  95. /package/dist/{chunk-ODC4PXZL.js.map → chunk-MQUOUVQP.js.map} +0 -0
  96. /package/dist/{chunk-LMCO7CYQ.js.map → chunk-OQFOMKBP.js.map} +0 -0
  97. /package/dist/{chunk-FFI7P74K.js.map → chunk-PBCGXXG6.js.map} +0 -0
  98. /package/dist/{chunk-T4FQ6TZT.js.map → chunk-QRDB4NLB.js.map} +0 -0
  99. /package/dist/{chunk-K5JZ5KLO.js.map → chunk-R4AB6GCT.js.map} +0 -0
  100. /package/dist/{chunk-FX7RAMGC.js.map → chunk-R6FPFMOS.js.map} +0 -0
  101. /package/dist/{chunk-MGJ72BA4.js.map → chunk-RJB3QUSP.js.map} +0 -0
  102. /package/dist/{chunk-ATJ6ZA2V.js.map → chunk-SDUJIANJ.js.map} +0 -0
  103. /package/dist/{chunk-7ADQL5Y3.js.map → chunk-THEFTZ7F.js.map} +0 -0
  104. /package/dist/{chunk-NT4WIP7I.js.map → chunk-UYCAF77E.js.map} +0 -0
  105. /package/dist/{chunk-YAMRVXOD.js.map → chunk-VRWUL4TR.js.map} +0 -0
  106. /package/dist/{chunk-4NAOR44V.js.map → chunk-ZMIPG6TR.js.map} +0 -0
  107. /package/dist/{convert-to-pnpm-UT2R6EOW.js.map → convert-to-pnpm-KCXOYOHW.js.map} +0 -0
  108. /package/dist/{ensure-site-LLPOXLWP.js.map → ensure-site-HYAIVZY2.js.map} +0 -0
  109. /package/dist/{forms-notify-target-PN6P3WPW.js.map → forms-notify-target-HSL4ZMT5.js.map} +0 -0
  110. /package/dist/{github-signals-HTH7474O.js.map → github-signals-I7XACMCE.js.map} +0 -0
  111. /package/dist/{header-image-WMHZFU7B.js.map → header-image-ADYPTGVY.js.map} +0 -0
  112. /package/dist/{health-endpoint-4E53ZMLE.js.map → health-endpoint-JNCUILHO.js.map} +0 -0
  113. /package/dist/{init-3ZJ7Z5NG.js.map → init-FLGLJCMJ.js.map} +0 -0
  114. /package/dist/{launch-FRCDOB56.js.map → launch-AVKH2JVF.js.map} +0 -0
  115. /package/dist/{lighthouse-airtable-I46QCEIS.js.map → lighthouse-airtable-CHANWVO6.js.map} +0 -0
  116. /package/dist/{onboard-45NPDMMW.js.map → onboard-HF4FTJDE.js.map} +0 -0
  117. /package/dist/{orchestrate-34JYNIXT.js.map → orchestrate-5FMNYJEA.js.map} +0 -0
  118. /package/dist/{preflight-WVD32COX.js.map → preflight-HTWIX3MN.js.map} +0 -0
  119. /package/dist/{protection-audit-2UR45SOC.js.map → protection-audit-64PLLBLI.js.map} +0 -0
  120. /package/dist/{renovate-dispatch-LOOCNZTW.js.map → renovate-dispatch-HBZG2UEU.js.map} +0 -0
  121. /package/dist/{self-updating-5TJ2NB26.js.map → self-updating-4Q65OVXY.js.map} +0 -0
  122. /package/dist/{selftest-BTCZLCTL.js.map → selftest-K5XZKVTE.js.map} +0 -0
  123. /package/dist/{smoke-suite-GHKXMKIR.js.map → smoke-suite-J3LL3JZJ.js.map} +0 -0
  124. /package/dist/{submissions-KUXLKCSQ.js.map → submissions-GMBUP4LV.js.map} +0 -0
  125. /package/dist/{svelte-codemods-YTCTLK7G.js.map → svelte-codemods-JTMX4BSX.js.map} +0 -0
  126. /package/dist/{sync-configs-KSSQZU5L.js.map → sync-configs-5DXSL64Q.js.map} +0 -0
  127. /package/dist/{upgrade-UDAPY3SN.js.map → upgrade-HCR25P25.js.map} +0 -0
  128. /package/dist/{websites-JXG3WWAU.js.map → websites-VITQ3WXN.js.map} +0 -0
  129. /package/dist/{write-audits-to-airtable-7D3LV4KP.js.map → write-audits-to-airtable-3SOXLKJK.js.map} +0 -0
@@ -0,0 +1,1020 @@
1
+ import {
2
+ describeDiff,
3
+ diffModels,
4
+ localModels,
5
+ noWorkingTreeFailure,
6
+ prismicTokenEnvName,
7
+ pushModels,
8
+ readPrismicConfig,
9
+ remoteModels,
10
+ resolvePrismicToken,
11
+ sendModel,
12
+ writeModelFile
13
+ } from "./chunk-VNB5XBQ4.js";
14
+ import {
15
+ appendSkipNotice,
16
+ fleetWorkdir,
17
+ prepareFleetSites
18
+ } from "./chunk-ZMIPG6TR.js";
19
+ import {
20
+ resolveSites
21
+ } from "./chunk-MQUOUVQP.js";
22
+ import {
23
+ PRETTIER_FLAG_NOTE,
24
+ formatWithPrettier
25
+ } from "./chunk-R53VL3RI.js";
26
+ import "./chunk-FDIFTIXC.js";
27
+ import {
28
+ makeSpawn
29
+ } from "./chunk-4DHQCN5H.js";
30
+ import {
31
+ siteLabel
32
+ } from "./chunk-XXTZBPUY.js";
33
+ import "./chunk-XTK5VIQB.js";
34
+ import "./chunk-U5SZ4FOP.js";
35
+ import {
36
+ siteSlug
37
+ } from "./chunk-CLWNAMUX.js";
38
+
39
+ // src/cli/commands/prismic-models.ts
40
+ import { readdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
41
+ import { basename, join, resolve } from "path";
42
+
43
+ // src/cli/commands/prismic-models-report.ts
44
+ function isClean(diff) {
45
+ return diff.toCreate.length === 0 && diff.toUpdate.length === 0 && diff.remoteOnly.length === 0;
46
+ }
47
+ var key = (e) => `${e.kind}:${e.id}`;
48
+ var keysOf = (xs) => new Set(xs.map(key));
49
+ var minus = (a, b) => [...a].filter((k) => !b.has(k)).sort();
50
+ var intersect = (a, b) => [...a].filter((k) => b.has(k)).sort();
51
+ function reconcileReport(diff, opts) {
52
+ const out = [];
53
+ const work = keysOf([...diff.toCreate, ...diff.toUpdate.map((u) => u.local)]);
54
+ if (opts.failed !== void 0) {
55
+ for (const k of minus(keysOf(opts.failed), work))
56
+ out.push(` reported as failed, but not a model this diff is pushing: ${k}`);
57
+ }
58
+ const report = opts.report;
59
+ if (report === void 0) return out;
60
+ if (report.mode === "apply" !== opts.apply) {
61
+ out.push(
62
+ ` mode: rendering as ${opts.apply ? "an APPLY" : "a DRY RUN"}, but the push run recorded mode "${report.mode}"`
63
+ );
64
+ }
65
+ const diffRemote = keysOf(diff.remoteOnly);
66
+ const reportRemote = keysOf(report.remoteOnlyReported);
67
+ for (const k of minus(diffRemote, reportRemote))
68
+ out.push(` remote-only per the comparison, absent from the push report: ${k}`);
69
+ for (const k of minus(reportRemote, diffRemote))
70
+ out.push(` remote-only per the push report, absent from the comparison: ${k}`);
71
+ if (opts.failed !== void 0) {
72
+ const callerFailed = keysOf(opts.failed);
73
+ const reportFailed = keysOf(report.failed);
74
+ for (const k of minus(callerFailed, reportFailed))
75
+ out.push(` failed per the caller's list, absent from the push report: ${k}`);
76
+ for (const k of minus(reportFailed, callerFailed))
77
+ out.push(` failed per the push report, absent from the caller's list: ${k}`);
78
+ }
79
+ const sent = keysOf(report.sent);
80
+ const failed = keysOf(report.failed);
81
+ const accounted = /* @__PURE__ */ new Set([...sent, ...failed]);
82
+ for (const k of minus(work, accounted))
83
+ out.push(` in this diff's work list, but the push run neither sent nor failed it: ${k}`);
84
+ for (const k of minus(accounted, work))
85
+ out.push(` sent or failed by the push run, but not in this diff's work list: ${k}`);
86
+ for (const k of intersect(sent, failed))
87
+ out.push(` recorded BOTH sent and failed by the push run: ${k}`);
88
+ return out;
89
+ }
90
+ var isDestructive = (line) => line.startsWith("- ") || line === "~ (model) repeatable";
91
+ function renderModelReport(repositoryName, diff, opts) {
92
+ const head = [`Prismic models \u2014 repository: ${repositoryName}`];
93
+ const disagreements = reconcileReport(diff, opts);
94
+ if (disagreements.length > 0) {
95
+ head.push(
96
+ `\u26A0 INCONSISTENT \u2014 the inputs to this report disagree with each other, so every figure in it is unreliable: they all came from the same inputs. Something upstream is broken (a diff and a push report from different runs, a stale diff, or a mismatched pair). Re-run; do not act on this.`,
97
+ ...disagreements
98
+ );
99
+ }
100
+ if (isClean(diff) && disagreements.length === 0) {
101
+ head.push(`${diff.unchanged.length} model(s) match Prismic \u2014 nothing to push.`);
102
+ if (diff.unchanged.length === 0) {
103
+ head.push(
104
+ `\u26A0 NOTHING WAS FOUND ON EITHER SIDE \u2014 the repo declared no models and Prismic holds none. For a live site that is a misconfiguration (a wrong repositoryName, or a slice library path that no longer resolves), not a clean run.`
105
+ );
106
+ }
107
+ return head.join("\n");
108
+ }
109
+ const body = [];
110
+ let destructive = 0;
111
+ for (const entry of diff.toCreate) {
112
+ body.push(`NEW ${entry.kind} ${entry.id} (${entry.path})`);
113
+ }
114
+ for (const { local, remote } of diff.toUpdate) {
115
+ body.push(`CHANGED ${local.kind} ${local.id} (${local.path})`);
116
+ for (const l of describeDiff(local.model, remote.model)) {
117
+ if (isDestructive(l)) destructive += 1;
118
+ body.push(` ${l}`);
119
+ }
120
+ }
121
+ const matchedNothing = diff.remoteOnly.length > 0 && diff.toCreate.length + diff.toUpdate.length + diff.unchanged.length === 0;
122
+ if (diff.remoteOnly.length > 0) {
123
+ if (body.length > 0) body.push("");
124
+ body.push(
125
+ `REMOTE-ONLY \u2014 present in Prismic, absent from this repo. These are reported and never deleted by CI; pull them down with \`reddoor-maint prismic-models --pull\` or delete them in the Prismic dashboard.`
126
+ );
127
+ for (const entry of diff.remoteOnly) body.push(` ${entry.kind} ${entry.id}`);
128
+ }
129
+ const failures = opts.failed ?? opts.report?.failed ?? [];
130
+ if (failures.length > 0) {
131
+ if (body.length > 0) body.push("");
132
+ for (const f of failures) body.push(`FAILED ${f.kind} ${f.id}: ${f.error}`);
133
+ const auth = failures.filter((f) => f.status === 401 || f.status === 403).length;
134
+ if (auth > 0) {
135
+ body.push(
136
+ `\u26A0 ${auth} failure(s) came back 401/403 \u2014 that is the WRITE TOKEN, not the models. Fix the secret and re-run; a model Prismic actually rejected would be a 422.`
137
+ );
138
+ }
139
+ }
140
+ const n = diff.toCreate.length + diff.toUpdate.length;
141
+ const pushed = opts.report !== void 0 && opts.report.mode === "apply" ? opts.report.sent.length : n - failures.length;
142
+ const verdict = opts.apply ? `${pushed}/${n} model(s) pushed. ${diff.unchanged.length} already matched.` : `DRY RUN \u2014 nothing was sent. ${n} model(s) would be pushed; ${diff.unchanged.length} already match.`;
143
+ const warnings = [];
144
+ if (matchedNothing) {
145
+ warnings.push(
146
+ `\u26A0 THIS CHECKOUT MATCHED NOTHING \u2014 every model below is remote-only and the repo declared none at all. That is what a broken slice-library path looks like (a renamed directory, a typo in \`libraries\`, a partial checkout), not a repo that legitimately has no models.`
147
+ );
148
+ }
149
+ if (destructive > 0) {
150
+ warnings.push(
151
+ `\u26A0 DESTRUCTIVE \u2014 ${destructive} line(s) below remove a field, variation, or tab from a live model, or flip \`repeatable\`. Prismic drops the document data for a field the model no longer declares, with HTTP 200 and no warning. Read every \`-\` line before merging.`
152
+ );
153
+ }
154
+ return [...head, verdict, ...warnings, ...body.length > 0 ? ["", ...body] : []].join("\n");
155
+ }
156
+
157
+ // src/cli/commands/prismic-models.ts
158
+ var defaultDeps = () => ({
159
+ remoteModels: (repo, token) => remoteModels(repo, token),
160
+ sendModel: (repo, token, entry, action) => sendModel(repo, token, entry, action),
161
+ env: process.env,
162
+ spawn: makeSpawn(),
163
+ // Imported HERE rather than at the top of the file: the Airtable client is a
164
+ // devDependency of this package, and a consuming fleet site running the in-repo
165
+ // check in its own CI has no `airtable` installed. A static import would make
166
+ // every one of those runs fail on module load.
167
+ openVerdictSink: async () => {
168
+ const { openBase, readAirtableConfig } = await import("./client-EPVO3FEH.js");
169
+ const { listWebsites, updatePrismicModels } = await import("./websites-VITQ3WXN.js");
170
+ const base = openBase(readAirtableConfig());
171
+ const websites = await listWebsites(base);
172
+ return {
173
+ websites: websites.map((w) => ({ id: w.id, name: w.name })),
174
+ update: (recordId, models) => updatePrismicModels(base, recordId, models)
175
+ };
176
+ }
177
+ });
178
+ var describeThrown = (e) => {
179
+ try {
180
+ if (e instanceof Error && typeof e.message === "string") return e.message;
181
+ } catch {
182
+ }
183
+ try {
184
+ return String(e);
185
+ } catch {
186
+ return `<a thrown ${typeof e} that cannot be converted to a string>`;
187
+ }
188
+ };
189
+ var inputFailure = (output, status, code, repositoryName) => ({
190
+ ok: false,
191
+ failure: {
192
+ output,
193
+ code,
194
+ // `null` on EVERY one of these, so nothing downstream can read a failed read
195
+ // as a verdict about the site.
196
+ clean: null,
197
+ status,
198
+ ...repositoryName !== void 0 ? { repositoryName } : {}
199
+ }
200
+ });
201
+ async function readSiteInputs(repoRoot, deps, opts) {
202
+ let entries;
203
+ try {
204
+ entries = await readdir(repoRoot);
205
+ } catch (e) {
206
+ return inputFailure(
207
+ `cannot read this checkout at ${repoRoot}: ${describeThrown(e)}. ${opts.noEffect} \u2014 this is NOT "no Prismic in this repo". Check the path (a typo, a wrong working-directory, or a clone that failed).`,
208
+ "failed",
209
+ 1
210
+ );
211
+ }
212
+ const noTree = noWorkingTreeFailure(repoRoot, entries, opts.noEffect);
213
+ if (noTree) return inputFailure(noTree, "failed", 1);
214
+ let cfg;
215
+ try {
216
+ cfg = await readPrismicConfig(repoRoot);
217
+ } catch (e) {
218
+ return inputFailure(
219
+ `Prismic config present but unusable: ${describeThrown(e)}. ${opts.noEffect} \u2014 this is NOT "no Prismic in this repo".`,
220
+ "failed",
221
+ 1
222
+ );
223
+ }
224
+ if (!cfg) {
225
+ return inputFailure("not a Prismic site (no repositoryName) \u2014 skipped", "skipped", 0);
226
+ }
227
+ let canonicalEnv;
228
+ try {
229
+ canonicalEnv = prismicTokenEnvName(cfg.repositoryName);
230
+ } catch (e) {
231
+ return inputFailure(
232
+ `cannot work out which secret holds this repository's token: ${describeThrown(e)}`,
233
+ "failed",
234
+ 1,
235
+ cfg.repositoryName
236
+ );
237
+ }
238
+ const resolved = resolvePrismicToken(cfg.repositoryName, deps.env, {
239
+ allowGeneric: opts.allowGenericToken
240
+ });
241
+ if (!resolved) {
242
+ const names = [canonicalEnv];
243
+ if (opts.allowGenericToken) names.push("PRISMIC_WRITE_TOKEN");
244
+ return inputFailure(
245
+ `no write token for Prismic repository "${cfg.repositoryName}" \u2014 set ${names.join(" or ")}`,
246
+ "failed",
247
+ 1,
248
+ cfg.repositoryName
249
+ );
250
+ }
251
+ let local;
252
+ try {
253
+ local = await localModels(repoRoot, cfg.libraries);
254
+ } catch (e) {
255
+ return inputFailure(
256
+ `could not read this repo's own models: ${describeThrown(e)}. ${opts.noEffect}.`,
257
+ "failed",
258
+ 1,
259
+ cfg.repositoryName
260
+ );
261
+ }
262
+ let remote;
263
+ try {
264
+ remote = await deps.remoteModels(cfg.repositoryName, resolved.token);
265
+ } catch (e) {
266
+ return inputFailure(
267
+ `could not read Prismic models: ${describeThrown(e)}`,
268
+ "failed",
269
+ 1,
270
+ cfg.repositoryName
271
+ );
272
+ }
273
+ return { ok: true, cfg, token: resolved.token, local, remote };
274
+ }
275
+ async function checkOneSite(repoRoot, deps, opts) {
276
+ const inputs = await readSiteInputs(repoRoot, deps, {
277
+ allowGenericToken: opts.allowGenericToken,
278
+ noEffect: "Nothing was compared and nothing was pushed"
279
+ });
280
+ if (!inputs.ok) return inputs.failure;
281
+ const { cfg, token, local, remote } = inputs;
282
+ const diff = diffModels(local, remote);
283
+ const report = await pushModels(diff, {
284
+ apply: opts.apply,
285
+ send: (entry, _remote, action) => deps.sendModel(cfg.repositoryName, token, entry, action)
286
+ });
287
+ const foundNothingAnywhere = isClean(diff) && diff.unchanged.length === 0;
288
+ const reportOptions = { apply: opts.apply, report };
289
+ const inconsistencies = reconcileReport(diff, reportOptions);
290
+ return {
291
+ output: renderModelReport(cfg.repositoryName, diff, reportOptions),
292
+ // A dry run NEVER fails on drift: a model PR is supposed to differ from the
293
+ // remote, and the comment is the review artifact, not a gate. Only a real
294
+ // push failure — or one of the "we could not find out" returns above — is an
295
+ // error.
296
+ //
297
+ // An inconsistency IS such an error, on both modes. Drift is a fact about
298
+ // the site; an inconsistency is a fact about the report, which says every
299
+ // figure in it is unreliable. Letting that pass green is a reviewer
300
+ // approving a merge from numbers the report itself disowns.
301
+ code: report.failed.length > 0 || inconsistencies.length > 0 ? 1 : 0,
302
+ // `false`, not `null`, and for the same reason as `foundNothingAnywhere`
303
+ // above: the check ran to completion and produced a finding — its inputs
304
+ // disagree — so a human needs to look. `null` means "we never found out".
305
+ //
306
+ // An earlier version of this comment justified that by saying `null` would
307
+ // count toward Task 17's majority-failure rule; Task 17 falsified it. That
308
+ // rule counts `status`, and this row is `status: "checked"` on either value,
309
+ // so the sweep's exit code would not move — which is the point. `clean` is
310
+ // the PER-SITE verdict: the field the sweep carries for each row and the one
311
+ // Task 19 writes to Airtable and the cockpit alarms on. `null` there writes
312
+ // no verdict at all for a check that ran to completion and found something,
313
+ // so the loudest failure this renderer can report would be the one row the
314
+ // fleet never hears about.
315
+ clean: inconsistencies.length > 0 ? false : isClean(diff) && !foundNothingAnywhere,
316
+ status: "checked",
317
+ repositoryName: cfg.repositoryName
318
+ };
319
+ }
320
+ var SITE_COL = 28;
321
+ var REPO_COL = 26;
322
+ var ENV_COL = 40;
323
+ var NO_VALUE = "\u2014";
324
+ var DOCTOR_HEADER = [
325
+ "Prismic write-token doctor \u2014 which secret each site needs, and whether it is set.",
326
+ "",
327
+ "The naming rule is PRISMIC_TOKEN_<REPOSITORY NAME>, upper-snaked from the PRISMIC",
328
+ "repository name, which is routinely not the repo directory name: reddoor-website's",
329
+ "repository is reddoor-la, data-dynamiq's is reddoor-wireframer, and",
330
+ "beachfront-dentistry's is 48bb12d1. Prefix-first is deliberate \u2014 it guarantees a",
331
+ "leading letter, and 48BB12D1_PRISMIC is rejected both by shell sourcing and by this",
332
+ "repo's own credentials parser.",
333
+ "",
334
+ "The generic PRISMIC_WRITE_TOKEN is NOT consulted here. It is the in-repo CI fallback",
335
+ "for whichever single repository a workflow is checked out against; this table is the",
336
+ "per-repository checklist, and a fallback cannot tick a repository's own box.",
337
+ "",
338
+ "Nothing below was verified against Prismic and no token value is ever printed \u2014 this",
339
+ "reads the environment for emptiness only. A secret that is set is reported as",
340
+ "PRESENT (not verified); run `reddoor-maint prismic-models` in the site to find out",
341
+ "whether it actually works.",
342
+ "",
343
+ `${"repo".padEnd(SITE_COL)} ${"Prismic repository".padEnd(REPO_COL)} ${"secret".padEnd(ENV_COL)} verdict`
344
+ ];
345
+ function renderTokenDoctor(probes) {
346
+ const lines = [...DOCTOR_HEADER];
347
+ let ok = 0;
348
+ let missing = 0;
349
+ let failing = 0;
350
+ let unverified = 0;
351
+ let unreadable = 0;
352
+ let skipped = 0;
353
+ for (const p of probes) {
354
+ const cols = `${p.site.padEnd(SITE_COL)} ${(p.repositoryName ?? NO_VALUE).padEnd(REPO_COL)} ${(p.expectedEnv ?? NO_VALUE).padEnd(ENV_COL)}`;
355
+ let verdict;
356
+ if (p.status === "failed") {
357
+ unreadable++;
358
+ verdict = `CANNOT TELL \u2014 ${p.error ?? "this site could not be read"}. Nothing was established about this site's secret.`;
359
+ } else if (p.status === "skipped") {
360
+ skipped++;
361
+ verdict = "no Prismic config (skipped) \u2014 this site needs no token";
362
+ } else if (!p.present) {
363
+ missing++;
364
+ verdict = "MISSING \u2014 mint this secret";
365
+ } else if (p.reads === false) {
366
+ failing++;
367
+ verdict = `PRESENT BUT 403/FAILED \u2014 ${p.error ?? "read failed"}`;
368
+ } else if (p.reads === true) {
369
+ ok++;
370
+ verdict = "OK";
371
+ } else {
372
+ unverified++;
373
+ verdict = "PRESENT (not verified)";
374
+ }
375
+ lines.push(`${cols} ${verdict}`);
376
+ }
377
+ lines.push("");
378
+ if (unreadable > 0) {
379
+ lines.push(
380
+ `\u26A0 ${unreadable} site(s) could not be read at all. Those are NOT sites without Prismic \u2014 nothing was established about their secrets, so do not read this table as a complete checklist.`
381
+ );
382
+ }
383
+ lines.push(
384
+ `${ok} ok, ${missing} missing, ${failing} failing, ${unverified} unverified, ${unreadable} unreadable, ${skipped} skipped.`
385
+ );
386
+ return lines.join("\n");
387
+ }
388
+ async function probeSiteToken(repoRoot, env, label) {
389
+ const site = label !== void 0 && label.trim() !== "" ? label : basename(repoRoot) || repoRoot;
390
+ const blank = {
391
+ site,
392
+ repositoryName: null,
393
+ expectedEnv: null,
394
+ present: false,
395
+ reads: null
396
+ };
397
+ let entries;
398
+ try {
399
+ entries = await readdir(repoRoot);
400
+ } catch (e) {
401
+ return {
402
+ ...blank,
403
+ status: "failed",
404
+ error: `cannot read this checkout at ${repoRoot}: ${describeThrown(e)}`
405
+ };
406
+ }
407
+ const noTree = noWorkingTreeFailure(
408
+ repoRoot,
409
+ entries,
410
+ "Nothing was established about this repo's token"
411
+ );
412
+ if (noTree) return { ...blank, status: "failed", error: noTree };
413
+ let cfg;
414
+ try {
415
+ cfg = await readPrismicConfig(repoRoot);
416
+ } catch (e) {
417
+ return {
418
+ ...blank,
419
+ status: "failed",
420
+ error: `Prismic config present but unusable: ${describeThrown(e)}`
421
+ };
422
+ }
423
+ if (!cfg) return { ...blank, status: "skipped" };
424
+ let expectedEnv;
425
+ try {
426
+ expectedEnv = prismicTokenEnvName(cfg.repositoryName);
427
+ } catch (e) {
428
+ return {
429
+ ...blank,
430
+ status: "failed",
431
+ repositoryName: cfg.repositoryName,
432
+ error: `cannot work out which secret holds this repository's token: ${describeThrown(e)}`
433
+ };
434
+ }
435
+ return {
436
+ site,
437
+ status: "checked",
438
+ repositoryName: cfg.repositoryName,
439
+ expectedEnv,
440
+ // The one read of the environment, and only its emptiness survives this
441
+ // expression. `.trim()` matches `resolvePrismicToken` exactly: a
442
+ // whitespace-only secret is one the pipeline itself refuses, so a doctor
443
+ // that called it present would tick a box the push would then fail on.
444
+ present: (env[expectedEnv] ?? "").trim() !== "",
445
+ reads: null
446
+ };
447
+ }
448
+ var tokenDoctorActionable = (probes) => probes.some(
449
+ (p) => p.status === "failed" || p.status === "checked" && (!p.present || p.reads === false)
450
+ );
451
+ async function runTokenDoctor(repoRoot, deps) {
452
+ const probes = [await probeSiteToken(repoRoot, deps.env)];
453
+ return { output: renderTokenDoctor(probes), code: tokenDoctorActionable(probes) ? 1 : 0 };
454
+ }
455
+ async function pullRemoteOnly(repoRoot, deps) {
456
+ const inputs = await readSiteInputs(repoRoot, deps, {
457
+ // TRUE, for the same reason the in-repo check sets it: one checkout, one
458
+ // Prismic repository, and `--pull` is refused outright in fleet mode — where
459
+ // a single generic token would attach the wrong credential to every site
460
+ // after the first.
461
+ allowGenericToken: true,
462
+ noEffect: "Nothing was pulled and nothing was written"
463
+ });
464
+ if (!inputs.ok) return { output: inputs.failure.output, code: inputs.failure.code };
465
+ const { cfg, local, remote } = inputs;
466
+ const diff = diffModels(local, remote);
467
+ if (diff.remoteOnly.length === 0) {
468
+ return { output: `nothing to pull \u2014 no remote-only models in ${cfg.repositoryName}`, code: 0 };
469
+ }
470
+ const library = cfg.libraries[0] ?? "";
471
+ const format = (root, paths, fmtOpts) => formatWithPrettier(deps.spawn, root, paths, fmtOpts);
472
+ const lines = [`Prismic pull-down \u2014 repository: ${cfg.repositoryName}`];
473
+ if (cfg.libraries.length === 0) {
474
+ lines.push(
475
+ `slice library: NONE \u2014 this site's Prismic config declares no slice library, so no slice can be placed. Custom types are unaffected.`
476
+ );
477
+ } else {
478
+ lines.push(
479
+ `slice library: ${library}` + (cfg.libraries.length > 1 ? ` (the first of ${cfg.libraries.length} declared; the others are not written to)` : "")
480
+ );
481
+ }
482
+ lines.push("");
483
+ let unformatted = 0;
484
+ let refused = 0;
485
+ for (const entry of diff.remoteOnly) {
486
+ try {
487
+ const res = await writeModelFile(format, repoRoot, entry, library);
488
+ if (!res.formatted) unformatted++;
489
+ lines.push(
490
+ `pulled ${entry.kind} ${entry.id} -> ${res.path}${res.formatted ? "" : " (unformatted)"}`
491
+ );
492
+ } catch (e) {
493
+ refused++;
494
+ lines.push(`REFUSED ${entry.kind} ${entry.id} \u2014 ${describeThrown(e)}`);
495
+ }
496
+ }
497
+ if (unformatted > 0) lines.push(`\u26A0 ${PRETTIER_FLAG_NOTE}`);
498
+ lines.push("");
499
+ lines.push(
500
+ `${diff.remoteOnly.length - refused} of ${diff.remoteOnly.length} model(s) pulled` + (refused > 0 ? `, ${refused} refused` : "") + `.`
501
+ );
502
+ if (refused < diff.remoteOnly.length) lines.push("Review, commit, and open a PR.");
503
+ if (refused > 0) {
504
+ lines.push(
505
+ `A refusal leaves that model ONLY in Prismic \u2014 nothing was deleted there and nothing was overwritten here. Act on each REFUSED line above.`
506
+ );
507
+ }
508
+ return { output: lines.join("\n"), code: refused > 0 ? 1 : 0 };
509
+ }
510
+ function prismicSweepExitCode(checked, failed) {
511
+ return failed > checked ? 1 : 0;
512
+ }
513
+ function uniqueBySite(rows) {
514
+ const seen = /* @__PURE__ */ new Set();
515
+ return rows.filter((r) => seen.has(r.site) ? false : (seen.add(r.site), true));
516
+ }
517
+ function findRepositoryCollisions(rows) {
518
+ const byRepository = /* @__PURE__ */ new Map();
519
+ for (const r of uniqueBySite(rows)) {
520
+ if (r.repositoryName === null) continue;
521
+ const list = byRepository.get(r.repositoryName) ?? [];
522
+ list.push(r.site);
523
+ byRepository.set(r.repositoryName, list);
524
+ }
525
+ return [...byRepository.entries()].filter(([, sites]) => sites.length > 1).map(([repositoryName, sites]) => ({ repositoryName, sites: [...sites].sort() })).sort((a, b) => a.repositoryName.localeCompare(b.repositoryName));
526
+ }
527
+ function findTokenEnvCollisions(rows) {
528
+ const byEnv = /* @__PURE__ */ new Map();
529
+ for (const r of uniqueBySite(rows)) {
530
+ if (r.repositoryName === null) continue;
531
+ let envName;
532
+ try {
533
+ envName = prismicTokenEnvName(r.repositoryName);
534
+ } catch {
535
+ continue;
536
+ }
537
+ const list = byEnv.get(envName) ?? [];
538
+ list.push({ site: r.site, repositoryName: r.repositoryName });
539
+ byEnv.set(envName, list);
540
+ }
541
+ return [...byEnv.entries()].filter(([, entries]) => new Set(entries.map((e) => e.repositoryName)).size > 1).map(([envName, entries]) => ({
542
+ envName,
543
+ entries: [...entries].sort((a, b) => a.site.localeCompare(b.site))
544
+ })).sort((a, b) => a.envName.localeCompare(b.envName));
545
+ }
546
+ function describeTokenEnvCollisions(collisions) {
547
+ if (collisions.length === 0) return "";
548
+ return [
549
+ "## \u26D4 One token secret derived by more than one Prismic repository",
550
+ "",
551
+ ...collisions.map(
552
+ (c) => `- **${c.envName}** is derived by ${c.entries.map((e) => `${e.site} (${e.repositoryName})`).join(" and ")}. These are DIFFERENT Prismic repositories reading ONE secret, so whichever token is set is sent to both \u2014 one client's credential at another client's repository. Neither repo's config is wrong, so do not "fix" one of them: the env var NAME is derived from the repository name and collapses them. Rename one Prismic repository, or give the derivation a disambiguator. Until then, treat both sites' verdicts as unproven.`
553
+ )
554
+ ].join("\n");
555
+ }
556
+ function findDuplicateSiteRows(rows) {
557
+ const bySite = /* @__PURE__ */ new Map();
558
+ for (const r of rows) {
559
+ const list = bySite.get(r.site) ?? [];
560
+ list.push(r.repositoryName);
561
+ bySite.set(r.site, list);
562
+ }
563
+ return [...bySite.entries()].filter(([, names]) => names.length > 1).map(([site, names]) => ({
564
+ site,
565
+ count: names.length,
566
+ repositoryNames: [...new Set(names.filter((n) => n !== null))].sort()
567
+ })).sort((a, b) => a.site.localeCompare(b.site));
568
+ }
569
+ function describeDuplicateSiteRows(duplicates) {
570
+ if (duplicates.length === 0) return "";
571
+ return [
572
+ "## \u26A0 A site is listed more than once in the inventory",
573
+ "",
574
+ ...duplicates.map((d) => {
575
+ const names = d.repositoryNames.length === 0 ? "no repositoryName was established for it" : `repositoryName ${d.repositoryNames.join(" and ")}`;
576
+ return `- **${d.site}** appears ${d.count}\xD7 (${names}). It was swept ${d.count}\xD7, so the counts below are inflated by ${d.count - 1}. This is an INVENTORY problem, not a conflict between two sites \u2014 do not change a slicemachine.config.json for it.`;
577
+ })
578
+ ].join("\n");
579
+ }
580
+ function describeCollisions(collisions) {
581
+ if (collisions.length === 0) return "";
582
+ return [
583
+ "## \u26D4 Prismic repository claimed by more than one site",
584
+ "",
585
+ ...collisions.map(
586
+ (c) => `- **${c.repositoryName}** is claimed by ${c.sites.join(" and ")}. Each repo's CI pushes to it on merge, so they overwrite each other \u2014 and neither repo can detect this on its own. Fix the slicemachine.config.json in whichever repo is not the owner.`
587
+ )
588
+ ].join("\n");
589
+ }
590
+ function countSweep(rows) {
591
+ return {
592
+ checked: rows.filter((r) => r.status === "checked").length,
593
+ failed: rows.filter((r) => r.status === "failed").length,
594
+ skipped: rows.filter((r) => r.status === "skipped").length
595
+ };
596
+ }
597
+ function summariseSweep(rows) {
598
+ const { checked, failed, skipped } = countSweep(rows);
599
+ const lines = [];
600
+ if (failed > 0) {
601
+ lines.push(
602
+ `\u26A0 ${failed} site(s) could not be read at all. Those are NOT sites without Prismic \u2014 nothing was established about their models, so do not read this sweep as a complete picture of the fleet.`
603
+ );
604
+ }
605
+ lines.push(
606
+ `${checked} checked, ${failed} failed, ${skipped} skipped (no Prismic config), of ${rows.length} site(s).`
607
+ );
608
+ lines.push(
609
+ `Each site was compared AS ITS CHECKOUT STANDS ON DISK. This sweep never fetches, pulls or resets a checkout, so the commit marked "@" beside each site above \u2014 not that repo's default branch \u2014 is what its verdict describes.`
610
+ );
611
+ return lines.join("\n");
612
+ }
613
+ function describeNothingChecked(rows) {
614
+ if (rows.length === 0) return "";
615
+ if (countSweep(rows).checked > 0) return "";
616
+ return `\u26D4 NOT ONE SITE WAS CHECKED. All ${rows.length} site(s) in this inventory came back skipped or unreadable, so nothing was compared against Prismic and this run learned nothing about the fleet's models. That is not a clean fleet: a skip is inferred from a MISSING config file, so anything that empties the checkouts makes every site in the fleet look like a repo that simply has no Prismic in it, and exit 0. Do NOT read this exit as a result.`;
617
+ }
618
+ async function resolveCheckoutCommit(repoRoot) {
619
+ const isSha = (s) => /^[0-9a-f]{7,64}$/i.test(s);
620
+ try {
621
+ const dotGit = join(repoRoot, ".git");
622
+ const s = await stat(dotGit);
623
+ let gitDir = dotGit;
624
+ if (!s.isDirectory()) {
625
+ const pointer = /^gitdir:\s*(.+)$/m.exec(await readFile(dotGit, "utf-8"))?.[1]?.trim();
626
+ if (!pointer) return { unresolved: `.git is a file that names no gitdir` };
627
+ gitDir = resolve(repoRoot, pointer);
628
+ }
629
+ const head = (await readFile(join(gitDir, "HEAD"), "utf-8")).trim();
630
+ if (isSha(head)) return { resolved: head };
631
+ const ref = /^ref:\s*(.+)$/.exec(head)?.[1]?.trim();
632
+ if (!ref) return { unresolved: `HEAD is neither a commit id nor a ref (${head.slice(0, 60)})` };
633
+ try {
634
+ const loose = (await readFile(join(gitDir, ref), "utf-8")).trim();
635
+ return isSha(loose) ? { resolved: loose } : { unresolved: `${ref} holds no commit id` };
636
+ } catch {
637
+ const packed = await readFile(join(gitDir, "packed-refs"), "utf-8");
638
+ for (const line of packed.split("\n")) {
639
+ const [sha, name] = line.trim().split(/\s+/);
640
+ if (name === ref && sha !== void 0 && isSha(sha)) return { resolved: sha };
641
+ }
642
+ return { unresolved: `${ref} is in neither this checkout's loose refs nor packed-refs` };
643
+ }
644
+ } catch (e) {
645
+ return { unresolved: describeThrown(e) };
646
+ }
647
+ }
648
+ function describeCheckoutCommit(commit) {
649
+ if (commit === null) return "";
650
+ return "resolved" in commit ? ` @ ${commit.resolved.slice(0, 12)}` : ` @ COMMIT NOT RESOLVED (${commit.unresolved})`;
651
+ }
652
+ async function sweepOneSite(s, deps) {
653
+ const label = siteLabel(s);
654
+ const commit = await resolveCheckoutCommit(s.path);
655
+ try {
656
+ const r = await checkOneSite(s.path, deps, {
657
+ apply: false,
658
+ // Fleet mode forbids the generic token: one PRISMIC_WRITE_TOKEN in the
659
+ // environment while iterating every fleet repository would attach the wrong
660
+ // credential to every site after the first. In-repo mode is the opposite,
661
+ // and that asymmetry is the whole point of the flag.
662
+ allowGenericToken: false
663
+ });
664
+ return {
665
+ site: label,
666
+ repositoryName: r.repositoryName ?? null,
667
+ commit,
668
+ // Carry `status` THROUGH — never re-derive a verdict here.
669
+ status: r.status,
670
+ clean: r.clean,
671
+ detail: r.output
672
+ };
673
+ } catch (e) {
674
+ return {
675
+ site: label,
676
+ repositoryName: null,
677
+ commit,
678
+ status: "failed",
679
+ clean: null,
680
+ detail: `unexpected error while checking this site: ${describeThrown(e)}. Nothing was compared for it \u2014 this is NOT "no Prismic in this repo".`
681
+ };
682
+ }
683
+ }
684
+ async function prepareFleet(site, opts, cwd) {
685
+ const sites = await resolveSites({
686
+ // Passed through so `resolveSites` can refuse `prismic-models espada --fleet
687
+ // inv.json` (exit 2). Dropping it would silently sweep the whole fleet for
688
+ // an operator who named one site.
689
+ ...site !== void 0 ? { site } : {},
690
+ ...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
691
+ ...opts.workdir !== void 0 ? { workdir: opts.workdir } : {},
692
+ cwd
693
+ });
694
+ const workdir = opts.workdir ?? fleetWorkdir();
695
+ return { prep: await prepareFleetSites(sites, { workdir }), resolved: sites.length };
696
+ }
697
+ async function sweepFleet(site, opts, deps, cwd) {
698
+ const { prep, resolved } = await prepareFleet(site, opts, cwd);
699
+ const rows = [];
700
+ for (const s of prep.prepared) rows.push(await sweepOneSite(s, deps));
701
+ for (const s of prep.skipped) {
702
+ rows.push({
703
+ site: s.site,
704
+ repositoryName: null,
705
+ // No checkout was established, so there is no commit to name. `null` here
706
+ // is "there is no tree", NOT "we could not read one" — that is the
707
+ // `unresolved` case, and they must not render alike.
708
+ commit: null,
709
+ status: "failed",
710
+ clean: null,
711
+ detail: `could not prepare this checkout: ${s.reason}. Nothing was compared for it \u2014 this is NOT "no Prismic in this repo".`
712
+ });
713
+ }
714
+ return { rows, skipped: prep.skipped, resolved };
715
+ }
716
+ var NO_VERDICT_BANNER = "\u26A0 NO VERDICT \u2014 this site's check FAILED, so nothing was established about its models:";
717
+ function sweepRowWriteback(row, checkedAt) {
718
+ if (row.status === "skipped") return { verdict: null, checkedAt, detail: row.detail };
719
+ if (row.status === "failed" || row.clean === null) {
720
+ return { verdict: "unknown", checkedAt, detail: `${NO_VERDICT_BANNER}
721
+ ${row.detail}` };
722
+ }
723
+ return {
724
+ verdict: row.clean ? "pass" : "fail",
725
+ checkedAt,
726
+ // A clean site clears its detail: leaving yesterday's drift text under a
727
+ // `pass` is a finding that reads as current and is not.
728
+ detail: row.clean ? null : row.detail
729
+ };
730
+ }
731
+ async function writeSweepToAirtable(rows, websites, update, checkedAt) {
732
+ const bySlug = /* @__PURE__ */ new Map();
733
+ for (const w of websites) {
734
+ const slug = siteSlug(w.name);
735
+ const list = bySlug.get(slug) ?? [];
736
+ list.push(w);
737
+ bySlug.set(slug, list);
738
+ }
739
+ const result = { written: [], failed: [] };
740
+ for (const row of rows) {
741
+ const slug = siteSlug(row.site);
742
+ const matches = bySlug.get(slug) ?? [];
743
+ const target = matches.length === 1 ? matches[0] : void 0;
744
+ if (!target) {
745
+ result.failed.push({
746
+ slug,
747
+ error: matches.length === 0 ? `no Websites row matched "${row.site}" \u2014 its verdict was NOT recorded anywhere, so the cockpit will keep whatever this site's row said before (or nothing). Check the inventory name against the Websites Name column.` : `${matches.length} Websites rows match "${row.site}" (${matches.map((m) => m.name).join(", ")}) \u2014 refusing to guess which one owns this verdict, because writing it to the wrong row is worse than not writing it.`
748
+ });
749
+ continue;
750
+ }
751
+ try {
752
+ await update(target.id, sweepRowWriteback(row, checkedAt));
753
+ result.written.push({
754
+ siteName: target.name,
755
+ writes: [{ audit: "prismic-models", counts: {} }]
756
+ });
757
+ } catch (e) {
758
+ result.failed.push({ slug, error: describeThrown(e) });
759
+ }
760
+ }
761
+ return result;
762
+ }
763
+ async function persistSweep(rows, deps) {
764
+ let sink;
765
+ let formatSummary;
766
+ try {
767
+ formatSummary = (await import("./write-audits-to-airtable-3SOXLKJK.js")).formatFleetWriteSummary;
768
+ sink = await deps.openVerdictSink();
769
+ } catch (e) {
770
+ return {
771
+ output: `\u26D4 --write-airtable was asked for and NOTHING WAS WRITTEN: ${describeThrown(e)}. Every verdict above is unrecorded, so the cockpit and the digest keep whatever they held before \u2014 which for a site that has broken since is a green tick. Do NOT read the report above as having reached the fleet's record.`,
772
+ code: 1
773
+ };
774
+ }
775
+ const result = await writeSweepToAirtable(
776
+ rows,
777
+ sink.websites,
778
+ sink.update,
779
+ (/* @__PURE__ */ new Date()).toISOString()
780
+ );
781
+ return { output: formatSummary(result), code: 0 };
782
+ }
783
+ async function runFleetSweep(site, opts, deps, cwd) {
784
+ const { rows, skipped, resolved } = await sweepFleet(site, opts, deps, cwd);
785
+ if (resolved === 0) {
786
+ return {
787
+ output: `the inventory resolved NO SITES, so no sites were swept and nothing was compared. This is not a clean fleet \u2014 check the inventory (an Airtable view filter, an empty JSON file, a dynamic inventory returning []). Do NOT read this exit as a result.`,
788
+ code: 1
789
+ };
790
+ }
791
+ const { checked, failed } = countSweep(rows);
792
+ const body = rows.map((r) => `[${r.site}]${describeCheckoutCommit(r.commit)} ${r.detail}`).join("\n\n");
793
+ const collisions = findRepositoryCollisions(rows);
794
+ const envCollisions = findTokenEnvCollisions(rows);
795
+ const duplicates = findDuplicateSiteRows(rows);
796
+ const nothingChecked = describeNothingChecked(rows);
797
+ const output = appendSkipNotice(
798
+ [
799
+ describeCollisions(collisions),
800
+ describeTokenEnvCollisions(envCollisions),
801
+ describeDuplicateSiteRows(duplicates),
802
+ body,
803
+ summariseSweep(rows),
804
+ nothingChecked
805
+ ].filter((s) => s !== "").join("\n\n"),
806
+ skipped
807
+ );
808
+ const alarms = collisions.length + envCollisions.length;
809
+ const code = alarms > 0 || nothingChecked !== "" ? 1 : prismicSweepExitCode(checked, failed);
810
+ if (!requested(opts.writeAirtable)) return { output, code };
811
+ const persisted = await persistSweep(rows, deps);
812
+ return {
813
+ output: `${output}
814
+
815
+ ${persisted.output}`,
816
+ code: code !== 0 ? code : persisted.code
817
+ };
818
+ }
819
+ async function runFleetTokenDoctor(site, opts, deps, cwd) {
820
+ const { prep, resolved } = await prepareFleet(site, opts, cwd);
821
+ if (resolved === 0) {
822
+ return {
823
+ output: `the inventory resolved NO SITES, so no site's token requirement was established. This is not a fleet that needs no secrets \u2014 check the inventory (an Airtable view filter, an empty JSON file, a dynamic inventory returning []). Do NOT read this exit as a checklist.`,
824
+ code: 1
825
+ };
826
+ }
827
+ const probes = [];
828
+ for (const s of prep.prepared) probes.push(await probeSiteToken(s.path, deps.env, siteLabel(s)));
829
+ for (const s of prep.skipped) {
830
+ probes.push({
831
+ site: s.site,
832
+ status: "failed",
833
+ repositoryName: null,
834
+ expectedEnv: null,
835
+ present: false,
836
+ reads: null,
837
+ error: `could not prepare this checkout: ${s.reason}`
838
+ });
839
+ }
840
+ const envCollisions = findTokenEnvCollisions(probes);
841
+ const duplicates = findDuplicateSiteRows(probes);
842
+ const nothingProbed = probes.some((p) => p.status === "checked") || probes.length === 0 ? "" : `\u26D4 NOT ONE SITE'S TOKEN REQUIREMENT WAS ESTABLISHED. All ${probes.length} site(s) in this inventory came back unreadable or without a Prismic config, so this run learnt nothing about which secrets the fleet needs. That is not a fleet that is already provisioned: a skip is inferred from a MISSING config file, so anything that empties the checkouts makes every site look like a repo with no Prismic in it. Do NOT read this exit as a result.`;
843
+ const output = appendSkipNotice(
844
+ [
845
+ describeTokenEnvCollisions(envCollisions),
846
+ describeDuplicateSiteRows(duplicates),
847
+ renderTokenDoctor(probes),
848
+ nothingProbed
849
+ ].filter((s) => s !== "").join("\n\n"),
850
+ prep.skipped
851
+ );
852
+ return {
853
+ output,
854
+ code: tokenDoctorActionable(probes) || envCollisions.length > 0 || nothingProbed !== "" ? 1 : 0
855
+ };
856
+ }
857
+ var requested = (v) => v !== void 0 && v !== false;
858
+ function modeConflict(opts) {
859
+ if (requested(opts.pull) && requested(opts.tokens)) {
860
+ return "cannot combine --pull with --tokens. They are two different modes: one writes remote-only models into this working tree, the other prints which secret each site needs. Nothing was pulled, written or read \u2014 ask for one of them.";
861
+ }
862
+ if (requested(opts.pull)) {
863
+ if (opts.apply === true) {
864
+ return "cannot combine --pull with --apply \u2014 pull, review, then push. Running both in one invocation would push this repo's models and adopt Prismic's in the same breath, with no review in between. Nothing was pulled, written or pushed.";
865
+ }
866
+ if (requested(opts.fleet)) {
867
+ return "cannot combine --pull with --fleet: --pull is single-repo only, because it WRITES INTO A WORKING TREE and the result is meant to land as one reviewed PR. Nothing was pulled or written.";
868
+ }
869
+ if (opts.commentFile !== void 0) {
870
+ return "cannot combine --pull with --comment-file. The comment file is the in-repo model check's review artifact; a pull-down's review artifact is the diff it leaves in the working tree. Nothing was pulled or written.";
871
+ }
872
+ }
873
+ if (requested(opts.tokens)) {
874
+ if (opts.apply === true) {
875
+ return "cannot combine --tokens with --apply. --tokens is a read-only checklist of which secret each site needs; it pushes nothing. Nothing was compared, pushed or written \u2014 drop one of the two flags.";
876
+ }
877
+ if (opts.commentFile !== void 0) {
878
+ return "cannot combine --tokens with --comment-file. The comment file is the in-repo model check's review artifact; the token doctor writes no report for a PR. Nothing was written \u2014 redirect the output instead.";
879
+ }
880
+ if (requested(opts.writeAirtable)) {
881
+ return "cannot combine --tokens with --write-airtable. --write-airtable persists a fleet sweep's per-site MODEL verdicts to the Websites table, and the token doctor produces none \u2014 it compares no models and calls Prismic not at all, so there is nothing for it to write. Accepting it would print a checklist, write nothing and exit 0. Nothing was read or written \u2014 run the sweep for verdicts, the doctor for secrets.";
882
+ }
883
+ }
884
+ if (requested(opts.fleet)) {
885
+ if (opts.apply === true) {
886
+ return "cannot combine --fleet with --apply. The fleet sweep is READ-ONLY BY CONSTRUCTION \u2014 `apply` is not plumbed through it at all, because a fleet-wide model push outside CI is \u{1F534} under AUTONOMY.md. Accepting the flag and sweeping read-only anyway would exit 0 having pushed nothing, which reads as a successful push. Nothing was compared, pushed or written \u2014 push per repo, through that repo's own CI.";
887
+ }
888
+ if (opts.commentFile !== void 0) {
889
+ return "cannot combine --fleet with --comment-file. The comment file is ONE repo's review artifact, posted on ONE pull request; a fleet sweep has no pull request to post to. Accepting it and writing nothing would leave a workflow waiting on a file that never appears. Nothing was swept or written \u2014 redirect the output instead.";
890
+ }
891
+ }
892
+ if (requested(opts.writeAirtable) && !requested(opts.fleet)) {
893
+ return "--write-airtable needs --fleet. It persists a fleet sweep's per-site verdicts to the Websites table, and the in-repo check has no sweep to write \u2014 its review artifact is the PR comment (--comment-file). Accepting the flag and comparing this one repo instead would exit 0 having written nothing. Nothing was compared, pushed or written.";
894
+ }
895
+ return null;
896
+ }
897
+ async function writeCommentFileAtomically(path, body) {
898
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
899
+ try {
900
+ await writeFile(tmp, body, "utf-8");
901
+ await rename(tmp, path);
902
+ } catch (e) {
903
+ await rm(tmp, { force: true }).catch(() => void 0);
904
+ throw e;
905
+ }
906
+ }
907
+ var runStamp = (env, repositoryName) => {
908
+ const bits = ["reddoor-maint prismic-models", repositoryName ?? "no Prismic repository resolved"];
909
+ if (env.GITHUB_RUN_ID) bits.push(`run ${env.GITHUB_RUN_ID}`);
910
+ if (env.GITHUB_RUN_ATTEMPT) bits.push(`attempt ${env.GITHUB_RUN_ATTEMPT}`);
911
+ if (env.GITHUB_SHA) bits.push(`commit ${env.GITHUB_SHA}`);
912
+ if (!env.GITHUB_RUN_ID) bits.push(`local pid ${process.pid}`);
913
+ bits.push((/* @__PURE__ */ new Date()).toISOString());
914
+ return bits.join(" \xB7 ");
915
+ };
916
+ async function runPrismicModelsCommand(site, opts, deps = defaultDeps()) {
917
+ const conflict = modeConflict(opts);
918
+ if (conflict) return { output: conflict, code: 2 };
919
+ const commentFile = opts.commentFile;
920
+ if (commentFile !== void 0 && (typeof commentFile !== "string" || !commentFile.trim())) {
921
+ return {
922
+ output: `--comment-file was given ${typeof commentFile === "string" ? "an empty value" : `a ${typeof commentFile}`}, which is not a path. Nothing was compared and nothing was pushed. An unset workflow variable expands to an empty string \u2014 pass a real path, or drop the flag. Do NOT read this exit as a result.`,
923
+ code: 1
924
+ };
925
+ }
926
+ const fleetInventory = opts.fleet;
927
+ if (requested(fleetInventory) && (typeof fleetInventory !== "string" || !fleetInventory.trim())) {
928
+ return {
929
+ output: `--fleet was given ${typeof fleetInventory === "string" ? "an empty value" : `a ${typeof fleetInventory}`}, which is not an inventory. Nothing was swept and nothing was compared. An unset workflow variable expands to an empty string, and an empty --fleet reads as "no fleet" \u2014 one directory swept and reported as the whole fleet. Pass an inventory path or "airtable". Do NOT read this exit as a result.`,
930
+ code: 2
931
+ };
932
+ }
933
+ const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();
934
+ const repoRoot = site ? resolve(cwd, site) : cwd;
935
+ if (requested(opts.tokens)) {
936
+ return requested(opts.fleet) ? runFleetTokenDoctor(site, opts, deps, cwd) : runTokenDoctor(repoRoot, deps);
937
+ }
938
+ if (requested(opts.pull)) return pullRemoteOnly(repoRoot, deps);
939
+ if (requested(opts.fleet)) return runFleetSweep(site, opts, deps, cwd);
940
+ const result = await checkOneSite(repoRoot, deps, {
941
+ // `=== true` rather than truthiness, so the ONE place that decides whether
942
+ // anything goes on the wire cannot be flipped by a stray string from a flag
943
+ // parser. Dry is the default and the only default.
944
+ apply: opts.apply === true,
945
+ // TRUE here, and false in fleet mode — the modes are opposites and this is
946
+ // the right side of it. An in-repo CI run has exactly one Prismic repository
947
+ // in scope, and `PRISMIC_WRITE_TOKEN` is the name the reusable workflow puts
948
+ // that repository's own Actions secret into (Task 24), rolled out per repo by
949
+ // Task 26. It is NOT a name sites already read: measured 2026-08-13 across
950
+ // the 15 in-scope repos from each one's origin/HEAD (`git grep`, never a
951
+ // working tree), 10 never mention it at all and the 5 that do only in
952
+ // one-shot migration scripts and docs — none in application code. A fleet run
953
+ // iterates every repository against one environment, where a generic token
954
+ // would attach the wrong credential to every site after the first.
955
+ allowGenericToken: true
956
+ });
957
+ let { output, code } = result;
958
+ if (commentFile !== void 0) {
959
+ let path = commentFile;
960
+ try {
961
+ path = resolve(cwd, commentFile);
962
+ const stamped = `${runStamp(deps.env, result.repositoryName)}
963
+
964
+ ${output}`;
965
+ await writeCommentFileAtomically(path, forComment(stamped));
966
+ } catch (e) {
967
+ output = `${output}
968
+
969
+ \u26A0 COULD NOT WRITE THE COMMENT FILE (${path}): ${describeThrown(e)}. No comment will be posted, so do not read a passing check as "reviewed".`;
970
+ code = code === 0 ? 1 : code;
971
+ }
972
+ }
973
+ return { output, code };
974
+ }
975
+ var GITHUB_COMMENT_LIMIT = 65536;
976
+ var COMMENT_WRAPPER_HEADROOM = 2048;
977
+ var DEFAULT_COMMENT_LIMIT = GITHUB_COMMENT_LIMIT - COMMENT_WRAPPER_HEADROOM;
978
+ function forComment(body, limit = DEFAULT_COMMENT_LIMIT) {
979
+ const headMarker = `\u26A0 TRUNCATED \u2014 this is not the whole report; the cut is explained at the end.
980
+
981
+ `;
982
+ const notice = `
983
+
984
+ \u26A0 TRUNCATED \u2014 this report is ${body.length} characters and the budget for this comment body is ${limit} (GitHub caps the whole comment, wrapper included, at ${GITHUB_COMMENT_LIMIT}). Everything below the cut is missing, including any further destructive lines. Run \`reddoor-maint prismic-models\` locally \u2014 dry is the default \u2014 for the whole report before approving.
985
+ `;
986
+ if (limit < headMarker.length + notice.length) {
987
+ throw new Error(
988
+ `forComment: limit ${limit} is smaller than the truncation warning itself (${headMarker.length + notice.length} characters), so a shortened report could not say that it was shortened. Raise the limit; do not drop the warning.`
989
+ );
990
+ }
991
+ if (body.length <= limit) return body;
992
+ let head = body.slice(0, Math.max(0, limit - notice.length - headMarker.length));
993
+ const last = head.charCodeAt(head.length - 1);
994
+ if (last >= 55296 && last <= 56319) head = head.slice(0, -1);
995
+ return headMarker + head + notice;
996
+ }
997
+ export {
998
+ DEFAULT_COMMENT_LIMIT,
999
+ checkOneSite,
1000
+ countSweep,
1001
+ defaultDeps,
1002
+ describeCheckoutCommit,
1003
+ describeCollisions,
1004
+ describeDuplicateSiteRows,
1005
+ describeNothingChecked,
1006
+ describeTokenEnvCollisions,
1007
+ findDuplicateSiteRows,
1008
+ findRepositoryCollisions,
1009
+ findTokenEnvCollisions,
1010
+ forComment,
1011
+ prismicSweepExitCode,
1012
+ renderTokenDoctor,
1013
+ resolveCheckoutCommit,
1014
+ runPrismicModelsCommand,
1015
+ summariseSweep,
1016
+ sweepFleet,
1017
+ sweepRowWriteback,
1018
+ writeSweepToAirtable
1019
+ };
1020
+ //# sourceMappingURL=prismic-models-HXCAM4ZL.js.map