@reddoorla/maintenance 0.94.1 → 0.95.1
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.
- package/README.md +31 -0
- package/dist/{announce-PGT4SZZB.js → announce-CLQYX5I4.js} +4 -4
- package/dist/{blux-NXQLMS4W.js → blux-4KNXUP2K.js} +1 -1
- package/dist/{bump-deps-6OR7LL4E.js → bump-deps-C5G2EFBB.js} +6 -6
- package/dist/{chunk-LS6QPANO.js → chunk-2LYOAB2E.js} +2 -2
- package/dist/{chunk-KQB2BX4V.js → chunk-2ZAZ7ETT.js} +2 -2
- package/dist/{chunk-XTK5VIQB.js → chunk-AVEQUR44.js} +48 -1
- package/dist/chunk-AVEQUR44.js.map +1 -0
- package/dist/{chunk-B6NUZ2ZX.js → chunk-BCKEP5S6.js} +2 -2
- package/dist/{chunk-DMPHP7UP.js → chunk-BI2VI6EM.js} +2 -2
- package/dist/{chunk-5FCBYK6L.js → chunk-BVEDKMNG.js} +10 -10
- package/dist/{chunk-OCNSIBMP.js → chunk-C3IQQKXL.js} +26 -1
- package/dist/{chunk-OCNSIBMP.js.map → chunk-C3IQQKXL.js.map} +1 -1
- package/dist/{chunk-PPIZHDJL.js → chunk-CJ2RMTT7.js} +2 -2
- package/dist/{chunk-7CJ3DVZW.js → chunk-CWXTNPNZ.js} +2 -2
- package/dist/{chunk-TGUESYGF.js → chunk-DFKBTBO3.js} +2 -2
- package/dist/{chunk-WHWVYL7B.js → chunk-FSGICFXB.js} +7 -32
- package/dist/chunk-FSGICFXB.js.map +1 -0
- package/dist/{chunk-3HS6LPFX.js → chunk-FUWFLAWQ.js} +4 -4
- package/dist/{chunk-4AIYWWKV.js → chunk-KJRVMMM3.js} +2 -2
- package/dist/{chunk-TCF6S7AD.js → chunk-LM2NHRCJ.js} +2 -2
- package/dist/{chunk-AEUABPAJ.js → chunk-R5IMFAFR.js} +2 -2
- package/dist/{chunk-NTLSMJHW.js → chunk-S7RYFUIK.js} +2 -2
- package/dist/{chunk-MLT4QY5X.js → chunk-SLFG7Y76.js} +1 -1
- package/dist/chunk-SLFG7Y76.js.map +1 -0
- package/dist/{chunk-REJCSA4H.js → chunk-TR3HFH6J.js} +4 -3
- package/dist/chunk-TR3HFH6J.js.map +1 -0
- package/dist/{chunk-22GKJO5F.js → chunk-VSGMCCGQ.js} +2 -2
- package/dist/{chunk-4R3YAQNV.js → chunk-WSJ7RO54.js} +2 -2
- package/dist/chunk-XPJQXSC4.js +112 -0
- package/dist/chunk-XPJQXSC4.js.map +1 -0
- package/dist/chunk-ZCL7C5LH.js +2698 -0
- package/dist/chunk-ZCL7C5LH.js.map +1 -0
- package/dist/{chunk-ZTPPCHDB.js → chunk-ZQV7UQNE.js} +2 -2
- package/dist/cli/bin.js +39 -26
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +1 -1
- package/dist/cli/commands/audit.js +8 -8
- package/dist/client-G2WGWPCA.js +11 -0
- package/dist/configs/eslint.js +27 -1
- package/dist/configs/eslint.js.map +1 -1
- package/dist/{convert-to-pnpm-3RVGUBBR.js → convert-to-pnpm-YTFREUJK.js} +6 -6
- package/dist/{db-FL7ZBF6H.js → db-XKZYDCKM.js} +9 -9
- package/dist/{digest-TZ3QQOSF.js → digest-7UBGUBRK.js} +14 -14
- package/dist/{digest-collectors-EHFY2OQC.js → digest-collectors-DJFN5ZDK.js} +8 -8
- package/dist/{ensure-site-WIDO6LGU.js → ensure-site-OAAE5YE2.js} +2 -2
- package/dist/{forms-notify-target-2LWF5C5B.js → forms-notify-target-UC6PF3AE.js} +2 -2
- package/dist/{github-signals-GAHNOAHS.js → github-signals-TX5FPYP6.js} +5 -5
- package/dist/{header-image-YN3AQRY3.js → header-image-PSPT7CUM.js} +2 -2
- package/dist/{health-endpoint-XLNBWVHK.js → health-endpoint-KVCJYWET.js} +6 -6
- package/dist/{health-mirror-EB76EERV.js → health-mirror-XJAKA3EY.js} +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +31 -29
- package/dist/index.js.map +1 -1
- package/dist/{init-YCLSGIEP.js → init-FFE4FCO2.js} +13 -13
- package/dist/{launch-FSHDTXSM.js → launch-UD36AHUN.js} +221 -7
- package/dist/launch-UD36AHUN.js.map +1 -0
- package/dist/match-harness-QRG6XZAR.js +70 -0
- package/dist/match-harness-QRG6XZAR.js.map +1 -0
- package/dist/migrate-Y7R4CESL.js +7 -0
- package/dist/{onboard-NKE6GZKL.js → onboard-TM2KESGF.js} +6 -6
- package/dist/{orchestrate-MWXENQOS.js → orchestrate-HYVQPRCE.js} +6 -6
- package/dist/{pipeline-MATFYQRC.js → pipeline-YROAY6DT.js} +2 -2
- package/dist/{preflight-4VQP7JJL.js → preflight-RYIP54NY.js} +7 -7
- package/dist/{prismic-ci-DM352L2Q.js → prismic-ci-6GKKZGB5.js} +13 -11
- package/dist/prismic-ci-6GKKZGB5.js.map +1 -0
- package/dist/{prismic-models-Q32ARSYS.js → prismic-models-P6FWWVRS.js} +5 -5
- package/dist/prospect/types.d.ts +1 -1
- package/dist/{prospect-audit-MT6RGMOY.js → prospect-audit-PRHU53HS.js} +5 -5
- package/dist/{prospect-audits-RVYEA4KH.js → prospect-audits-R3LWF6U7.js} +10 -4
- package/dist/recipes/sync-configs.d.ts +1 -1
- package/dist/recipes/sync-configs.js +4 -4
- package/dist/{render-JE5P26H5.js → render-YGNGTX3W.js} +2 -2
- package/dist/render-from-row-IFSBHWDH.js +1 -1
- package/dist/{report-6FFHGUUS.js → report-LQS4ECTM.js} +12 -12
- package/dist/{report-mirror-IP64R7B3.js → report-mirror-HDTOJE64.js} +3 -3
- package/dist/{schema-2V7BGYVT.js → schema-A4WCPIF4.js} +1 -1
- package/dist/{schema-2V7BGYVT.js.map → schema-A4WCPIF4.js.map} +1 -1
- package/dist/{self-updating-5DVDIEHB.js → self-updating-MWXIZLL7.js} +6 -6
- package/dist/{selftest-LJYXBIO5.js → selftest-VXTHRPRN.js} +9 -9
- package/dist/{site-mirror-UKHREAD6.js → site-mirror-WON4FTJ2.js} +3 -3
- package/dist/{smoke-suite-NUTC7IUK.js → smoke-suite-7DGITCTZ.js} +6 -6
- package/dist/{submissions-5GKJPCIH.js → submissions-OVZPVBYG.js} +2 -2
- package/dist/{svelte-codemods-U4NIT6CV.js → svelte-codemods-WSV2FGNG.js} +6 -6
- package/dist/{sync-configs-S2JYY6QU.js → sync-configs-RRIHNLIJ.js} +7 -7
- package/dist/{types-OIQZ5hWN.d.ts → types-D_bpy89i.d.ts} +1 -1
- package/dist/{upgrade-YFZCMUPE.js → upgrade-VTD6V4HS.js} +6 -6
- package/dist/util/git.d.ts +46 -1
- package/dist/util/git.js +7 -1
- package/package.json +1 -1
- package/dist/chunk-7MKT4I5T.js +0 -62
- package/dist/chunk-7MKT4I5T.js.map +0 -1
- package/dist/chunk-MLT4QY5X.js.map +0 -1
- package/dist/chunk-REJCSA4H.js.map +0 -1
- package/dist/chunk-WHWVYL7B.js.map +0 -1
- package/dist/chunk-XTK5VIQB.js.map +0 -1
- package/dist/client-M2V6FHH5.js +0 -11
- package/dist/launch-FSHDTXSM.js.map +0 -1
- package/dist/migrate-K4JETR36.js +0 -7
- package/dist/prismic-ci-DM352L2Q.js.map +0 -1
- /package/dist/{announce-PGT4SZZB.js.map → announce-CLQYX5I4.js.map} +0 -0
- /package/dist/{blux-NXQLMS4W.js.map → blux-4KNXUP2K.js.map} +0 -0
- /package/dist/{bump-deps-6OR7LL4E.js.map → bump-deps-C5G2EFBB.js.map} +0 -0
- /package/dist/{chunk-LS6QPANO.js.map → chunk-2LYOAB2E.js.map} +0 -0
- /package/dist/{chunk-KQB2BX4V.js.map → chunk-2ZAZ7ETT.js.map} +0 -0
- /package/dist/{chunk-B6NUZ2ZX.js.map → chunk-BCKEP5S6.js.map} +0 -0
- /package/dist/{chunk-DMPHP7UP.js.map → chunk-BI2VI6EM.js.map} +0 -0
- /package/dist/{chunk-5FCBYK6L.js.map → chunk-BVEDKMNG.js.map} +0 -0
- /package/dist/{chunk-PPIZHDJL.js.map → chunk-CJ2RMTT7.js.map} +0 -0
- /package/dist/{chunk-7CJ3DVZW.js.map → chunk-CWXTNPNZ.js.map} +0 -0
- /package/dist/{chunk-TGUESYGF.js.map → chunk-DFKBTBO3.js.map} +0 -0
- /package/dist/{chunk-3HS6LPFX.js.map → chunk-FUWFLAWQ.js.map} +0 -0
- /package/dist/{chunk-4AIYWWKV.js.map → chunk-KJRVMMM3.js.map} +0 -0
- /package/dist/{chunk-TCF6S7AD.js.map → chunk-LM2NHRCJ.js.map} +0 -0
- /package/dist/{chunk-AEUABPAJ.js.map → chunk-R5IMFAFR.js.map} +0 -0
- /package/dist/{chunk-NTLSMJHW.js.map → chunk-S7RYFUIK.js.map} +0 -0
- /package/dist/{chunk-22GKJO5F.js.map → chunk-VSGMCCGQ.js.map} +0 -0
- /package/dist/{chunk-4R3YAQNV.js.map → chunk-WSJ7RO54.js.map} +0 -0
- /package/dist/{chunk-ZTPPCHDB.js.map → chunk-ZQV7UQNE.js.map} +0 -0
- /package/dist/{client-M2V6FHH5.js.map → client-G2WGWPCA.js.map} +0 -0
- /package/dist/{convert-to-pnpm-3RVGUBBR.js.map → convert-to-pnpm-YTFREUJK.js.map} +0 -0
- /package/dist/{db-FL7ZBF6H.js.map → db-XKZYDCKM.js.map} +0 -0
- /package/dist/{digest-TZ3QQOSF.js.map → digest-7UBGUBRK.js.map} +0 -0
- /package/dist/{digest-collectors-EHFY2OQC.js.map → digest-collectors-DJFN5ZDK.js.map} +0 -0
- /package/dist/{ensure-site-WIDO6LGU.js.map → ensure-site-OAAE5YE2.js.map} +0 -0
- /package/dist/{forms-notify-target-2LWF5C5B.js.map → forms-notify-target-UC6PF3AE.js.map} +0 -0
- /package/dist/{github-signals-GAHNOAHS.js.map → github-signals-TX5FPYP6.js.map} +0 -0
- /package/dist/{header-image-YN3AQRY3.js.map → header-image-PSPT7CUM.js.map} +0 -0
- /package/dist/{health-endpoint-XLNBWVHK.js.map → health-endpoint-KVCJYWET.js.map} +0 -0
- /package/dist/{health-mirror-EB76EERV.js.map → health-mirror-XJAKA3EY.js.map} +0 -0
- /package/dist/{init-YCLSGIEP.js.map → init-FFE4FCO2.js.map} +0 -0
- /package/dist/{migrate-K4JETR36.js.map → migrate-Y7R4CESL.js.map} +0 -0
- /package/dist/{onboard-NKE6GZKL.js.map → onboard-TM2KESGF.js.map} +0 -0
- /package/dist/{orchestrate-MWXENQOS.js.map → orchestrate-HYVQPRCE.js.map} +0 -0
- /package/dist/{pipeline-MATFYQRC.js.map → pipeline-YROAY6DT.js.map} +0 -0
- /package/dist/{preflight-4VQP7JJL.js.map → preflight-RYIP54NY.js.map} +0 -0
- /package/dist/{prismic-models-Q32ARSYS.js.map → prismic-models-P6FWWVRS.js.map} +0 -0
- /package/dist/{prospect-audit-MT6RGMOY.js.map → prospect-audit-PRHU53HS.js.map} +0 -0
- /package/dist/{prospect-audits-RVYEA4KH.js.map → prospect-audits-R3LWF6U7.js.map} +0 -0
- /package/dist/{render-JE5P26H5.js.map → render-YGNGTX3W.js.map} +0 -0
- /package/dist/{report-6FFHGUUS.js.map → report-LQS4ECTM.js.map} +0 -0
- /package/dist/{report-mirror-IP64R7B3.js.map → report-mirror-HDTOJE64.js.map} +0 -0
- /package/dist/{self-updating-5DVDIEHB.js.map → self-updating-MWXIZLL7.js.map} +0 -0
- /package/dist/{selftest-LJYXBIO5.js.map → selftest-VXTHRPRN.js.map} +0 -0
- /package/dist/{site-mirror-UKHREAD6.js.map → site-mirror-WON4FTJ2.js.map} +0 -0
- /package/dist/{smoke-suite-NUTC7IUK.js.map → smoke-suite-7DGITCTZ.js.map} +0 -0
- /package/dist/{submissions-5GKJPCIH.js.map → submissions-OVZPVBYG.js.map} +0 -0
- /package/dist/{svelte-codemods-U4NIT6CV.js.map → svelte-codemods-WSV2FGNG.js.map} +0 -0
- /package/dist/{sync-configs-S2JYY6QU.js.map → sync-configs-RRIHNLIJ.js.map} +0 -0
- /package/dist/{upgrade-YFZCMUPE.js.map → upgrade-VTD6V4HS.js.map} +0 -0
package/README.md
CHANGED
|
@@ -151,6 +151,37 @@ Pre-flights that the site is on pnpm (refuses with a clear remediation if `packa
|
|
|
151
151
|
|
|
152
152
|
The full 7-step Svelte 4 → 5 migration: bump framework versions, migrate `svelte.config.js`, run the official `svelte-migrate` codemod, run `@tailwindcss/upgrade`, apply gotcha codemods over `src/**/*.svelte`, verify with `pnpm install` + `pnpm run check`, and write a `MIGRATION_SVELTE_5.md` summary. Each step is its own commit; the file leaves a record of what ran and what may need manual review.
|
|
153
153
|
|
|
154
|
+
### `match-harness` _(standalone — not part of `init`)_
|
|
155
|
+
|
|
156
|
+
Installs the matching harness used to rebuild a site against a live reference (a Webflow or Blux original), in one commit on its own branch.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
reddoor-maint match-harness --ref https://client.webflow.io
|
|
160
|
+
reddoor-maint match-harness acme --ref https://acme.webflow.io --cand http://localhost:4173
|
|
161
|
+
reddoor-maint match-harness --fleet airtable --ref https://client.webflow.io --matrix 1440,834,390
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`--ref` is **required**. The harness gates against a live reference; with no reference it can only ever report an absence of errors, so the recipe refuses instead of installing something that goes green on nothing. `--cand` defaults to `http://localhost:5173` and `--matrix` to `1440,834,390`.
|
|
165
|
+
|
|
166
|
+
What lands, and who owns it:
|
|
167
|
+
|
|
168
|
+
| Installed | Owner |
|
|
169
|
+
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
170
|
+
| `matching/harness.json` | **site** — the page table, hosts, matrix, marks |
|
|
171
|
+
| `matching/harness.mjs` | recipe — the only reader of `harness.json` |
|
|
172
|
+
| `matching/gate.sh`, `census.sh` | recipe — the geometry and style gates |
|
|
173
|
+
| `matching/next.mjs`, `strikes.mjs`, `build-spec.mjs`, `census-count.mjs` | recipe — round protocol |
|
|
174
|
+
| `matching/floors.mjs`, `census-deviations.mjs`, `LEDGER.md`, `spec-sections/_header.md`, `spec-sections/_chrome.md` | **site** — the records |
|
|
175
|
+
| `src/routes/dev/match/[uid]/+page.server.ts`, `+page.svelte` | recipe — the dev-guarded candidate twin |
|
|
176
|
+
| `src/lib/site-pages.js` / `site-pages.test.ts` | **site** scaffold / recipe test |
|
|
177
|
+
| `.gitignore`, `.prettierignore`, `CLAUDE.md` | marked blocks appended, once, on their own marker |
|
|
178
|
+
|
|
179
|
+
**Edit `matching/harness.json`, never `matching/harness.mjs`.** `harness.json` is the data — the page table, the reference and candidate origins, the breakpoint matrix, the threshold, and the `refMark` / `candMark` / `selfHosts` the reference preflight uses. `harness.mjs` is the read layer every gate goes through, and the recipe owns its bytes: it is byte-compared on the next run, safe-replaced when it still matches a shipped version, and FLAGGED and left alone when it does not. A hand edit there is never silently reverted — and never silently kept as though it were ours either.
|
|
180
|
+
|
|
181
|
+
Site-owned files are install-if-absent and are never rewritten. Re-running is a `noop`: the three appended blocks are idempotent on their markers, and every installed file that still matches its template is skipped.
|
|
182
|
+
|
|
183
|
+
Two things a fresh install deliberately will **not** do until the operator acts. `harness.json` ships `refMark: ""`, so `gate.sh` refuses before spending a run — a 200 is not evidence that a host is the reference, and a production host repointed at our own build answers 200 too. And no page has a `matching/SPEC.md` section yet, so the Phase 1 preflight refuses geometry rounds. Both are fail-closed on purpose.
|
|
184
|
+
|
|
154
185
|
---
|
|
155
186
|
|
|
156
187
|
## Audits
|
|
@@ -31,8 +31,8 @@ import {
|
|
|
31
31
|
renderReportHtml,
|
|
32
32
|
resolveCopy
|
|
33
33
|
} from "./chunk-KJGKZLCT.js";
|
|
34
|
-
import "./chunk-5UINJNAJ.js";
|
|
35
34
|
import "./chunk-BGTYPVLT.js";
|
|
35
|
+
import "./chunk-5UINJNAJ.js";
|
|
36
36
|
import {
|
|
37
37
|
listWebsites,
|
|
38
38
|
siteSlug,
|
|
@@ -203,8 +203,8 @@ function formatAnnounceResult(result) {
|
|
|
203
203
|
return result.results.map(formatSiteResult).join("\n");
|
|
204
204
|
}
|
|
205
205
|
async function runAnnounceCommand(site, _opts) {
|
|
206
|
-
const { makeReportMirror } = await import("./report-mirror-
|
|
207
|
-
const { makeSiteMirror } = await import("./site-mirror-
|
|
206
|
+
const { makeReportMirror } = await import("./report-mirror-HDTOJE64.js");
|
|
207
|
+
const { makeSiteMirror } = await import("./site-mirror-WON4FTJ2.js");
|
|
208
208
|
const reportMirror = await makeReportMirror();
|
|
209
209
|
const siteMirror = await makeSiteMirror();
|
|
210
210
|
const result = await announce({ ...site ? { site } : {}, reportMirror, siteMirror });
|
|
@@ -215,4 +215,4 @@ export {
|
|
|
215
215
|
formatAnnounceResult,
|
|
216
216
|
runAnnounceCommand
|
|
217
217
|
};
|
|
218
|
-
//# sourceMappingURL=announce-
|
|
218
|
+
//# sourceMappingURL=announce-CLQYX5I4.js.map
|
|
@@ -5,19 +5,19 @@ import {
|
|
|
5
5
|
appendSkipNotice,
|
|
6
6
|
fleetWorkdir,
|
|
7
7
|
prepareFleetSites
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-LM2NHRCJ.js";
|
|
9
9
|
import {
|
|
10
10
|
resolveSites
|
|
11
11
|
} from "./chunk-Z2K4JPQ3.js";
|
|
12
12
|
import {
|
|
13
13
|
bumpDeps
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
} from "./chunk-CJ2RMTT7.js";
|
|
15
|
+
import "./chunk-ZQV7UQNE.js";
|
|
16
16
|
import "./chunk-Z3JXLULD.js";
|
|
17
17
|
import "./chunk-LBYOLBW7.js";
|
|
18
|
-
import "./chunk-XXTZBPUY.js";
|
|
19
18
|
import "./chunk-RARREDQE.js";
|
|
20
|
-
import "./chunk-
|
|
19
|
+
import "./chunk-XXTZBPUY.js";
|
|
20
|
+
import "./chunk-AVEQUR44.js";
|
|
21
21
|
|
|
22
22
|
// src/cli/commands/bump-deps.ts
|
|
23
23
|
import { resolve } from "path";
|
|
@@ -57,4 +57,4 @@ async function runBumpDepsCommand(site, opts) {
|
|
|
57
57
|
export {
|
|
58
58
|
runBumpDepsCommand
|
|
59
59
|
};
|
|
60
|
-
//# sourceMappingURL=bump-deps-
|
|
60
|
+
//# sourceMappingURL=bump-deps-C5G2EFBB.js.map
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-J7SZQUCW.js";
|
|
4
4
|
import {
|
|
5
5
|
withRecipe
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZQV7UQNE.js";
|
|
7
7
|
|
|
8
8
|
// src/recipes/svelte-codemods.ts
|
|
9
9
|
import { writeFile } from "fs/promises";
|
|
@@ -32,4 +32,4 @@ async function svelteCodemods(site) {
|
|
|
32
32
|
export {
|
|
33
33
|
svelteCodemods
|
|
34
34
|
};
|
|
35
|
-
//# sourceMappingURL=chunk-
|
|
35
|
+
//# sourceMappingURL=chunk-2LYOAB2E.js.map
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from "./chunk-WEKL2HYJ.js";
|
|
28
28
|
import {
|
|
29
29
|
recordFleetEventsBestEffort
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-BI2VI6EM.js";
|
|
31
31
|
|
|
32
32
|
// src/reports/send/idempotency.ts
|
|
33
33
|
function isIdempotencyConflict(err) {
|
|
@@ -227,4 +227,4 @@ export {
|
|
|
227
227
|
parseAddresses,
|
|
228
228
|
isProbablyEmail
|
|
229
229
|
};
|
|
230
|
-
//# sourceMappingURL=chunk-
|
|
230
|
+
//# sourceMappingURL=chunk-2ZAZ7ETT.js.map
|
|
@@ -40,6 +40,27 @@ async function removeFromIndex(cwd, paths) {
|
|
|
40
40
|
if (paths.length === 0) return;
|
|
41
41
|
await git(cwd, ["rm", "-r", "--cached", "--", ...paths]);
|
|
42
42
|
}
|
|
43
|
+
async function pathsMissingFromHead(cwd, paths) {
|
|
44
|
+
if (paths.length === 0) return [];
|
|
45
|
+
let inTree;
|
|
46
|
+
try {
|
|
47
|
+
const { stdout } = await git(cwd, ["ls-tree", "-r", "-z", "--name-only", "HEAD"]);
|
|
48
|
+
inTree = new Set(stdout.split("\0").filter((p) => p.length > 0));
|
|
49
|
+
} catch {
|
|
50
|
+
inTree = /* @__PURE__ */ new Set();
|
|
51
|
+
}
|
|
52
|
+
return paths.filter((p) => !inTree.has(p));
|
|
53
|
+
}
|
|
54
|
+
async function ignoreRulesFor(cwd, paths) {
|
|
55
|
+
if (paths.length === 0) return [];
|
|
56
|
+
let out;
|
|
57
|
+
try {
|
|
58
|
+
({ stdout: out } = await git(cwd, ["check-ignore", "-v", "--no-index", "--", ...paths]));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
out = err.stdout ?? "";
|
|
61
|
+
}
|
|
62
|
+
return out.split("\n").filter((l) => l.length > 0);
|
|
63
|
+
}
|
|
43
64
|
async function commit(cwd, message) {
|
|
44
65
|
await stageAll(cwd);
|
|
45
66
|
const { stdout: status } = await git(cwd, ["status", "--porcelain"]);
|
|
@@ -71,6 +92,29 @@ async function getRemoteUrl(cwd) {
|
|
|
71
92
|
const { stdout } = await git(cwd, ["remote", "get-url", "origin"]);
|
|
72
93
|
return stdout.trim();
|
|
73
94
|
}
|
|
95
|
+
async function resolveOwnerRepo(site) {
|
|
96
|
+
if (site.gitRepo) {
|
|
97
|
+
if (!isOwnerRepo(site.gitRepo)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`refusing to act on malformed repo identity: expected "owner/repo", got ${JSON.stringify(site.gitRepo)}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return site.gitRepo;
|
|
103
|
+
}
|
|
104
|
+
let fromOrigin;
|
|
105
|
+
try {
|
|
106
|
+
fromOrigin = parseOwnerRepo(await getRemoteUrl(site.path));
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
if (fromOrigin === null) return null;
|
|
111
|
+
if (!isOwnerRepo(fromOrigin)) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`refusing to act on malformed repo identity from origin: ${JSON.stringify(fromOrigin)}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return fromOrigin;
|
|
117
|
+
}
|
|
74
118
|
async function push(cwd, branch) {
|
|
75
119
|
await git(cwd, ["push", "-u", "origin", branch]);
|
|
76
120
|
}
|
|
@@ -86,12 +130,15 @@ export {
|
|
|
86
130
|
stageAll,
|
|
87
131
|
listTrackedFiles,
|
|
88
132
|
removeFromIndex,
|
|
133
|
+
pathsMissingFromHead,
|
|
134
|
+
ignoreRulesFor,
|
|
89
135
|
commit,
|
|
90
136
|
OWNER_REPO_RE,
|
|
91
137
|
isOwnerRepo,
|
|
92
138
|
sameOwnerRepo,
|
|
93
139
|
parseOwnerRepo,
|
|
94
140
|
getRemoteUrl,
|
|
141
|
+
resolveOwnerRepo,
|
|
95
142
|
push
|
|
96
143
|
};
|
|
97
|
-
//# sourceMappingURL=chunk-
|
|
144
|
+
//# sourceMappingURL=chunk-AVEQUR44.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/util/git.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport type { Site } from \"../types.js\";\n\nconst exec = promisify(execFile);\n\nasync function git(cwd: string, args: string[]): Promise<{ stdout: string; stderr: string }> {\n return exec(\"git\", args, { cwd, env: process.env });\n}\n\nexport function branchName(recipe: string, when: Date = new Date()): string {\n // ISO with millisecond precision: 2026-05-20T10:30:00.123Z → 20260520T103000123Z.\n // Millis (vs. second-precision) shrinks the collision window for parallel runs.\n const compact = when.toISOString().replace(/[-:.]/g, \"\");\n return `maint/${recipe}-${compact}`;\n}\n\nexport async function currentBranch(cwd: string): Promise<string> {\n const { stdout } = await git(cwd, [\"rev-parse\", \"--abbrev-ref\", \"HEAD\"]);\n return stdout.trim();\n}\n\nexport async function isWorkingTreeClean(cwd: string): Promise<boolean> {\n const { stdout } = await git(cwd, [\"status\", \"--porcelain\"]);\n return stdout.trim().length === 0;\n}\n\nexport async function createBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", \"-b\", name]);\n}\n\n/** Check out an existing branch. Throws (via git) if the branch is missing or\n * the checkout is blocked (e.g. uncommitted changes that would be overwritten). */\nexport async function checkoutBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", name]);\n}\n\n/**\n * Force-check-out an existing branch, DISCARDING any uncommitted changes on the\n * current branch. Used by the recipe failure path to return the operator to\n * their original branch even when a recipe left the work-in-progress branch\n * dirty. Only ever called with the operator's ORIGINAL branch as `name`. Does\n * not run `git clean`, so untracked operator files are left untouched.\n */\nexport async function forceCheckoutBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"checkout\", \"-f\", name]);\n}\n\n/**\n * Delete a local branch with `-D` (force). Used by the recipe failure path to\n * remove the branch the recipe itself created so a re-run starts clean. Callers\n * MUST only ever pass the recipe-created branch here, never the operator's\n * original branch.\n */\nexport async function deleteBranch(cwd: string, name: string): Promise<void> {\n await git(cwd, [\"branch\", \"-D\", name]);\n}\n\nexport async function stageAll(cwd: string): Promise<void> {\n await git(cwd, [\"add\", \"-A\"]);\n}\n\nexport async function listTrackedFiles(cwd: string): Promise<string[]> {\n const { stdout } = await git(cwd, [\"ls-files\"]);\n return stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n}\n\nexport async function removeFromIndex(cwd: string, paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n await git(cwd, [\"rm\", \"-r\", \"--cached\", \"--\", ...paths]);\n}\n\n/**\n * The subset of `paths` that is NOT in HEAD's tree — what a fresh clone would\n * NOT get. POSITIVE evidence: a path counts as installed only when it is FOUND\n * in `git ls-tree`'s output. `git add -A` honours .gitignore and exits 0 either\n * way, so \"staging did not error\" proves nothing about what landed.\n *\n * `-z`, because `ls-tree` C-quotes any path outside core.quotePath's safe set\n * (`\"caf\\303\\251.txt\"`), which would read as missing. NOT `--full-tree`:\n * without it `ls-tree` reports paths relative to `cwd`, the same base the\n * caller's relative paths use.\n *\n * A git failure (no HEAD yet, not a repo) yields an EMPTY tree, so every path\n * comes back missing. An error may only ever DENY.\n */\nexport async function pathsMissingFromHead(\n cwd: string,\n paths: readonly string[],\n): Promise<string[]> {\n if (paths.length === 0) return [];\n let inTree: Set<string>;\n try {\n const { stdout } = await git(cwd, [\"ls-tree\", \"-r\", \"-z\", \"--name-only\", \"HEAD\"]);\n inTree = new Set(stdout.split(\"\\0\").filter((p) => p.length > 0));\n } catch {\n inTree = new Set();\n }\n return paths.filter((p) => !inTree.has(p));\n}\n\n/**\n * Why git is excluding `paths`: `<source>:<line>:<pattern>\\t<path>` rows from\n * `git check-ignore -v --no-index`. A path with no row is absent for some other\n * reason (a nested repository, say) and the caller must say so rather than\n * guess. Never throws: check-ignore exits 1 when NOTHING matches and 128 on\n * error, and both must read as \"no rule found\" — the caller is already\n * reporting a failure and must not lose it to a second one.\n */\nexport async function ignoreRulesFor(cwd: string, paths: readonly string[]): Promise<string[]> {\n if (paths.length === 0) return [];\n let out: string;\n try {\n ({ stdout: out } = await git(cwd, [\"check-ignore\", \"-v\", \"--no-index\", \"--\", ...paths]));\n } catch (err) {\n out = (err as { stdout?: string }).stdout ?? \"\";\n }\n return out.split(\"\\n\").filter((l) => l.length > 0);\n}\n\n/**\n * Stages all current changes and commits with `message`. Returns the commit SHA,\n * or `null` if there was nothing to commit.\n */\nexport async function commit(cwd: string, message: string): Promise<string | null> {\n await stageAll(cwd);\n const { stdout: status } = await git(cwd, [\"status\", \"--porcelain\"]);\n if (status.trim().length === 0) return null;\n await git(cwd, [\"commit\", \"-m\", message]);\n const { stdout: sha } = await git(cwd, [\"rev-parse\", \"HEAD\"]);\n return sha.trim();\n}\n\n/**\n * Strict GitHub repo identity: exactly two `[A-Za-z0-9._-]` segments separated\n * by a single slash. Rejects a scheme, host, extra path segment, traversal\n * (`..`), whitespace, or an argv flag — anything that could retarget a `gh`\n * write at an unintended (attacker/typo-controlled) repo.\n */\nexport const OWNER_REPO_RE = /^[A-Za-z0-9._-]+\\/[A-Za-z0-9._-]+$/;\n\n/** True when `repo` is a clean `owner/repo` (see {@link OWNER_REPO_RE}). The\n * explicit `..` reject covers traversal segments the char-class would otherwise\n * admit (`.` is a legal repo char, so `../evil` matches the shape regex). */\nexport function isOwnerRepo(repo: string): boolean {\n if (repo.includes(\"..\")) return false;\n return OWNER_REPO_RE.test(repo);\n}\n\n/**\n * True when two repo references name the same `owner/repo`. Each side may be a\n * full remote URL (https or scp-style), or already an `owner/repo`. Used to\n * verify an existing checkout's `origin` matches the site's expected repo\n * before reusing it. Returns false if either side is unparseable.\n */\nexport function sameOwnerRepo(a: string, b: string): boolean {\n const na = isOwnerRepo(a.trim()) ? a.trim() : parseOwnerRepo(a);\n const nb = isOwnerRepo(b.trim()) ? b.trim() : parseOwnerRepo(b);\n if (na === null || nb === null) return false;\n return na.toLowerCase() === nb.toLowerCase();\n}\n\n/** Derive `owner/repo` from a git remote URL (https or scp-style). Null if unparseable. */\nexport function parseOwnerRepo(remoteUrl: string): string | null {\n const trimmed = remoteUrl\n .trim()\n .replace(/\\.git$/, \"\")\n .replace(/\\/$/, \"\");\n // scp-style: git@github.com:owner/repo\n const scp = trimmed.match(/^[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:(.+)$/);\n const path = scp ? scp[1]! : trimmed.replace(/^https?:\\/\\/[^/]+\\//, \"\");\n const segments = path.split(\"/\").filter(Boolean);\n if (segments.length < 2) return null;\n return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;\n}\n\n/** `origin` remote URL for a checkout, trimmed. Throws (via git) if there's no origin. */\nexport async function getRemoteUrl(cwd: string): Promise<string> {\n const { stdout } = await git(cwd, [\"remote\", \"get-url\", \"origin\"]);\n return stdout.trim();\n}\n\n/**\n * Resolve the `owner/repo` a recipe will act on. An explicit `site.gitRepo`\n * (Airtable, or a JSON inventory) wins; otherwise derive it from the checkout's\n * `origin`.\n *\n * Returns `null` when there is nothing to act on (no `gitRepo`, no origin) — a\n * benign \"nothing wired\" state. THROWS when a value IS present but does not\n * match the strict `owner/repo` shape: callers write repo secrets, branch\n * protection and pull requests at this identity, so a typo'd or\n * attacker-controlled value must be rejected here, before the first `gh` call.\n *\n * Extracted from `self-updating`'s private `resolveRepo` because `prismic-ci`\n * read `site.gitRepo` directly and therefore refused every POSITIONAL run with\n * \"no Git repo on this site\" — `localPath()` (src/inventory/local.ts:11) builds\n * `{ path, name }` and nothing else. A site being bootstrapped is exactly the\n * case that has no Airtable row yet (`--fleet airtable` filters pre-launch\n * statuses), so the positional path is the ONLY one `/new-site` can use.\n */\nexport async function resolveOwnerRepo(site: Site): Promise<string | null> {\n if (site.gitRepo) {\n if (!isOwnerRepo(site.gitRepo)) {\n throw new Error(\n `refusing to act on malformed repo identity: expected \"owner/repo\", got ${JSON.stringify(site.gitRepo)}`,\n );\n }\n return site.gitRepo;\n }\n let fromOrigin: string | null;\n try {\n fromOrigin = parseOwnerRepo(await getRemoteUrl(site.path));\n } catch {\n return null;\n }\n if (fromOrigin === null) return null;\n if (!isOwnerRepo(fromOrigin)) {\n throw new Error(\n `refusing to act on malformed repo identity from origin: ${JSON.stringify(fromOrigin)}`,\n );\n }\n return fromOrigin;\n}\n\n/** Push a branch to origin, setting upstream. Throws on non-zero (execFile rejects). */\nexport async function push(cwd: string, branch: string): Promise<void> {\n await git(cwd, [\"push\", \"-u\", \"origin\", branch]);\n}\n"],"mappings":";AAAA,SAAS,gBAAgB;AACzB,SAAS,iBAAiB;AAG1B,IAAM,OAAO,UAAU,QAAQ;AAE/B,eAAe,IAAI,KAAa,MAA6D;AAC3F,SAAO,KAAK,OAAO,MAAM,EAAE,KAAK,KAAK,QAAQ,IAAI,CAAC;AACpD;AAEO,SAAS,WAAW,QAAgB,OAAa,oBAAI,KAAK,GAAW;AAG1E,QAAM,UAAU,KAAK,YAAY,EAAE,QAAQ,UAAU,EAAE;AACvD,SAAO,SAAS,MAAM,IAAI,OAAO;AACnC;AAEA,eAAsB,cAAc,KAA8B;AAChE,QAAM,EAAE,OAAO,IAAI,MAAM,IAAI,KAAK,CAAC,aAAa,gBAAgB,MAAM,CAAC;AACvE,SAAO,OAAO,KAAK;AACrB;AAEA,eAAsB,mBAAmB,KAA+B;AACtE,QAAM,EAAE,OAAO,IAAI,MAAM,IAAI,KAAK,CAAC,UAAU,aAAa,CAAC;AAC3D,SAAO,OAAO,KAAK,EAAE,WAAW;AAClC;AAEA,eAAsB,aAAa,KAAa,MAA6B;AAC3E,QAAM,IAAI,KAAK,CAAC,YAAY,MAAM,IAAI,CAAC;AACzC;AAIA,eAAsB,eAAe,KAAa,MAA6B;AAC7E,QAAM,IAAI,KAAK,CAAC,YAAY,IAAI,CAAC;AACnC;AASA,eAAsB,oBAAoB,KAAa,MAA6B;AAClF,QAAM,IAAI,KAAK,CAAC,YAAY,MAAM,IAAI,CAAC;AACzC;AAQA,eAAsB,aAAa,KAAa,MAA6B;AAC3E,QAAM,IAAI,KAAK,CAAC,UAAU,MAAM,IAAI,CAAC;AACvC;AAEA,eAAsB,SAAS,KAA4B;AACzD,QAAM,IAAI,KAAK,CAAC,OAAO,IAAI,CAAC;AAC9B;AAEA,eAAsB,iBAAiB,KAAgC;AACrE,QAAM,EAAE,OAAO,IAAI,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC;AAC9C,SAAO,OACJ,MAAM,IAAI,EACV,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AAC/B;AAEA,eAAsB,gBAAgB,KAAa,OAAgC;AACjF,MAAI,MAAM,WAAW,EAAG;AACxB,QAAM,IAAI,KAAK,CAAC,MAAM,MAAM,YAAY,MAAM,GAAG,KAAK,CAAC;AACzD;AAgBA,eAAsB,qBACpB,KACA,OACmB;AACnB,MAAI,MAAM,WAAW,EAAG,QAAO,CAAC;AAChC,MAAI;AACJ,MAAI;AACF,UAAM,EAAE,OAAO,IAAI,MAAM,IAAI,KAAK,CAAC,WAAW,MAAM,MAAM,eAAe,MAAM,CAAC;AAChF,aAAS,IAAI,IAAI,OAAO,MAAM,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAAA,EACjE,QAAQ;AACN,aAAS,oBAAI,IAAI;AAAA,EACnB;AACA,SAAO,MAAM,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;AAC3C;AAUA,eAAsB,eAAe,KAAa,OAA6C;AAC7F,MAAI,MAAM,WAAW,EAAG,QAAO,CAAC;AAChC,MAAI;AACJ,MAAI;AACF,KAAC,EAAE,QAAQ,IAAI,IAAI,MAAM,IAAI,KAAK,CAAC,gBAAgB,MAAM,cAAc,MAAM,GAAG,KAAK,CAAC;AAAA,EACxF,SAAS,KAAK;AACZ,UAAO,IAA4B,UAAU;AAAA,EAC/C;AACA,SAAO,IAAI,MAAM,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC;AACnD;AAMA,eAAsB,OAAO,KAAa,SAAyC;AACjF,QAAM,SAAS,GAAG;AAClB,QAAM,EAAE,QAAQ,OAAO,IAAI,MAAM,IAAI,KAAK,CAAC,UAAU,aAAa,CAAC;AACnE,MAAI,OAAO,KAAK,EAAE,WAAW,EAAG,QAAO;AACvC,QAAM,IAAI,KAAK,CAAC,UAAU,MAAM,OAAO,CAAC;AACxC,QAAM,EAAE,QAAQ,IAAI,IAAI,MAAM,IAAI,KAAK,CAAC,aAAa,MAAM,CAAC;AAC5D,SAAO,IAAI,KAAK;AAClB;AAQO,IAAM,gBAAgB;AAKtB,SAAS,YAAY,MAAuB;AACjD,MAAI,KAAK,SAAS,IAAI,EAAG,QAAO;AAChC,SAAO,cAAc,KAAK,IAAI;AAChC;AAQO,SAAS,cAAc,GAAW,GAAoB;AAC3D,QAAM,KAAK,YAAY,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,eAAe,CAAC;AAC9D,QAAM,KAAK,YAAY,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,IAAI,eAAe,CAAC;AAC9D,MAAI,OAAO,QAAQ,OAAO,KAAM,QAAO;AACvC,SAAO,GAAG,YAAY,MAAM,GAAG,YAAY;AAC7C;AAGO,SAAS,eAAe,WAAkC;AAC/D,QAAM,UAAU,UACb,KAAK,EACL,QAAQ,UAAU,EAAE,EACpB,QAAQ,OAAO,EAAE;AAEpB,QAAM,MAAM,QAAQ,MAAM,wCAAwC;AAClE,QAAM,OAAO,MAAM,IAAI,CAAC,IAAK,QAAQ,QAAQ,uBAAuB,EAAE;AACtE,QAAM,WAAW,KAAK,MAAM,GAAG,EAAE,OAAO,OAAO;AAC/C,MAAI,SAAS,SAAS,EAAG,QAAO;AAChC,SAAO,GAAG,SAAS,SAAS,SAAS,CAAC,CAAC,IAAI,SAAS,SAAS,SAAS,CAAC,CAAC;AAC1E;AAGA,eAAsB,aAAa,KAA8B;AAC/D,QAAM,EAAE,OAAO,IAAI,MAAM,IAAI,KAAK,CAAC,UAAU,WAAW,QAAQ,CAAC;AACjE,SAAO,OAAO,KAAK;AACrB;AAoBA,eAAsB,iBAAiB,MAAoC;AACzE,MAAI,KAAK,SAAS;AAChB,QAAI,CAAC,YAAY,KAAK,OAAO,GAAG;AAC9B,YAAM,IAAI;AAAA,QACR,0EAA0E,KAAK,UAAU,KAAK,OAAO,CAAC;AAAA,MACxG;AAAA,IACF;AACA,WAAO,KAAK;AAAA,EACd;AACA,MAAI;AACJ,MAAI;AACF,iBAAa,eAAe,MAAM,aAAa,KAAK,IAAI,CAAC;AAAA,EAC3D,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,eAAe,KAAM,QAAO;AAChC,MAAI,CAAC,YAAY,UAAU,GAAG;AAC5B,UAAM,IAAI;AAAA,MACR,2DAA2D,KAAK,UAAU,UAAU,CAAC;AAAA,IACvF;AAAA,EACF;AACA,SAAO;AACT;AAGA,eAAsB,KAAK,KAAa,QAA+B;AACrE,QAAM,IAAI,KAAK,CAAC,QAAQ,MAAM,UAAU,MAAM,CAAC;AACjD;","names":[]}
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-J7SZQUCW.js";
|
|
4
4
|
import {
|
|
5
5
|
withRecipe
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZQV7UQNE.js";
|
|
7
7
|
import {
|
|
8
8
|
bumpDep,
|
|
9
9
|
readPackageJson,
|
|
@@ -243,4 +243,4 @@ async function upgradeSvelte4to5(site, opts = {}) {
|
|
|
243
243
|
export {
|
|
244
244
|
upgradeSvelte4to5
|
|
245
245
|
};
|
|
246
|
-
//# sourceMappingURL=chunk-
|
|
246
|
+
//# sourceMappingURL=chunk-BCKEP5S6.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
openDb,
|
|
3
3
|
readDbConfig
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-VSGMCCGQ.js";
|
|
5
5
|
|
|
6
6
|
// src/db/fleet-events.ts
|
|
7
7
|
async function recordFleetEvent(db, e) {
|
|
@@ -47,4 +47,4 @@ async function recordFleetEventsBestEffort(events, now, open = () => openDb(read
|
|
|
47
47
|
export {
|
|
48
48
|
recordFleetEventsBestEffort
|
|
49
49
|
};
|
|
50
|
-
//# sourceMappingURL=chunk-
|
|
50
|
+
//# sourceMappingURL=chunk-BI2VI6EM.js.map
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
onboard
|
|
3
|
+
} from "./chunk-S7RYFUIK.js";
|
|
1
4
|
import {
|
|
2
5
|
convertToPnpm
|
|
3
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-CWXTNPNZ.js";
|
|
4
7
|
import {
|
|
5
8
|
svelteCodemods
|
|
6
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-2LYOAB2E.js";
|
|
7
10
|
import {
|
|
8
11
|
healthEndpoint
|
|
9
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-WSJ7RO54.js";
|
|
10
13
|
import {
|
|
11
14
|
smokeSuite
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import {
|
|
14
|
-
onboard
|
|
15
|
-
} from "./chunk-NTLSMJHW.js";
|
|
15
|
+
} from "./chunk-DFKBTBO3.js";
|
|
16
16
|
import {
|
|
17
17
|
syncConfigs
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-FUWFLAWQ.js";
|
|
19
19
|
import {
|
|
20
20
|
withRecipe
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-ZQV7UQNE.js";
|
|
22
22
|
import {
|
|
23
23
|
runAudits
|
|
24
24
|
} from "./chunk-TO7DJYLZ.js";
|
|
@@ -143,4 +143,4 @@ export {
|
|
|
143
143
|
DEFAULT_INIT_STEPS,
|
|
144
144
|
init
|
|
145
145
|
};
|
|
146
|
-
//# sourceMappingURL=chunk-
|
|
146
|
+
//# sourceMappingURL=chunk-BVEDKMNG.js.map
|
|
@@ -383,6 +383,31 @@ var MIGRATIONS = [
|
|
|
383
383
|
CREATE INDEX IF NOT EXISTS idx_prospect_audits_site_key
|
|
384
384
|
ON prospect_audits (site_key, created_at DESC);
|
|
385
385
|
`
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
// Operator edits on a prospect report. One JSON blob rather than a side
|
|
389
|
+
// table: the map is always read and written whole, and no query wants a
|
|
390
|
+
// single override in isolation. Same shape of decision as `result_json`.
|
|
391
|
+
//
|
|
392
|
+
// THREE MIGRATIONS, NOT ONE. Verified against migrate.ts: it runs
|
|
393
|
+
// `executeMultiple(m.sql)` inside a try, swallows any "duplicate column
|
|
394
|
+
// name" error, then records the marker unconditionally. That recovery is
|
|
395
|
+
// sound for ONE statement. With three, a crash partway through leaves the
|
|
396
|
+
// marker unwritten; the re-run's first statement then throws duplicate,
|
|
397
|
+
// executeMultiple aborts before the later statements, the catch swallows
|
|
398
|
+
// it, and the marker IS written — leaving the remaining columns
|
|
399
|
+
// permanently missing on a migration recorded as applied. Same rule as
|
|
400
|
+
// 0003 and 0013.
|
|
401
|
+
id: "0015_prospect_audits_overrides",
|
|
402
|
+
sql: `ALTER TABLE prospect_audits ADD COLUMN overrides_json TEXT;`
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
id: "0016_prospect_audits_edited_at",
|
|
406
|
+
sql: `ALTER TABLE prospect_audits ADD COLUMN edited_at TEXT;`
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
id: "0017_prospect_audits_opened_at",
|
|
410
|
+
sql: `ALTER TABLE prospect_audits ADD COLUMN opened_at TEXT;`
|
|
386
411
|
}
|
|
387
412
|
];
|
|
388
413
|
|
|
@@ -417,4 +442,4 @@ async function runMigrations(client) {
|
|
|
417
442
|
export {
|
|
418
443
|
runMigrations
|
|
419
444
|
};
|
|
420
|
-
//# sourceMappingURL=chunk-
|
|
445
|
+
//# sourceMappingURL=chunk-C3IQQKXL.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/db/migrations.ts","../src/db/migrate.ts"],"sourcesContent":["/** Ordered, append-only list of standard-SQL migration scripts. Each runs once,\n * tracked by `id` in the `_migrations` table (see migrate.ts). Statements use\n * IF NOT EXISTS so even a partial re-apply is safe. Never edit a shipped script —\n * add a new one. Every statement in a script MUST be independently idempotent:\n * `migrate.ts` runs each script via `executeMultiple`, which is NOT transactional,\n * so a mid-script failure can leave earlier statements applied and the id unrecorded\n * — a re-run then re-executes the whole script. Standard SQLite SQL only (no\n * Turso-specific syntax) so the host stays a connection-string swap. */\nexport type Migration = { id: string; sql: string };\n\nexport const MIGRATIONS: Migration[] = [\n {\n id: \"0001_init\",\n sql: `\n CREATE TABLE IF NOT EXISTS submissions (\n id TEXT PRIMARY KEY,\n submission_id INTEGER,\n site_id TEXT NOT NULL,\n form_type TEXT NOT NULL,\n name TEXT NOT NULL,\n email TEXT NOT NULL,\n phone TEXT,\n message TEXT,\n extra_fields TEXT,\n source_url TEXT,\n utm TEXT,\n submitted_at TEXT,\n status TEXT NOT NULL DEFAULT 'new',\n notify_status TEXT NOT NULL DEFAULT 'skipped',\n resend_message_id TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_submissions_site_submitted\n ON submissions (site_id, submitted_at DESC);\n CREATE INDEX IF NOT EXISTS idx_submissions_status\n ON submissions (status);\n CREATE TABLE IF NOT EXISTS spam_screenouts (\n site_id TEXT NOT NULL,\n date TEXT NOT NULL,\n honeypot INTEGER NOT NULL DEFAULT 0,\n too_fast INTEGER NOT NULL DEFAULT 0,\n marked_spam INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (site_id, date)\n );\n `,\n },\n {\n id: \"0002_fleet_events\",\n sql: `\n CREATE TABLE IF NOT EXISTS fleet_events (\n id TEXT PRIMARY KEY,\n ts TEXT NOT NULL,\n type TEXT NOT NULL,\n site_id TEXT,\n site_name TEXT,\n summary TEXT NOT NULL,\n data TEXT,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_fleet_events_ts ON fleet_events (ts);\n `,\n },\n {\n id: \"0003_add_spam_score\",\n // SQLite `ADD COLUMN` has no `IF NOT EXISTS`, so it is NOT self-idempotent: if the\n // `_migrations` marker is lost after the column is added (crash between statement\n // and marker write), a re-run would throw `duplicate column name`. `migrate.ts`\n // recognizes that error as already-applied and records the marker, keeping the\n // runner idempotent (see `isAlreadyAppliedError`).\n sql: `ALTER TABLE submissions ADD COLUMN spam_score REAL;`,\n },\n {\n id: \"0004_add_spam_reason\",\n sql: `ALTER TABLE submissions ADD COLUMN spam_reason TEXT;`,\n },\n {\n // Outcome of the newsletter fan-out (site webhook + Mailchimp). Before this the\n // results were console.error-only, so a rotated Mailchimp key or an outage would\n // silently stop signups reaching the audience with no trace anywhere the operator\n // looks — the row still read `notify=sent`, healthy. Same non-idempotent\n // ADD COLUMN caveat as 0003.\n id: \"0005_add_fanout_status\",\n sql: `ALTER TABLE submissions ADD COLUMN fanout_status TEXT;`,\n },\n {\n // Phase 0 of the Airtable → Turso migration (#539): a lead whose SITE LOOKUP\n // fails must still land somewhere durable. On 2026-08-17 the Airtable quota\n // outage made `getWebsiteBySlug` throw before `createSubmission` ran, so every\n // lead in the window 502'd away unrecorded — while THIS store (which holds\n // submissions) was healthy the whole time. Rows here are written only on that\n // path and replayed through the normal pipeline once the lookup recovers\n // (`db replay-deadletters`), producing an ordinary submissions row with real\n // spam classification and notify. Deliberately a separate table rather than a\n // nullable `submissions.site_id`: site_id is NOT NULL, every reader assumes a\n // real rec id, and a sentinel would collapse dashboard grouping (the \"\" key\n // hazard audit.ts already documents).\n //\n // `turnstile` stores the verification COMPUTED AT RECEIPT ({outcome, hostname}\n // JSON) — tokens expire in 300s, so replay could never re-verify; it replays\n // with the answer we already had.\n id: \"0006_submission_deadletter\",\n sql: `\n CREATE TABLE IF NOT EXISTS submission_deadletter (\n id TEXT PRIMARY KEY,\n site_slug TEXT NOT NULL,\n payload TEXT NOT NULL,\n turnstile TEXT NOT NULL,\n error TEXT NOT NULL,\n received_at TEXT NOT NULL,\n replayed_at TEXT,\n replay_outcome TEXT,\n replay_submission_id TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_deadletter_unreplayed\n ON submission_deadletter (replayed_at);\n `,\n },\n {\n // Phase 1.2 of the Airtable → Turso migration (#539): the fleet-state tables.\n // Split by WRITER, not topic (design D2, confirmed by the 2026-08-23 writer\n // map — every code-written column has exactly one writer):\n // sites — operator (dashboard editor / console / launch flow)\n // site_health — the nightly audit write-back, one batched upsert\n // site_schedule — updateNextDueDates (report cron, derived)\n // reports — report drafting + the operator approve flow\n // PKs are the Airtable `rec…` ids (design D1): 278+ submissions rows already\n // reference them, and the parity harness diffs the two stores row-for-row.\n //\n // Naming: snake_case throughout; Airtable's misspellings (\"maintenence\") and\n // display quirks die here. `sites.legacy` is a JSON object holding the 33\n // populated-but-code-unreferenced columns (launch-era checklist, hosting\n // reference cells) keyed by their original Airtable column name — data kept,\n // schema not fossilized. The plaintext DNS/cms credential cells deliberately\n // do NOT migrate at all (operator ruling 2026-08-23); they live on only in\n // the frozen base. `site_health.analytics_soft_fail_at` is the column code\n // always wanted (\"Analytics soft-fail at\") but no operator ever created in\n // Airtable — it ships real here.\n //\n // reports.checklist is JSON keyed by the STABLE checklist key (\"deploy\",\n // \"cms\", …, from src/reports/checklist.ts), not the Airtable column name —\n // the importer translates, so \"Test: Verified After Updates\" stops leaking\n // its legacy name into a second store.\n id: \"0007_fleet_state\",\n sql: `\n CREATE TABLE IF NOT EXISTS sites (\n id TEXT PRIMARY KEY,\n slug TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n url TEXT,\n status TEXT,\n point_of_contact TEXT,\n maintenance_freq TEXT,\n testing_freq TEXT,\n maintenance_day TEXT,\n testing_day TEXT,\n ga4_property_id TEXT,\n search_query TEXT,\n search_console_property TEXT,\n git_repo TEXT,\n netlify_id TEXT,\n report_recipients_to TEXT,\n report_recipients_cc TEXT,\n copy_intro TEXT,\n copy_contact TEXT,\n copy_footer TEXT,\n newsletter_webhook TEXT,\n mailchimp_api_key TEXT,\n mailchimp_audience_id TEXT,\n notify_routing TEXT,\n require_turnstile INTEGER NOT NULL DEFAULT 0,\n accepted_watch_conditions TEXT,\n prismic_ack_until TEXT,\n launched_at TEXT,\n header_image BLOB,\n header_image_filename TEXT,\n header_image_type TEXT,\n header_image_generated_at TEXT,\n legacy TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_sites_status ON sites (status);\n\n CREATE TABLE IF NOT EXISTS site_health (\n site_id TEXT PRIMARY KEY,\n p_score INTEGER,\n r_score INTEGER,\n bp_score INTEGER,\n seo_score INTEGER,\n lighthouse_at TEXT,\n a11y_violations INTEGER,\n deps_drifted INTEGER,\n deps_major_behind INTEGER,\n deps_outdated INTEGER,\n deps_major_outdated INTEGER,\n vulns_critical INTEGER,\n vulns_high INTEGER,\n vulns_moderate INTEGER,\n vulns_low INTEGER,\n security_audit_at TEXT,\n security_advisories TEXT,\n auto_fix_attempts INTEGER,\n analytics_soft_fail_at TEXT,\n cert_days_remaining INTEGER,\n domain_checked_at TEXT,\n deploy_status TEXT,\n last_deploy_at TEXT,\n deploy_log_url TEXT,\n deploy_checked_at TEXT,\n function_health TEXT,\n cms_reachable TEXT,\n turnstile_widget TEXT,\n function_health_checked_at TEXT,\n crossbrowser_ok INTEGER,\n mobile_ok INTEGER,\n links_ok INTEGER,\n broken_links INTEGER,\n browser_checked_at TEXT,\n uptime_reachable TEXT,\n titles_meta_ok TEXT,\n smoke_ok TEXT,\n last_smoke_at TEXT,\n form_e2e_ok TEXT,\n form_e2e_checked_at TEXT,\n renovate_failing_cis INTEGER,\n default_branch_ci TEXT,\n last_commit_at TEXT,\n github_signals_at TEXT,\n prismic_models TEXT,\n prismic_models_checked_at TEXT,\n prismic_models_drift TEXT\n );\n\n CREATE TABLE IF NOT EXISTS site_schedule (\n site_id TEXT PRIMARY KEY,\n next_maintenance_at TEXT,\n next_testing_at TEXT,\n computed_at TEXT\n );\n\n CREATE TABLE IF NOT EXISTS reports (\n id TEXT PRIMARY KEY,\n site_id TEXT,\n report_id TEXT,\n report_type TEXT,\n period TEXT,\n period_start TEXT,\n period_end TEXT,\n completed_on TEXT,\n lighthouse_performance INTEGER,\n lighthouse_accessibility INTEGER,\n lighthouse_best_practices INTEGER,\n lighthouse_seo INTEGER,\n ga_users_current INTEGER,\n ga_users_previous INTEGER,\n search_found_page1 INTEGER,\n search_position REAL,\n last_tested_date TEXT,\n commentary TEXT,\n subject_override TEXT,\n draft_ready INTEGER NOT NULL DEFAULT 0,\n approved_to_send INTEGER NOT NULL DEFAULT 0,\n approved_at TEXT,\n approved_by TEXT,\n send_override INTEGER NOT NULL DEFAULT 0,\n override_reason TEXT,\n override_by TEXT,\n override_at TEXT,\n sent_at TEXT,\n delivery_status TEXT,\n resend_message_id TEXT,\n checklist TEXT,\n checklist_auto_evidence TEXT,\n rendered_html TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_reports_site ON reports (site_id, period_start DESC);\n `,\n },\n {\n // Indexes the EXPLAIN-query-plan gate (tests/db/query-plans.test.ts) proved the\n // hot paths need — each serves a named query shape that otherwise full-scans\n // submissions, the one unbounded-growth table:\n // submitted_at — every windowed read (/submissions default page, digest\n // per-site counts, duplicate/repeat-sender scans)\n // resend_message_id — the resend-webhook bounce lookup (markNotifyBouncedByMessageId)\n // submission_id — createSubmission's MAX(submission_id)+1 display number,\n // O(1) off the index instead of a scan per insert\n // spam_reason (partial, covering) — listSpamReasonsFiltered's facet tally\n // reads ONLY spam_reason WHERE spam_reason IS NOT NULL;\n // partial keeps it to the spam subset\n // spam_screenouts(date) — listScreenOutsSince windows on date, which the\n // (site_id, date) PK can't serve\n id: \"0008_query_plan_indexes\",\n sql: `\n CREATE INDEX IF NOT EXISTS idx_submissions_submitted\n ON submissions (submitted_at DESC);\n CREATE INDEX IF NOT EXISTS idx_submissions_spam_reason\n ON submissions (spam_reason) WHERE spam_reason IS NOT NULL;\n CREATE INDEX IF NOT EXISTS idx_submissions_resend_message\n ON submissions (resend_message_id);\n CREATE INDEX IF NOT EXISTS idx_submissions_display_number\n ON submissions (submission_id);\n CREATE INDEX IF NOT EXISTS idx_screenouts_date\n ON spam_screenouts (date);\n `,\n },\n {\n // Prospect audits (spec 2026-08-24): one row per `prospect-audit` CLI run.\n // `token` is the unguessable public handle for GET /r/{token}; `result_json`\n // is the full ProspectAuditResult, re-rendered on every request so report\n // styling updates apply to already-shared links.\n id: \"0009_prospect_audits\",\n sql: `\n CREATE TABLE IF NOT EXISTS prospect_audits (\n id TEXT PRIMARY KEY,\n token TEXT NOT NULL UNIQUE,\n url TEXT NOT NULL,\n business TEXT,\n created_at TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'complete',\n result_json TEXT NOT NULL\n );\n `,\n },\n {\n // Cockpit /audits listing page (spec 2026-08-25, Piece 5):\n // listRecentProspectAudits orders by created_at DESC with no WHERE clause —\n // without this index that's a raw SCAN of prospect_audits followed by a temp\n // b-tree sort, which the EXPLAIN-query-plan gate (tests/db/query-plans.test.ts)\n // fails the build on. DESC matches the query's own ORDER BY so SQLite can walk\n // the index directly instead of sorting.\n id: \"0010_prospect_audits_created_index\",\n sql: `\n CREATE INDEX IF NOT EXISTS idx_prospect_audits_created\n ON prospect_audits (created_at DESC);\n `,\n },\n {\n // #609 (#539 Phase 5): the digest's prior-run snapshot moves off Airtable.\n // Unlike the rest of Phase 5 this is a MIGRATION, not a dual-write — there\n // is no Airtable counterpart left afterwards, so parity does not cover it.\n //\n // A single row holding the whole snapshot as JSON, keyed by a constant id.\n // Both readers (runDigest's diff and the fleet homepage's NEW badges) need\n // the ENTIRE map, so a keyed table would buy nothing on reads and its\n // \"give me everything\" query would be a raw scan needing a justified entry\n // in the EXPLAIN gate's allowlist. One row by primary key needs neither.\n id: \"0011_digest_state\",\n sql: `\n CREATE TABLE IF NOT EXISTS digest_state (\n id TEXT PRIMARY KEY,\n snapshot TEXT NOT NULL,\n updated_at TEXT\n );\n `,\n },\n {\n // form_type was the one SubmissionFilter key with an equality predicate and no\n // index — countSubmissionsFiltered({formType}) raw-scanned `submissions`, twice\n // per load of the submissions page. It shipped green because the EXPLAIN gate\n // named its scenarios by hand and never wrote one for this shape; the gate is\n // now driven by a filter matrix, which is what surfaced it.\n //\n // `submissions` is the one unbounded-growth table in the schema — append-only,\n // one row per fleet lead forever — and Turso meters ROW SCANS, so this is the\n // cost that grows without a ceiling.\n id: \"0012_submissions_form_type_index\",\n sql: `\n CREATE INDEX IF NOT EXISTS idx_submissions_form_type\n ON submissions (form_type);\n `,\n },\n {\n // `url` is stored exactly as audited, which makes it a record of the run and\n // a bad thing to group by: the table already holds beachfrontdentistry.com\n // and beachfrontdentistry.com/ as two histories of one practice, and both\n // spellings of ludlowkingsley.com. That costs nothing today because nothing\n // compares two audits of one site — and becomes a returning client with no\n // history the day the report offers a before and an after.\n //\n // Deliberately no SQL backfill. Doing it with lower()/replace()/rtrim()\n // would mangle any URL containing \"www.\" or \"http://\" away from its start,\n // and a key that is quietly wrong is worse than one that is null. Existing\n // rows are backfilled once with the real siteKey().\n // Same ADD COLUMN caveat as 0003: no IF NOT EXISTS in SQLite, so the runner\n // treats \"duplicate column name\" as already-applied. Kept as its own\n // migration, separate from its index below, so that recovery path stays a\n // single statement.\n id: \"0013_prospect_audits_site_key\",\n sql: `ALTER TABLE prospect_audits ADD COLUMN site_key TEXT;`,\n },\n {\n id: \"0014_prospect_audits_site_key_index\",\n sql: `\n CREATE INDEX IF NOT EXISTS idx_prospect_audits_site_key\n ON prospect_audits (site_key, created_at DESC);\n `,\n },\n];\n","import type { Client } from \"@libsql/client\";\nimport { MIGRATIONS } from \"./migrations.js\";\n\n/** True when an error means \"this DDL was already applied\" and re-applying is a\n * no-op we can safely ignore. `CREATE ... IF NOT EXISTS` never throws, but SQLite\n * `ALTER TABLE ... ADD COLUMN` has no `IF NOT EXISTS`, so a re-run after a lost\n * `_migrations` marker throws `duplicate column name: …`. Treating that as\n * already-applied keeps `runMigrations` idempotent for `ADD COLUMN` migrations —\n * without it a lost marker would boot-loop `openDb()` on every call. */\nfunction isAlreadyAppliedError(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n return /duplicate column name/i.test(message);\n}\n\n/** Apply every not-yet-applied migration in order against a raw libSQL client,\n * tracking applied ids in `_migrations`. Idempotent: ids already present are\n * skipped, the DDL uses IF NOT EXISTS where it can, and an already-applied\n * `ADD COLUMN` (recognized by its error) is swallowed so the marker is still\n * recorded. Returns the ids applied this run. Runs on every openDb() — cheap\n * (one indexed SELECT) and the gate every fresh Turso database needs before its\n * first write. */\nexport async function runMigrations(client: Client): Promise<string[]> {\n await client.execute(\n \"CREATE TABLE IF NOT EXISTS _migrations (id TEXT PRIMARY KEY, applied_at TEXT NOT NULL)\",\n );\n const existing = await client.execute(\"SELECT id FROM _migrations\");\n const applied = new Set(existing.rows.map((r) => String(r.id)));\n const ran: string[] = [];\n for (const m of MIGRATIONS) {\n if (applied.has(m.id)) continue;\n try {\n await client.executeMultiple(m.sql);\n } catch (err) {\n // A re-run after a lost marker can re-execute a non-idempotent `ADD COLUMN`;\n // treat \"already applied\" as success so we still record the marker below and\n // never boot-loop. Any other error propagates unchanged.\n if (!isAlreadyAppliedError(err)) throw err;\n }\n await client.execute({\n sql: \"INSERT INTO _migrations (id, applied_at) VALUES (?, ?)\",\n args: [m.id, new Date().toISOString()],\n });\n ran.push(m.id);\n }\n return ran;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/db/migrations.ts","../src/db/migrate.ts"],"sourcesContent":["/** Ordered, append-only list of standard-SQL migration scripts. Each runs once,\n * tracked by `id` in the `_migrations` table (see migrate.ts). Statements use\n * IF NOT EXISTS so even a partial re-apply is safe. Never edit a shipped script —\n * add a new one. Every statement in a script MUST be independently idempotent:\n * `migrate.ts` runs each script via `executeMultiple`, which is NOT transactional,\n * so a mid-script failure can leave earlier statements applied and the id unrecorded\n * — a re-run then re-executes the whole script. Standard SQLite SQL only (no\n * Turso-specific syntax) so the host stays a connection-string swap. */\nexport type Migration = { id: string; sql: string };\n\nexport const MIGRATIONS: Migration[] = [\n {\n id: \"0001_init\",\n sql: `\n CREATE TABLE IF NOT EXISTS submissions (\n id TEXT PRIMARY KEY,\n submission_id INTEGER,\n site_id TEXT NOT NULL,\n form_type TEXT NOT NULL,\n name TEXT NOT NULL,\n email TEXT NOT NULL,\n phone TEXT,\n message TEXT,\n extra_fields TEXT,\n source_url TEXT,\n utm TEXT,\n submitted_at TEXT,\n status TEXT NOT NULL DEFAULT 'new',\n notify_status TEXT NOT NULL DEFAULT 'skipped',\n resend_message_id TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_submissions_site_submitted\n ON submissions (site_id, submitted_at DESC);\n CREATE INDEX IF NOT EXISTS idx_submissions_status\n ON submissions (status);\n CREATE TABLE IF NOT EXISTS spam_screenouts (\n site_id TEXT NOT NULL,\n date TEXT NOT NULL,\n honeypot INTEGER NOT NULL DEFAULT 0,\n too_fast INTEGER NOT NULL DEFAULT 0,\n marked_spam INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (site_id, date)\n );\n `,\n },\n {\n id: \"0002_fleet_events\",\n sql: `\n CREATE TABLE IF NOT EXISTS fleet_events (\n id TEXT PRIMARY KEY,\n ts TEXT NOT NULL,\n type TEXT NOT NULL,\n site_id TEXT,\n site_name TEXT,\n summary TEXT NOT NULL,\n data TEXT,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_fleet_events_ts ON fleet_events (ts);\n `,\n },\n {\n id: \"0003_add_spam_score\",\n // SQLite `ADD COLUMN` has no `IF NOT EXISTS`, so it is NOT self-idempotent: if the\n // `_migrations` marker is lost after the column is added (crash between statement\n // and marker write), a re-run would throw `duplicate column name`. `migrate.ts`\n // recognizes that error as already-applied and records the marker, keeping the\n // runner idempotent (see `isAlreadyAppliedError`).\n sql: `ALTER TABLE submissions ADD COLUMN spam_score REAL;`,\n },\n {\n id: \"0004_add_spam_reason\",\n sql: `ALTER TABLE submissions ADD COLUMN spam_reason TEXT;`,\n },\n {\n // Outcome of the newsletter fan-out (site webhook + Mailchimp). Before this the\n // results were console.error-only, so a rotated Mailchimp key or an outage would\n // silently stop signups reaching the audience with no trace anywhere the operator\n // looks — the row still read `notify=sent`, healthy. Same non-idempotent\n // ADD COLUMN caveat as 0003.\n id: \"0005_add_fanout_status\",\n sql: `ALTER TABLE submissions ADD COLUMN fanout_status TEXT;`,\n },\n {\n // Phase 0 of the Airtable → Turso migration (#539): a lead whose SITE LOOKUP\n // fails must still land somewhere durable. On 2026-08-17 the Airtable quota\n // outage made `getWebsiteBySlug` throw before `createSubmission` ran, so every\n // lead in the window 502'd away unrecorded — while THIS store (which holds\n // submissions) was healthy the whole time. Rows here are written only on that\n // path and replayed through the normal pipeline once the lookup recovers\n // (`db replay-deadletters`), producing an ordinary submissions row with real\n // spam classification and notify. Deliberately a separate table rather than a\n // nullable `submissions.site_id`: site_id is NOT NULL, every reader assumes a\n // real rec id, and a sentinel would collapse dashboard grouping (the \"\" key\n // hazard audit.ts already documents).\n //\n // `turnstile` stores the verification COMPUTED AT RECEIPT ({outcome, hostname}\n // JSON) — tokens expire in 300s, so replay could never re-verify; it replays\n // with the answer we already had.\n id: \"0006_submission_deadletter\",\n sql: `\n CREATE TABLE IF NOT EXISTS submission_deadletter (\n id TEXT PRIMARY KEY,\n site_slug TEXT NOT NULL,\n payload TEXT NOT NULL,\n turnstile TEXT NOT NULL,\n error TEXT NOT NULL,\n received_at TEXT NOT NULL,\n replayed_at TEXT,\n replay_outcome TEXT,\n replay_submission_id TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_deadletter_unreplayed\n ON submission_deadletter (replayed_at);\n `,\n },\n {\n // Phase 1.2 of the Airtable → Turso migration (#539): the fleet-state tables.\n // Split by WRITER, not topic (design D2, confirmed by the 2026-08-23 writer\n // map — every code-written column has exactly one writer):\n // sites — operator (dashboard editor / console / launch flow)\n // site_health — the nightly audit write-back, one batched upsert\n // site_schedule — updateNextDueDates (report cron, derived)\n // reports — report drafting + the operator approve flow\n // PKs are the Airtable `rec…` ids (design D1): 278+ submissions rows already\n // reference them, and the parity harness diffs the two stores row-for-row.\n //\n // Naming: snake_case throughout; Airtable's misspellings (\"maintenence\") and\n // display quirks die here. `sites.legacy` is a JSON object holding the 33\n // populated-but-code-unreferenced columns (launch-era checklist, hosting\n // reference cells) keyed by their original Airtable column name — data kept,\n // schema not fossilized. The plaintext DNS/cms credential cells deliberately\n // do NOT migrate at all (operator ruling 2026-08-23); they live on only in\n // the frozen base. `site_health.analytics_soft_fail_at` is the column code\n // always wanted (\"Analytics soft-fail at\") but no operator ever created in\n // Airtable — it ships real here.\n //\n // reports.checklist is JSON keyed by the STABLE checklist key (\"deploy\",\n // \"cms\", …, from src/reports/checklist.ts), not the Airtable column name —\n // the importer translates, so \"Test: Verified After Updates\" stops leaking\n // its legacy name into a second store.\n id: \"0007_fleet_state\",\n sql: `\n CREATE TABLE IF NOT EXISTS sites (\n id TEXT PRIMARY KEY,\n slug TEXT NOT NULL UNIQUE,\n name TEXT NOT NULL,\n url TEXT,\n status TEXT,\n point_of_contact TEXT,\n maintenance_freq TEXT,\n testing_freq TEXT,\n maintenance_day TEXT,\n testing_day TEXT,\n ga4_property_id TEXT,\n search_query TEXT,\n search_console_property TEXT,\n git_repo TEXT,\n netlify_id TEXT,\n report_recipients_to TEXT,\n report_recipients_cc TEXT,\n copy_intro TEXT,\n copy_contact TEXT,\n copy_footer TEXT,\n newsletter_webhook TEXT,\n mailchimp_api_key TEXT,\n mailchimp_audience_id TEXT,\n notify_routing TEXT,\n require_turnstile INTEGER NOT NULL DEFAULT 0,\n accepted_watch_conditions TEXT,\n prismic_ack_until TEXT,\n launched_at TEXT,\n header_image BLOB,\n header_image_filename TEXT,\n header_image_type TEXT,\n header_image_generated_at TEXT,\n legacy TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_sites_status ON sites (status);\n\n CREATE TABLE IF NOT EXISTS site_health (\n site_id TEXT PRIMARY KEY,\n p_score INTEGER,\n r_score INTEGER,\n bp_score INTEGER,\n seo_score INTEGER,\n lighthouse_at TEXT,\n a11y_violations INTEGER,\n deps_drifted INTEGER,\n deps_major_behind INTEGER,\n deps_outdated INTEGER,\n deps_major_outdated INTEGER,\n vulns_critical INTEGER,\n vulns_high INTEGER,\n vulns_moderate INTEGER,\n vulns_low INTEGER,\n security_audit_at TEXT,\n security_advisories TEXT,\n auto_fix_attempts INTEGER,\n analytics_soft_fail_at TEXT,\n cert_days_remaining INTEGER,\n domain_checked_at TEXT,\n deploy_status TEXT,\n last_deploy_at TEXT,\n deploy_log_url TEXT,\n deploy_checked_at TEXT,\n function_health TEXT,\n cms_reachable TEXT,\n turnstile_widget TEXT,\n function_health_checked_at TEXT,\n crossbrowser_ok INTEGER,\n mobile_ok INTEGER,\n links_ok INTEGER,\n broken_links INTEGER,\n browser_checked_at TEXT,\n uptime_reachable TEXT,\n titles_meta_ok TEXT,\n smoke_ok TEXT,\n last_smoke_at TEXT,\n form_e2e_ok TEXT,\n form_e2e_checked_at TEXT,\n renovate_failing_cis INTEGER,\n default_branch_ci TEXT,\n last_commit_at TEXT,\n github_signals_at TEXT,\n prismic_models TEXT,\n prismic_models_checked_at TEXT,\n prismic_models_drift TEXT\n );\n\n CREATE TABLE IF NOT EXISTS site_schedule (\n site_id TEXT PRIMARY KEY,\n next_maintenance_at TEXT,\n next_testing_at TEXT,\n computed_at TEXT\n );\n\n CREATE TABLE IF NOT EXISTS reports (\n id TEXT PRIMARY KEY,\n site_id TEXT,\n report_id TEXT,\n report_type TEXT,\n period TEXT,\n period_start TEXT,\n period_end TEXT,\n completed_on TEXT,\n lighthouse_performance INTEGER,\n lighthouse_accessibility INTEGER,\n lighthouse_best_practices INTEGER,\n lighthouse_seo INTEGER,\n ga_users_current INTEGER,\n ga_users_previous INTEGER,\n search_found_page1 INTEGER,\n search_position REAL,\n last_tested_date TEXT,\n commentary TEXT,\n subject_override TEXT,\n draft_ready INTEGER NOT NULL DEFAULT 0,\n approved_to_send INTEGER NOT NULL DEFAULT 0,\n approved_at TEXT,\n approved_by TEXT,\n send_override INTEGER NOT NULL DEFAULT 0,\n override_reason TEXT,\n override_by TEXT,\n override_at TEXT,\n sent_at TEXT,\n delivery_status TEXT,\n resend_message_id TEXT,\n checklist TEXT,\n checklist_auto_evidence TEXT,\n rendered_html TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_reports_site ON reports (site_id, period_start DESC);\n `,\n },\n {\n // Indexes the EXPLAIN-query-plan gate (tests/db/query-plans.test.ts) proved the\n // hot paths need — each serves a named query shape that otherwise full-scans\n // submissions, the one unbounded-growth table:\n // submitted_at — every windowed read (/submissions default page, digest\n // per-site counts, duplicate/repeat-sender scans)\n // resend_message_id — the resend-webhook bounce lookup (markNotifyBouncedByMessageId)\n // submission_id — createSubmission's MAX(submission_id)+1 display number,\n // O(1) off the index instead of a scan per insert\n // spam_reason (partial, covering) — listSpamReasonsFiltered's facet tally\n // reads ONLY spam_reason WHERE spam_reason IS NOT NULL;\n // partial keeps it to the spam subset\n // spam_screenouts(date) — listScreenOutsSince windows on date, which the\n // (site_id, date) PK can't serve\n id: \"0008_query_plan_indexes\",\n sql: `\n CREATE INDEX IF NOT EXISTS idx_submissions_submitted\n ON submissions (submitted_at DESC);\n CREATE INDEX IF NOT EXISTS idx_submissions_spam_reason\n ON submissions (spam_reason) WHERE spam_reason IS NOT NULL;\n CREATE INDEX IF NOT EXISTS idx_submissions_resend_message\n ON submissions (resend_message_id);\n CREATE INDEX IF NOT EXISTS idx_submissions_display_number\n ON submissions (submission_id);\n CREATE INDEX IF NOT EXISTS idx_screenouts_date\n ON spam_screenouts (date);\n `,\n },\n {\n // Prospect audits (spec 2026-08-24): one row per `prospect-audit` CLI run.\n // `token` is the unguessable public handle for GET /r/{token}; `result_json`\n // is the full ProspectAuditResult, re-rendered on every request so report\n // styling updates apply to already-shared links.\n id: \"0009_prospect_audits\",\n sql: `\n CREATE TABLE IF NOT EXISTS prospect_audits (\n id TEXT PRIMARY KEY,\n token TEXT NOT NULL UNIQUE,\n url TEXT NOT NULL,\n business TEXT,\n created_at TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'complete',\n result_json TEXT NOT NULL\n );\n `,\n },\n {\n // Cockpit /audits listing page (spec 2026-08-25, Piece 5):\n // listRecentProspectAudits orders by created_at DESC with no WHERE clause —\n // without this index that's a raw SCAN of prospect_audits followed by a temp\n // b-tree sort, which the EXPLAIN-query-plan gate (tests/db/query-plans.test.ts)\n // fails the build on. DESC matches the query's own ORDER BY so SQLite can walk\n // the index directly instead of sorting.\n id: \"0010_prospect_audits_created_index\",\n sql: `\n CREATE INDEX IF NOT EXISTS idx_prospect_audits_created\n ON prospect_audits (created_at DESC);\n `,\n },\n {\n // #609 (#539 Phase 5): the digest's prior-run snapshot moves off Airtable.\n // Unlike the rest of Phase 5 this is a MIGRATION, not a dual-write — there\n // is no Airtable counterpart left afterwards, so parity does not cover it.\n //\n // A single row holding the whole snapshot as JSON, keyed by a constant id.\n // Both readers (runDigest's diff and the fleet homepage's NEW badges) need\n // the ENTIRE map, so a keyed table would buy nothing on reads and its\n // \"give me everything\" query would be a raw scan needing a justified entry\n // in the EXPLAIN gate's allowlist. One row by primary key needs neither.\n id: \"0011_digest_state\",\n sql: `\n CREATE TABLE IF NOT EXISTS digest_state (\n id TEXT PRIMARY KEY,\n snapshot TEXT NOT NULL,\n updated_at TEXT\n );\n `,\n },\n {\n // form_type was the one SubmissionFilter key with an equality predicate and no\n // index — countSubmissionsFiltered({formType}) raw-scanned `submissions`, twice\n // per load of the submissions page. It shipped green because the EXPLAIN gate\n // named its scenarios by hand and never wrote one for this shape; the gate is\n // now driven by a filter matrix, which is what surfaced it.\n //\n // `submissions` is the one unbounded-growth table in the schema — append-only,\n // one row per fleet lead forever — and Turso meters ROW SCANS, so this is the\n // cost that grows without a ceiling.\n id: \"0012_submissions_form_type_index\",\n sql: `\n CREATE INDEX IF NOT EXISTS idx_submissions_form_type\n ON submissions (form_type);\n `,\n },\n {\n // `url` is stored exactly as audited, which makes it a record of the run and\n // a bad thing to group by: the table already holds beachfrontdentistry.com\n // and beachfrontdentistry.com/ as two histories of one practice, and both\n // spellings of ludlowkingsley.com. That costs nothing today because nothing\n // compares two audits of one site — and becomes a returning client with no\n // history the day the report offers a before and an after.\n //\n // Deliberately no SQL backfill. Doing it with lower()/replace()/rtrim()\n // would mangle any URL containing \"www.\" or \"http://\" away from its start,\n // and a key that is quietly wrong is worse than one that is null. Existing\n // rows are backfilled once with the real siteKey().\n // Same ADD COLUMN caveat as 0003: no IF NOT EXISTS in SQLite, so the runner\n // treats \"duplicate column name\" as already-applied. Kept as its own\n // migration, separate from its index below, so that recovery path stays a\n // single statement.\n id: \"0013_prospect_audits_site_key\",\n sql: `ALTER TABLE prospect_audits ADD COLUMN site_key TEXT;`,\n },\n {\n id: \"0014_prospect_audits_site_key_index\",\n sql: `\n CREATE INDEX IF NOT EXISTS idx_prospect_audits_site_key\n ON prospect_audits (site_key, created_at DESC);\n `,\n },\n {\n // Operator edits on a prospect report. One JSON blob rather than a side\n // table: the map is always read and written whole, and no query wants a\n // single override in isolation. Same shape of decision as `result_json`.\n //\n // THREE MIGRATIONS, NOT ONE. Verified against migrate.ts: it runs\n // `executeMultiple(m.sql)` inside a try, swallows any \"duplicate column\n // name\" error, then records the marker unconditionally. That recovery is\n // sound for ONE statement. With three, a crash partway through leaves the\n // marker unwritten; the re-run's first statement then throws duplicate,\n // executeMultiple aborts before the later statements, the catch swallows\n // it, and the marker IS written — leaving the remaining columns\n // permanently missing on a migration recorded as applied. Same rule as\n // 0003 and 0013.\n id: \"0015_prospect_audits_overrides\",\n sql: `ALTER TABLE prospect_audits ADD COLUMN overrides_json TEXT;`,\n },\n {\n id: \"0016_prospect_audits_edited_at\",\n sql: `ALTER TABLE prospect_audits ADD COLUMN edited_at TEXT;`,\n },\n {\n id: \"0017_prospect_audits_opened_at\",\n sql: `ALTER TABLE prospect_audits ADD COLUMN opened_at TEXT;`,\n },\n];\n","import type { Client } from \"@libsql/client\";\nimport { MIGRATIONS } from \"./migrations.js\";\n\n/** True when an error means \"this DDL was already applied\" and re-applying is a\n * no-op we can safely ignore. `CREATE ... IF NOT EXISTS` never throws, but SQLite\n * `ALTER TABLE ... ADD COLUMN` has no `IF NOT EXISTS`, so a re-run after a lost\n * `_migrations` marker throws `duplicate column name: …`. Treating that as\n * already-applied keeps `runMigrations` idempotent for `ADD COLUMN` migrations —\n * without it a lost marker would boot-loop `openDb()` on every call. */\nfunction isAlreadyAppliedError(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n return /duplicate column name/i.test(message);\n}\n\n/** Apply every not-yet-applied migration in order against a raw libSQL client,\n * tracking applied ids in `_migrations`. Idempotent: ids already present are\n * skipped, the DDL uses IF NOT EXISTS where it can, and an already-applied\n * `ADD COLUMN` (recognized by its error) is swallowed so the marker is still\n * recorded. Returns the ids applied this run. Runs on every openDb() — cheap\n * (one indexed SELECT) and the gate every fresh Turso database needs before its\n * first write. */\nexport async function runMigrations(client: Client): Promise<string[]> {\n await client.execute(\n \"CREATE TABLE IF NOT EXISTS _migrations (id TEXT PRIMARY KEY, applied_at TEXT NOT NULL)\",\n );\n const existing = await client.execute(\"SELECT id FROM _migrations\");\n const applied = new Set(existing.rows.map((r) => String(r.id)));\n const ran: string[] = [];\n for (const m of MIGRATIONS) {\n if (applied.has(m.id)) continue;\n try {\n await client.executeMultiple(m.sql);\n } catch (err) {\n // A re-run after a lost marker can re-execute a non-idempotent `ADD COLUMN`;\n // treat \"already applied\" as success so we still record the marker below and\n // never boot-loop. Any other error propagates unchanged.\n if (!isAlreadyAppliedError(err)) throw err;\n }\n await client.execute({\n sql: \"INSERT INTO _migrations (id, applied_at) VALUES (?, ?)\",\n args: [m.id, new Date().toISOString()],\n });\n ran.push(m.id);\n }\n return ran;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
withRecipe
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZQV7UQNE.js";
|
|
4
4
|
import {
|
|
5
5
|
defaultSpawn
|
|
6
6
|
} from "./chunk-LBYOLBW7.js";
|
|
@@ -76,4 +76,4 @@ async function bumpDeps(site, opts = {}) {
|
|
|
76
76
|
export {
|
|
77
77
|
bumpDeps
|
|
78
78
|
};
|
|
79
|
-
//# sourceMappingURL=chunk-
|
|
79
|
+
//# sourceMappingURL=chunk-CJ2RMTT7.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
withRecipe
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ZQV7UQNE.js";
|
|
4
4
|
import {
|
|
5
5
|
readPackageJson,
|
|
6
6
|
writePackageJson
|
|
@@ -96,4 +96,4 @@ async function convertToPnpm(site, opts = {}) {
|
|
|
96
96
|
export {
|
|
97
97
|
convertToPnpm
|
|
98
98
|
};
|
|
99
|
-
//# sourceMappingURL=chunk-
|
|
99
|
+
//# sourceMappingURL=chunk-CWXTNPNZ.js.map
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-R53VL3RI.js";
|
|
5
5
|
import {
|
|
6
6
|
withRecipe
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ZQV7UQNE.js";
|
|
8
8
|
import {
|
|
9
9
|
defaultSpawn
|
|
10
10
|
} from "./chunk-LBYOLBW7.js";
|
|
@@ -329,4 +329,4 @@ async function smokeSuite(site, deps = { spawn: defaultSpawn }) {
|
|
|
329
329
|
export {
|
|
330
330
|
smokeSuite
|
|
331
331
|
};
|
|
332
|
-
//# sourceMappingURL=chunk-
|
|
332
|
+
//# sourceMappingURL=chunk-DFKBTBO3.js.map
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
renovateActionGaps,
|
|
10
10
|
templatesByName,
|
|
11
11
|
withRenovatePinsFrom
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-SLFG7Y76.js";
|
|
13
13
|
import {
|
|
14
14
|
readGitHubConfig
|
|
15
15
|
} from "./chunk-GASBX52O.js";
|
|
@@ -25,12 +25,10 @@ import {
|
|
|
25
25
|
commit,
|
|
26
26
|
createBranch,
|
|
27
27
|
currentBranch,
|
|
28
|
-
getRemoteUrl,
|
|
29
|
-
isOwnerRepo,
|
|
30
28
|
isWorkingTreeClean,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} from "./chunk-
|
|
29
|
+
push,
|
|
30
|
+
resolveOwnerRepo
|
|
31
|
+
} from "./chunk-AVEQUR44.js";
|
|
34
32
|
|
|
35
33
|
// src/recipes/self-updating/index.ts
|
|
36
34
|
import { mkdir, writeFile } from "fs/promises";
|
|
@@ -44,34 +42,11 @@ function sameConfigContents(current, canonical) {
|
|
|
44
42
|
const norm = (s) => s.replace(/\r\n/g, "\n").replace(/\s+$/, "");
|
|
45
43
|
return norm(current) === norm(canonical);
|
|
46
44
|
}
|
|
47
|
-
async function resolveRepo(site) {
|
|
48
|
-
if (site.gitRepo) {
|
|
49
|
-
if (!isOwnerRepo(site.gitRepo)) {
|
|
50
|
-
throw new Error(
|
|
51
|
-
`refusing to act on malformed repo identity: expected "owner/repo", got ${JSON.stringify(site.gitRepo)}`
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
return site.gitRepo;
|
|
55
|
-
}
|
|
56
|
-
let fromOrigin;
|
|
57
|
-
try {
|
|
58
|
-
fromOrigin = parseOwnerRepo(await getRemoteUrl(site.path));
|
|
59
|
-
} catch {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
if (fromOrigin === null) return null;
|
|
63
|
-
if (!isOwnerRepo(fromOrigin)) {
|
|
64
|
-
throw new Error(
|
|
65
|
-
`refusing to act on malformed repo identity from origin: ${JSON.stringify(fromOrigin)}`
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
return fromOrigin;
|
|
69
|
-
}
|
|
70
45
|
async function selfUpdating(site, deps = {}) {
|
|
71
46
|
const templates = templatesByName([...SELF_UPDATING_CONFIGS]);
|
|
72
47
|
let repo;
|
|
73
48
|
try {
|
|
74
|
-
repo = await
|
|
49
|
+
repo = await resolveOwnerRepo(site);
|
|
75
50
|
} catch (err) {
|
|
76
51
|
return resultOf(site, "failed", err instanceof Error ? err.message : String(err));
|
|
77
52
|
}
|
|
@@ -79,7 +54,7 @@ async function selfUpdating(site, deps = {}) {
|
|
|
79
54
|
return resultOf(
|
|
80
55
|
site,
|
|
81
56
|
"failed",
|
|
82
|
-
"
|
|
57
|
+
"could not determine a GitHub repo for this site \u2014 set Airtable 'Git repo', or give the checkout an origin remote whose URL is a GitHub owner/repo"
|
|
83
58
|
);
|
|
84
59
|
}
|
|
85
60
|
const cfg = readGitHubConfig();
|
|
@@ -190,4 +165,4 @@ async function selfUpdating(site, deps = {}) {
|
|
|
190
165
|
export {
|
|
191
166
|
selfUpdating
|
|
192
167
|
};
|
|
193
|
-
//# sourceMappingURL=chunk-
|
|
168
|
+
//# sourceMappingURL=chunk-FSGICFXB.js.map
|