@reddoorla/maintenance 0.54.4 → 0.56.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 (149) hide show
  1. package/README.md +3 -1
  2. package/dist/airtable-2BJX4ATJ.js +9 -0
  3. package/dist/airtable-2BJX4ATJ.js.map +1 -0
  4. package/dist/announce-JEVCDDKL.js +171 -0
  5. package/dist/announce-JEVCDDKL.js.map +1 -0
  6. package/dist/bump-deps-O3SOLQYY.js +57 -0
  7. package/dist/bump-deps-O3SOLQYY.js.map +1 -0
  8. package/dist/chunk-2X322ZES.js +99 -0
  9. package/dist/chunk-2X322ZES.js.map +1 -0
  10. package/dist/chunk-4NAOR44V.js +153 -0
  11. package/dist/chunk-4NAOR44V.js.map +1 -0
  12. package/dist/chunk-4PY4I5OR.js +1167 -0
  13. package/dist/chunk-4PY4I5OR.js.map +1 -0
  14. package/dist/chunk-5WL3ET24.js +203 -0
  15. package/dist/chunk-5WL3ET24.js.map +1 -0
  16. package/dist/chunk-6LIWDIXU.js +38 -0
  17. package/dist/chunk-6LIWDIXU.js.map +1 -0
  18. package/dist/chunk-6VAL7XJT.js +52 -0
  19. package/dist/chunk-6VAL7XJT.js.map +1 -0
  20. package/dist/chunk-BWLUNY7L.js +585 -0
  21. package/dist/chunk-BWLUNY7L.js.map +1 -0
  22. package/dist/chunk-CASCWQQX.js +161 -0
  23. package/dist/chunk-CASCWQQX.js.map +1 -0
  24. package/dist/chunk-CLVTVDOU.js +274 -0
  25. package/dist/chunk-CLVTVDOU.js.map +1 -0
  26. package/dist/chunk-D3DQFC32.js +79 -0
  27. package/dist/chunk-D3DQFC32.js.map +1 -0
  28. package/dist/chunk-DIVB6ZF7.js +217 -0
  29. package/dist/chunk-DIVB6ZF7.js.map +1 -0
  30. package/dist/chunk-EYODPUHV.js +37 -0
  31. package/dist/chunk-EYODPUHV.js.map +1 -0
  32. package/dist/chunk-FDIFTIXC.js +70 -0
  33. package/dist/chunk-FDIFTIXC.js.map +1 -0
  34. package/dist/chunk-FUD4I2ZW.js +35 -0
  35. package/dist/chunk-FUD4I2ZW.js.map +1 -0
  36. package/dist/chunk-HG5DTFNF.js +62 -0
  37. package/dist/chunk-HG5DTFNF.js.map +1 -0
  38. package/dist/chunk-HN6I2XWI.js +65 -0
  39. package/dist/chunk-HN6I2XWI.js.map +1 -0
  40. package/dist/chunk-HVOOCK6L.js +84 -0
  41. package/dist/chunk-HVOOCK6L.js.map +1 -0
  42. package/dist/chunk-IMIR7SC2.js +51 -0
  43. package/dist/chunk-IMIR7SC2.js.map +1 -0
  44. package/dist/chunk-J3OPWFZE.js +64 -0
  45. package/dist/chunk-J3OPWFZE.js.map +1 -0
  46. package/dist/chunk-JNXCDUYB.js +211 -0
  47. package/dist/chunk-JNXCDUYB.js.map +1 -0
  48. package/dist/chunk-JQG222CI.js +49 -0
  49. package/dist/chunk-JQG222CI.js.map +1 -0
  50. package/dist/chunk-LKGVSM2O.js +43 -0
  51. package/dist/chunk-LKGVSM2O.js.map +1 -0
  52. package/dist/chunk-LYCKN7ZR.js +41 -0
  53. package/dist/chunk-LYCKN7ZR.js.map +1 -0
  54. package/dist/chunk-O65I7G7G.js +246 -0
  55. package/dist/chunk-O65I7G7G.js.map +1 -0
  56. package/dist/chunk-OCS7SZUZ.js +243 -0
  57. package/dist/chunk-OCS7SZUZ.js.map +1 -0
  58. package/dist/chunk-OGGRFBL4.js +14 -0
  59. package/dist/chunk-OGGRFBL4.js.map +1 -0
  60. package/dist/chunk-OQPMZ6BM.js +368 -0
  61. package/dist/chunk-OQPMZ6BM.js.map +1 -0
  62. package/dist/chunk-OTGB6YQO.js +52 -0
  63. package/dist/chunk-OTGB6YQO.js.map +1 -0
  64. package/dist/chunk-PF6BNYGE.js +162 -0
  65. package/dist/chunk-PF6BNYGE.js.map +1 -0
  66. package/dist/chunk-QTHKOIFJ.js +13 -0
  67. package/dist/chunk-QTHKOIFJ.js.map +1 -0
  68. package/dist/chunk-U5SZ4FOP.js +26 -0
  69. package/dist/chunk-U5SZ4FOP.js.map +1 -0
  70. package/dist/chunk-VXSX2FHO.js +269 -0
  71. package/dist/chunk-VXSX2FHO.js.map +1 -0
  72. package/dist/chunk-W3G7XFBZ.js +444 -0
  73. package/dist/chunk-W3G7XFBZ.js.map +1 -0
  74. package/dist/chunk-WEKL2HYJ.js +31 -0
  75. package/dist/chunk-WEKL2HYJ.js.map +1 -0
  76. package/dist/chunk-WVRPOXKH.js +74 -0
  77. package/dist/chunk-WVRPOXKH.js.map +1 -0
  78. package/dist/chunk-XKOZUVK2.js +121 -0
  79. package/dist/chunk-XKOZUVK2.js.map +1 -0
  80. package/dist/chunk-XO2RUYVM.js +138 -0
  81. package/dist/chunk-XO2RUYVM.js.map +1 -0
  82. package/dist/chunk-XTK5VIQB.js +97 -0
  83. package/dist/chunk-XTK5VIQB.js.map +1 -0
  84. package/dist/chunk-XXTZBPUY.js +9 -0
  85. package/dist/chunk-XXTZBPUY.js.map +1 -0
  86. package/dist/chunk-ZYBOT5PM.js +153 -0
  87. package/dist/chunk-ZYBOT5PM.js.map +1 -0
  88. package/dist/cli/bin.js +75 -7689
  89. package/dist/cli/bin.js.map +1 -1
  90. package/dist/cli/commands/audit.js +37 -2330
  91. package/dist/cli/commands/audit.js.map +1 -1
  92. package/dist/client-EPVO3FEH.js +10 -0
  93. package/dist/client-EPVO3FEH.js.map +1 -0
  94. package/dist/client-JA6QLEDW.js +48 -0
  95. package/dist/client-JA6QLEDW.js.map +1 -0
  96. package/dist/configs/lighthouse.js +4 -31
  97. package/dist/configs/lighthouse.js.map +1 -1
  98. package/dist/configs/playwright-a11y.js +6 -33
  99. package/dist/configs/playwright-a11y.js.map +1 -1
  100. package/dist/convert-to-pnpm-UR64X3OX.js +51 -0
  101. package/dist/convert-to-pnpm-UR64X3OX.js.map +1 -0
  102. package/dist/db-AXKMRTVA.js +23 -0
  103. package/dist/db-AXKMRTVA.js.map +1 -0
  104. package/dist/digest-YTO73BEO.js +408 -0
  105. package/dist/digest-YTO73BEO.js.map +1 -0
  106. package/dist/forms/index.js +3 -8
  107. package/dist/forms/index.js.map +1 -1
  108. package/dist/github-signals-YD64R2AF.js +122 -0
  109. package/dist/github-signals-YD64R2AF.js.map +1 -0
  110. package/dist/index.d.ts +6 -1
  111. package/dist/index.js +102 -4741
  112. package/dist/index.js.map +1 -1
  113. package/dist/init-JITQXFVL.js +85 -0
  114. package/dist/init-JITQXFVL.js.map +1 -0
  115. package/dist/launch-HXHEL6ID.js +207 -0
  116. package/dist/launch-HXHEL6ID.js.map +1 -0
  117. package/dist/lighthouse-airtable-4FKYKFH6.js +12 -0
  118. package/dist/lighthouse-airtable-4FKYKFH6.js.map +1 -0
  119. package/dist/migrate-P2LS4WXI.js +7 -0
  120. package/dist/migrate-P2LS4WXI.js.map +1 -0
  121. package/dist/onboard-TCQOP2W4.js +69 -0
  122. package/dist/onboard-TCQOP2W4.js.map +1 -0
  123. package/dist/orchestrate-RMSBHLQK.js +19 -0
  124. package/dist/orchestrate-RMSBHLQK.js.map +1 -0
  125. package/dist/recipes/sync-configs.js +9 -505
  126. package/dist/recipes/sync-configs.js.map +1 -1
  127. package/dist/renovate-dispatch-ZGLXNPEK.js +97 -0
  128. package/dist/renovate-dispatch-ZGLXNPEK.js.map +1 -0
  129. package/dist/report-SD3CNBOQ.js +240 -0
  130. package/dist/report-SD3CNBOQ.js.map +1 -0
  131. package/dist/resend-G2BJMAQV.js +7 -0
  132. package/dist/resend-G2BJMAQV.js.map +1 -0
  133. package/dist/self-updating-CIUMOMYI.js +61 -0
  134. package/dist/self-updating-CIUMOMYI.js.map +1 -0
  135. package/dist/svelte-codemods-BYRJAU7I.js +51 -0
  136. package/dist/svelte-codemods-BYRJAU7I.js.map +1 -0
  137. package/dist/sync-configs-JPRLAW5J.js +109 -0
  138. package/dist/sync-configs-JPRLAW5J.js.map +1 -0
  139. package/dist/upgrade-ERA45EVR.js +64 -0
  140. package/dist/upgrade-ERA45EVR.js.map +1 -0
  141. package/dist/util/git.js +19 -76
  142. package/dist/util/git.js.map +1 -1
  143. package/dist/util/pkg.js +5 -45
  144. package/dist/util/pkg.js.map +1 -1
  145. package/dist/websites-X5DLC2PC.js +43 -0
  146. package/dist/websites-X5DLC2PC.js.map +1 -0
  147. package/dist/write-audits-to-airtable-CE4POBAF.js +13 -0
  148. package/dist/write-audits-to-airtable-CE4POBAF.js.map +1 -0
  149. package/package.json +13 -16
@@ -0,0 +1,62 @@
1
+ // src/db/migrations.ts
2
+ var MIGRATIONS = [
3
+ {
4
+ id: "0001_init",
5
+ sql: `
6
+ CREATE TABLE IF NOT EXISTS submissions (
7
+ id TEXT PRIMARY KEY,
8
+ submission_id INTEGER,
9
+ site_id TEXT NOT NULL,
10
+ form_type TEXT NOT NULL,
11
+ name TEXT NOT NULL,
12
+ email TEXT NOT NULL,
13
+ phone TEXT,
14
+ message TEXT,
15
+ extra_fields TEXT,
16
+ source_url TEXT,
17
+ utm TEXT,
18
+ submitted_at TEXT,
19
+ status TEXT NOT NULL DEFAULT 'new',
20
+ notify_status TEXT NOT NULL DEFAULT 'skipped',
21
+ resend_message_id TEXT
22
+ );
23
+ CREATE INDEX IF NOT EXISTS idx_submissions_site_submitted
24
+ ON submissions (site_id, submitted_at DESC);
25
+ CREATE INDEX IF NOT EXISTS idx_submissions_status
26
+ ON submissions (status);
27
+ CREATE TABLE IF NOT EXISTS spam_screenouts (
28
+ site_id TEXT NOT NULL,
29
+ date TEXT NOT NULL,
30
+ honeypot INTEGER NOT NULL DEFAULT 0,
31
+ too_fast INTEGER NOT NULL DEFAULT 0,
32
+ marked_spam INTEGER NOT NULL DEFAULT 0,
33
+ PRIMARY KEY (site_id, date)
34
+ );
35
+ `
36
+ }
37
+ ];
38
+
39
+ // src/db/migrate.ts
40
+ async function runMigrations(client) {
41
+ await client.execute(
42
+ "CREATE TABLE IF NOT EXISTS _migrations (id TEXT PRIMARY KEY, applied_at TEXT NOT NULL)"
43
+ );
44
+ const existing = await client.execute("SELECT id FROM _migrations");
45
+ const applied = new Set(existing.rows.map((r) => String(r.id)));
46
+ const ran = [];
47
+ for (const m of MIGRATIONS) {
48
+ if (applied.has(m.id)) continue;
49
+ await client.executeMultiple(m.sql);
50
+ await client.execute({
51
+ sql: "INSERT INTO _migrations (id, applied_at) VALUES (?, ?)",
52
+ args: [m.id, (/* @__PURE__ */ new Date()).toISOString()]
53
+ });
54
+ ran.push(m.id);
55
+ }
56
+ return ran;
57
+ }
58
+
59
+ export {
60
+ runMigrations
61
+ };
62
+ //# sourceMappingURL=chunk-HG5DTFNF.js.map
@@ -0,0 +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","import type { Client } from \"@libsql/client\";\nimport { MIGRATIONS } from \"./migrations.js\";\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, and the DDL uses IF NOT EXISTS. Returns the ids applied this run.\n * Runs on every openDb() — cheap (one indexed SELECT) and the gate every fresh\n * Turso database needs before its 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 await client.executeMultiple(m.sql);\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":";AAUO,IAAM,aAA0B;AAAA,EACrC;AAAA,IACE,IAAI;AAAA,IACJ,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA+BP;AACF;;;ACrCA,eAAsB,cAAc,QAAmC;AACrE,QAAM,OAAO;AAAA,IACX;AAAA,EACF;AACA,QAAM,WAAW,MAAM,OAAO,QAAQ,4BAA4B;AAClE,QAAM,UAAU,IAAI,IAAI,SAAS,KAAK,IAAI,CAAC,MAAM,OAAO,EAAE,EAAE,CAAC,CAAC;AAC9D,QAAM,MAAgB,CAAC;AACvB,aAAW,KAAK,YAAY;AAC1B,QAAI,QAAQ,IAAI,EAAE,EAAE,EAAG;AACvB,UAAM,OAAO,gBAAgB,EAAE,GAAG;AAClC,UAAM,OAAO,QAAQ;AAAA,MACnB,KAAK;AAAA,MACL,MAAM,CAAC,EAAE,KAAI,oBAAI,KAAK,GAAE,YAAY,CAAC;AAAA,IACvC,CAAC;AACD,QAAI,KAAK,EAAE,EAAE;AAAA,EACf;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,65 @@
1
+ import {
2
+ defaultCredentialsPath
3
+ } from "./chunk-6VAL7XJT.js";
4
+
5
+ // src/reports/airtable/client.ts
6
+ import Airtable from "airtable";
7
+
8
+ // src/reports/airtable/throttle.ts
9
+ function createMinIntervalThrottle(opts) {
10
+ const { minIntervalMs, now, delay } = opts;
11
+ return function wrap(fn) {
12
+ let chain = Promise.resolve();
13
+ let last = Number.NEGATIVE_INFINITY;
14
+ return (...args) => {
15
+ chain = chain.then(async () => {
16
+ const wait = minIntervalMs - (now() - last);
17
+ if (wait > 0) await delay(wait);
18
+ last = now();
19
+ fn(...args);
20
+ }).catch(() => {
21
+ });
22
+ };
23
+ };
24
+ }
25
+ function applyThrottle(base, opts) {
26
+ const real = base._base?.runAction;
27
+ if (typeof real !== "function") return base;
28
+ const wrap = createMinIntervalThrottle(opts);
29
+ const throttled = wrap(real.bind(base._base));
30
+ base._base.runAction = throttled;
31
+ base.runAction = throttled;
32
+ return base;
33
+ }
34
+
35
+ // src/reports/airtable/client.ts
36
+ var MIN_REQUEST_INTERVAL_MS = 220;
37
+ function missing(name) {
38
+ return Object.assign(
39
+ new Error(
40
+ `${name} not set. Export it in your shell or put it in ${defaultCredentialsPath()} as ${name}=...`
41
+ ),
42
+ { exitCode: 2 }
43
+ );
44
+ }
45
+ function readAirtableConfig() {
46
+ const apiKey = process.env.AIRTABLE_PAT;
47
+ const baseId = process.env.AIRTABLE_BASE_ID;
48
+ if (!apiKey) throw missing("AIRTABLE_PAT");
49
+ if (!baseId) throw missing("AIRTABLE_BASE_ID");
50
+ return { apiKey, baseId };
51
+ }
52
+ function openBase(cfg) {
53
+ const base = new Airtable({ apiKey: cfg.apiKey }).base(cfg.baseId);
54
+ return applyThrottle(base, {
55
+ minIntervalMs: MIN_REQUEST_INTERVAL_MS,
56
+ now: () => Date.now(),
57
+ delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
58
+ });
59
+ }
60
+
61
+ export {
62
+ readAirtableConfig,
63
+ openBase
64
+ };
65
+ //# sourceMappingURL=chunk-HN6I2XWI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/airtable/client.ts","../src/reports/airtable/throttle.ts"],"sourcesContent":["import Airtable from \"airtable\";\nimport { defaultCredentialsPath } from \"../../util/credentials.js\";\nimport { applyThrottle } from \"./throttle.js\";\n\n/** Min ms between Airtable HTTP starts. 220 ⇒ ≤ ~4.5 req/s, comfortably under\n * Airtable's ~5 req/s-per-base cap with headroom for clock jitter. */\nconst MIN_REQUEST_INTERVAL_MS = 220;\n\nexport type AirtableConfig = {\n apiKey: string;\n baseId: string;\n};\n\nfunction missing(name: string): Error {\n return Object.assign(\n new Error(\n `${name} not set. Export it in your shell or put it in ${defaultCredentialsPath()} as ${name}=...`,\n ),\n { exitCode: 2 },\n );\n}\n\nexport function readAirtableConfig(): AirtableConfig {\n const apiKey = process.env.AIRTABLE_PAT;\n const baseId = process.env.AIRTABLE_BASE_ID;\n if (!apiKey) throw missing(\"AIRTABLE_PAT\");\n if (!baseId) throw missing(\"AIRTABLE_BASE_ID\");\n return { apiKey, baseId };\n}\n\nexport type AirtableBase = ReturnType<typeof openBase>;\n\nexport function openBase(cfg: AirtableConfig) {\n const base = new Airtable({ apiKey: cfg.apiKey }).base(cfg.baseId);\n // Throttle every Airtable HTTP call at its single funnel so paging bursts\n // (cockpit scans, fleet sweeps) stay under the per-base rate limit instead of\n // tripping 429s. See throttle.ts.\n return applyThrottle(base as Parameters<typeof applyThrottle>[0], {\n minIntervalMs: MIN_REQUEST_INTERVAL_MS,\n now: () => Date.now(),\n delay: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),\n }) as typeof base;\n}\n","/**\n * Airtable enforces ~5 requests/second per base. Even fully *sequential* paging\n * (each `eachPage` fetch awaits the previous) bursts past that when responses are\n * fast — a single cockpit load scanning Reports + Submissions can fire a dozen\n * page-GETs in under a second and trip 429s. The SDK retries 429s with backoff,\n * but under sustained over-limit volume that retry budget runs out and the error\n * surfaces.\n *\n * Rather than rate-limit each call site, we throttle at the ONE chokepoint every\n * Airtable HTTP call funnels through: `base._base.runAction` (used by query\n * paging, create, update, and destroy alike). The throttle serializes call\n * *starts* so consecutive requests begin at least `minIntervalMs` apart, keeping\n * the whole process under the per-base limit. The SDK's built-in 429 retry stays\n * as a backstop for cross-process contention we can't see from here.\n */\n\nexport type ThrottleClock = {\n /** Current time in ms (e.g. `Date.now`). */\n now: () => number;\n /** Resolves after at least `ms` have elapsed (e.g. setTimeout-backed). */\n delay: (ms: number) => Promise<void>;\n};\n\nexport type ThrottleOptions = ThrottleClock & {\n /** Minimum ms between successive call starts (220 ⇒ ≤ ~4.5 req/s). */\n minIntervalMs: number;\n};\n\n/**\n * Build a wrapper that spaces the *starts* of calls to any callback-style\n * function by `minIntervalMs`. Calls preserve order. The wrapper does NOT await\n * the wrapped function's completion (Airtable's `runAction` is fire-and-forget\n * with a callback), so spacing is measured start-to-start, which is exactly what\n * the per-second request limit counts.\n */\nexport function createMinIntervalThrottle(opts: ThrottleOptions) {\n const { minIntervalMs, now, delay } = opts;\n // A single promise chain serializes the gate; `last` is the start time of the\n // most recent dispatch. Both are captured per-wrapper so distinct wrapped fns\n // throttle independently (we only ever wrap one base, but this keeps it pure).\n return function wrap<A extends unknown[]>(fn: (...args: A) => unknown): (...args: A) => void {\n let chain: Promise<void> = Promise.resolve();\n let last = Number.NEGATIVE_INFINITY;\n return (...args: A): void => {\n // The trailing `.catch` is load-bearing: if any step rejected, the next\n // `chain.then(...)` would never run its onFulfilled handler and the queue\n // would stall — silently hanging EVERY subsequent Airtable call in the\n // process. Swallowing keeps the chain perpetually fulfilled.\n chain = chain\n .then(async () => {\n const wait = minIntervalMs - (now() - last);\n if (wait > 0) await delay(wait);\n last = now();\n fn(...args);\n })\n .catch(() => {});\n };\n };\n}\n\ntype ThrottleableBase = {\n _base?: { runAction?: (...args: unknown[]) => unknown };\n runAction?: (...args: unknown[]) => unknown;\n};\n\n/**\n * Replace `base._base.runAction` (the real funnel every table operation calls)\n * and `base.runAction` (its bound public copy) with throttled versions. Returns\n * the same base for chaining. No-op when `_base.runAction` is absent so an\n * unexpected SDK shape degrades gracefully rather than throwing at startup.\n */\nexport function applyThrottle<T extends ThrottleableBase>(base: T, opts: ThrottleOptions): T {\n const real = base._base?.runAction;\n if (typeof real !== \"function\") return base;\n const wrap = createMinIntervalThrottle(opts);\n const throttled = wrap(real.bind(base._base));\n base._base!.runAction = throttled;\n base.runAction = throttled;\n return base;\n}\n"],"mappings":";;;;;AAAA,OAAO,cAAc;;;ACmCd,SAAS,0BAA0B,MAAuB;AAC/D,QAAM,EAAE,eAAe,KAAK,MAAM,IAAI;AAItC,SAAO,SAAS,KAA0B,IAAmD;AAC3F,QAAI,QAAuB,QAAQ,QAAQ;AAC3C,QAAI,OAAO,OAAO;AAClB,WAAO,IAAI,SAAkB;AAK3B,cAAQ,MACL,KAAK,YAAY;AAChB,cAAM,OAAO,iBAAiB,IAAI,IAAI;AACtC,YAAI,OAAO,EAAG,OAAM,MAAM,IAAI;AAC9B,eAAO,IAAI;AACX,WAAG,GAAG,IAAI;AAAA,MACZ,CAAC,EACA,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IACnB;AAAA,EACF;AACF;AAaO,SAAS,cAA0C,MAAS,MAA0B;AAC3F,QAAM,OAAO,KAAK,OAAO;AACzB,MAAI,OAAO,SAAS,WAAY,QAAO;AACvC,QAAM,OAAO,0BAA0B,IAAI;AAC3C,QAAM,YAAY,KAAK,KAAK,KAAK,KAAK,KAAK,CAAC;AAC5C,OAAK,MAAO,YAAY;AACxB,OAAK,YAAY;AACjB,SAAO;AACT;;;ADzEA,IAAM,0BAA0B;AAOhC,SAAS,QAAQ,MAAqB;AACpC,SAAO,OAAO;AAAA,IACZ,IAAI;AAAA,MACF,GAAG,IAAI,kDAAkD,uBAAuB,CAAC,OAAO,IAAI;AAAA,IAC9F;AAAA,IACA,EAAE,UAAU,EAAE;AAAA,EAChB;AACF;AAEO,SAAS,qBAAqC;AACnD,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,SAAS,QAAQ,IAAI;AAC3B,MAAI,CAAC,OAAQ,OAAM,QAAQ,cAAc;AACzC,MAAI,CAAC,OAAQ,OAAM,QAAQ,kBAAkB;AAC7C,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAIO,SAAS,SAAS,KAAqB;AAC5C,QAAM,OAAO,IAAI,SAAS,EAAE,QAAQ,IAAI,OAAO,CAAC,EAAE,KAAK,IAAI,MAAM;AAIjE,SAAO,cAAc,MAA6C;AAAA,IAChE,eAAe;AAAA,IACf,KAAK,MAAM,KAAK,IAAI;AAAA,IACpB,OAAO,CAAC,OAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAAA,EACjE,CAAC;AACH;","names":[]}
@@ -0,0 +1,84 @@
1
+ // src/audits/util/spawn.ts
2
+ import { spawn } from "child_process";
3
+ import { StringDecoder } from "string_decoder";
4
+ var TRUNCATION_MARKER = "\n\u2026[output truncated]";
5
+ function makeSpawn(internals = {}) {
6
+ const spawnImpl = internals.spawnImpl ?? spawn;
7
+ const killImpl = internals.killImpl ?? ((pid, sig) => process.kill(pid, sig));
8
+ const killGraceMs = internals.killGraceMs ?? 5e3;
9
+ const maxOutputBytes = internals.maxOutputBytes ?? 10 * 1024 * 1024;
10
+ return (cmd, args, opts = {}) => new Promise((resolve, reject) => {
11
+ const streaming = opts.streaming === true;
12
+ const child = spawnImpl(cmd, [...args], {
13
+ cwd: opts.cwd,
14
+ env: opts.env ?? process.env,
15
+ stdio: streaming ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"],
16
+ // Detach ONLY when a timeout can fire: the child then leads its own
17
+ // process group, so the timeout can kill the WHOLE tree (vite, and
18
+ // Chromium under lhci/playwright) via process.kill(-pid), not just the
19
+ // npx/pnpm wrapper. Without it, killing the wrapper orphaned the
20
+ // grandchildren — a zombie vite squatting its port, Chrome left running.
21
+ // We do NOT detach timeout-less streaming calls (pnpm install/up):
22
+ // detaching gains nothing there (no timeout → no group-kill) and would
23
+ // break terminal Ctrl-C, which only reaches the foreground group — i.e.
24
+ // it would re-orphan the very children this guards. We never unref() the
25
+ // child since we still await it.
26
+ detached: opts.timeoutMs !== void 0
27
+ });
28
+ const cap = (acc, chunk) => {
29
+ if (acc.length >= maxOutputBytes) return acc;
30
+ const next = acc + chunk;
31
+ return next.length > maxOutputBytes ? next.slice(0, maxOutputBytes) + TRUNCATION_MARKER : next;
32
+ };
33
+ let stdout = "";
34
+ let stderr = "";
35
+ const outDecoder = new StringDecoder("utf-8");
36
+ const errDecoder = new StringDecoder("utf-8");
37
+ if (!streaming) {
38
+ child.stdout?.on(
39
+ "data",
40
+ (chunk) => stdout = cap(stdout, outDecoder.write(chunk))
41
+ );
42
+ child.stderr?.on(
43
+ "data",
44
+ (chunk) => stderr = cap(stderr, errDecoder.write(chunk))
45
+ );
46
+ }
47
+ const killGroup = (sig) => {
48
+ if (child.pid === void 0) return;
49
+ try {
50
+ killImpl(-child.pid, sig);
51
+ } catch {
52
+ }
53
+ };
54
+ let killTimer;
55
+ const timer = opts.timeoutMs ? setTimeout(() => {
56
+ killGroup("SIGTERM");
57
+ killTimer = setTimeout(() => killGroup("SIGKILL"), killGraceMs);
58
+ killTimer.unref();
59
+ reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));
60
+ }, opts.timeoutMs) : void 0;
61
+ const clearTimers = () => {
62
+ if (timer) clearTimeout(timer);
63
+ if (killTimer) clearTimeout(killTimer);
64
+ };
65
+ child.on("error", (err) => {
66
+ clearTimers();
67
+ reject(err);
68
+ });
69
+ child.on("close", (code) => {
70
+ clearTimers();
71
+ if (!streaming) {
72
+ stdout = cap(stdout, outDecoder.end());
73
+ stderr = cap(stderr, errDecoder.end());
74
+ }
75
+ resolve({ code: code ?? -1, stdout, stderr });
76
+ });
77
+ });
78
+ }
79
+ var defaultSpawn = makeSpawn();
80
+
81
+ export {
82
+ defaultSpawn
83
+ };
84
+ //# sourceMappingURL=chunk-HVOOCK6L.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/audits/util/spawn.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { StringDecoder } from \"node:string_decoder\";\n\nexport type SpawnResult = { code: number; stdout: string; stderr: string };\n\nexport type SpawnOptions = {\n cwd?: string;\n env?: NodeJS.ProcessEnv;\n timeoutMs?: number;\n /** When true, the child inherits stdout/stderr so the user sees live\n * progress (useful for long-running `pnpm up` / `npm install`). The\n * returned `stdout` and `stderr` will be empty strings in that case. */\n streaming?: boolean;\n};\n\nexport type SpawnFn = (\n cmd: string,\n args: readonly string[],\n opts?: SpawnOptions,\n) => Promise<SpawnResult>;\n\ntype KillFn = (pid: number, signal: NodeJS.Signals | number) => void;\n\n/** Construction-time knobs, separated from per-call {@link SpawnOptions} mainly\n * so tests can inject deterministic `spawnImpl`/`killImpl` and a tiny grace. */\nexport type SpawnInternals = {\n spawnImpl?: typeof spawn;\n killImpl?: KillFn;\n /** Delay after SIGTERM before escalating to SIGKILL on a timeout (default 5s). */\n killGraceMs?: number;\n /** Cap on captured stdout/stderr length so a runaway child can't OOM the CLI. */\n maxOutputBytes?: number;\n};\n\nconst TRUNCATION_MARKER = \"\\n…[output truncated]\";\n\nexport function makeSpawn(internals: SpawnInternals = {}): SpawnFn {\n const spawnImpl = internals.spawnImpl ?? spawn;\n const killImpl: KillFn = internals.killImpl ?? ((pid, sig) => process.kill(pid, sig));\n const killGraceMs = internals.killGraceMs ?? 5000;\n const maxOutputBytes = internals.maxOutputBytes ?? 10 * 1024 * 1024;\n\n return (cmd, args, opts = {}) =>\n new Promise((resolve, reject) => {\n const streaming = opts.streaming === true;\n const child = spawnImpl(cmd, [...args], {\n cwd: opts.cwd,\n env: opts.env ?? process.env,\n stdio: streaming ? [\"ignore\", \"inherit\", \"inherit\"] : [\"ignore\", \"pipe\", \"pipe\"],\n // Detach ONLY when a timeout can fire: the child then leads its own\n // process group, so the timeout can kill the WHOLE tree (vite, and\n // Chromium under lhci/playwright) via process.kill(-pid), not just the\n // npx/pnpm wrapper. Without it, killing the wrapper orphaned the\n // grandchildren — a zombie vite squatting its port, Chrome left running.\n // We do NOT detach timeout-less streaming calls (pnpm install/up):\n // detaching gains nothing there (no timeout → no group-kill) and would\n // break terminal Ctrl-C, which only reaches the foreground group — i.e.\n // it would re-orphan the very children this guards. We never unref() the\n // child since we still await it.\n detached: opts.timeoutMs !== undefined,\n });\n\n // Cap appended output so an unbounded stream can't exhaust memory.\n const cap = (acc: string, chunk: string): string => {\n if (acc.length >= maxOutputBytes) return acc;\n const next = acc + chunk;\n return next.length > maxOutputBytes\n ? next.slice(0, maxOutputBytes) + TRUNCATION_MARKER\n : next;\n };\n\n let stdout = \"\";\n let stderr = \"\";\n // Decode each stream through a StringDecoder so a multibyte UTF-8 char\n // split across two `data` chunks isn't corrupted: the decoder holds the\n // partial trailing bytes until the rest arrives, instead of the old\n // `String(chunk)` which decoded each chunk in isolation (and replaced the\n // split char with U+FFFD). Flushed via `.end()` on close.\n const outDecoder = new StringDecoder(\"utf-8\");\n const errDecoder = new StringDecoder(\"utf-8\");\n if (!streaming) {\n child.stdout?.on(\n \"data\",\n (chunk: Buffer) => (stdout = cap(stdout, outDecoder.write(chunk))),\n );\n child.stderr?.on(\n \"data\",\n (chunk: Buffer) => (stderr = cap(stderr, errDecoder.write(chunk))),\n );\n }\n\n /** Signal the child's whole process group; ignore if it's already gone.\n * POSIX-only: a negative pid signals the group (the project targets\n * macOS/Linux; this is only reached when detached, i.e. on a timeout). */\n const killGroup = (sig: NodeJS.Signals): void => {\n if (child.pid === undefined) return;\n try {\n killImpl(-child.pid, sig);\n } catch {\n // ESRCH: the group already exited between the timeout and the kill.\n }\n };\n\n let killTimer: ReturnType<typeof setTimeout> | undefined;\n const timer = opts.timeoutMs\n ? setTimeout(() => {\n killGroup(\"SIGTERM\");\n // Escalate if SIGTERM is ignored (a wedged Chrome can swallow it).\n killTimer = setTimeout(() => killGroup(\"SIGKILL\"), killGraceMs);\n // Best-effort cleanup AFTER we've already rejected — it must never\n // hold the CLI open past its real work.\n killTimer.unref();\n reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));\n }, opts.timeoutMs)\n : undefined;\n\n const clearTimers = (): void => {\n if (timer) clearTimeout(timer);\n if (killTimer) clearTimeout(killTimer);\n };\n\n child.on(\"error\", (err) => {\n clearTimers();\n reject(err);\n });\n child.on(\"close\", (code) => {\n clearTimers();\n if (!streaming) {\n // Flush any bytes the decoder buffered mid-character (e.g. a truncated\n // final UTF-8 sequence). `.end()` returns \"\" when nothing is pending.\n stdout = cap(stdout, outDecoder.end());\n stderr = cap(stderr, errDecoder.end());\n }\n resolve({ code: code ?? -1, stdout, stderr });\n });\n });\n}\n\nexport const defaultSpawn: SpawnFn = makeSpawn();\n"],"mappings":";AAAA,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAiC9B,IAAM,oBAAoB;AAEnB,SAAS,UAAU,YAA4B,CAAC,GAAY;AACjE,QAAM,YAAY,UAAU,aAAa;AACzC,QAAM,WAAmB,UAAU,aAAa,CAAC,KAAK,QAAQ,QAAQ,KAAK,KAAK,GAAG;AACnF,QAAM,cAAc,UAAU,eAAe;AAC7C,QAAM,iBAAiB,UAAU,kBAAkB,KAAK,OAAO;AAE/D,SAAO,CAAC,KAAK,MAAM,OAAO,CAAC,MACzB,IAAI,QAAQ,CAAC,SAAS,WAAW;AAC/B,UAAM,YAAY,KAAK,cAAc;AACrC,UAAM,QAAQ,UAAU,KAAK,CAAC,GAAG,IAAI,GAAG;AAAA,MACtC,KAAK,KAAK;AAAA,MACV,KAAK,KAAK,OAAO,QAAQ;AAAA,MACzB,OAAO,YAAY,CAAC,UAAU,WAAW,SAAS,IAAI,CAAC,UAAU,QAAQ,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAW/E,UAAU,KAAK,cAAc;AAAA,IAC/B,CAAC;AAGD,UAAM,MAAM,CAAC,KAAa,UAA0B;AAClD,UAAI,IAAI,UAAU,eAAgB,QAAO;AACzC,YAAM,OAAO,MAAM;AACnB,aAAO,KAAK,SAAS,iBACjB,KAAK,MAAM,GAAG,cAAc,IAAI,oBAChC;AAAA,IACN;AAEA,QAAI,SAAS;AACb,QAAI,SAAS;AAMb,UAAM,aAAa,IAAI,cAAc,OAAO;AAC5C,UAAM,aAAa,IAAI,cAAc,OAAO;AAC5C,QAAI,CAAC,WAAW;AACd,YAAM,QAAQ;AAAA,QACZ;AAAA,QACA,CAAC,UAAmB,SAAS,IAAI,QAAQ,WAAW,MAAM,KAAK,CAAC;AAAA,MAClE;AACA,YAAM,QAAQ;AAAA,QACZ;AAAA,QACA,CAAC,UAAmB,SAAS,IAAI,QAAQ,WAAW,MAAM,KAAK,CAAC;AAAA,MAClE;AAAA,IACF;AAKA,UAAM,YAAY,CAAC,QAA8B;AAC/C,UAAI,MAAM,QAAQ,OAAW;AAC7B,UAAI;AACF,iBAAS,CAAC,MAAM,KAAK,GAAG;AAAA,MAC1B,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,QAAI;AACJ,UAAM,QAAQ,KAAK,YACf,WAAW,MAAM;AACf,gBAAU,SAAS;AAEnB,kBAAY,WAAW,MAAM,UAAU,SAAS,GAAG,WAAW;AAG9D,gBAAU,MAAM;AAChB,aAAO,IAAI,MAAM,uBAAuB,KAAK,SAAS,OAAO,GAAG,EAAE,CAAC;AAAA,IACrE,GAAG,KAAK,SAAS,IACjB;AAEJ,UAAM,cAAc,MAAY;AAC9B,UAAI,MAAO,cAAa,KAAK;AAC7B,UAAI,UAAW,cAAa,SAAS;AAAA,IACvC;AAEA,UAAM,GAAG,SAAS,CAAC,QAAQ;AACzB,kBAAY;AACZ,aAAO,GAAG;AAAA,IACZ,CAAC;AACD,UAAM,GAAG,SAAS,CAAC,SAAS;AAC1B,kBAAY;AACZ,UAAI,CAAC,WAAW;AAGd,iBAAS,IAAI,QAAQ,WAAW,IAAI,CAAC;AACrC,iBAAS,IAAI,QAAQ,WAAW,IAAI,CAAC;AAAA,MACvC;AACA,cAAQ,EAAE,MAAM,QAAQ,IAAI,QAAQ,OAAO,CAAC;AAAA,IAC9C,CAAC;AAAA,EACH,CAAC;AACL;AAEO,IAAM,eAAwB,UAAU;","names":[]}
@@ -0,0 +1,51 @@
1
+ import {
2
+ siteSlug
3
+ } from "./chunk-CLVTVDOU.js";
4
+
5
+ // src/audits/lighthouse-airtable.ts
6
+ import { readFile } from "fs/promises";
7
+ import { join } from "path";
8
+ var LIGHTHOUSE_CATEGORIES = ["performance", "accessibility", "best-practices", "seo"];
9
+ function hasRealScores(result) {
10
+ if (result.audit !== "lighthouse") return false;
11
+ const details = result.details ?? {};
12
+ const summary = details.summary ?? {};
13
+ return LIGHTHOUSE_CATEGORIES.some(
14
+ (k) => typeof summary[k] === "number" && !Number.isNaN(summary[k])
15
+ );
16
+ }
17
+ function lighthouseScoresFromResult(result) {
18
+ if (result.audit !== "lighthouse") {
19
+ throw new Error(`Expected a 'lighthouse' AuditResult, got '${result.audit}'`);
20
+ }
21
+ const details = result.details ?? {};
22
+ const summary = details.summary ?? {};
23
+ const toPct = (n) => typeof n === "number" && !Number.isNaN(n) ? Math.round(n * 100) : 0;
24
+ return {
25
+ performance: toPct(summary["performance"]),
26
+ accessibility: toPct(summary["accessibility"]),
27
+ bestPractices: toPct(summary["best-practices"]),
28
+ seo: toPct(summary["seo"])
29
+ };
30
+ }
31
+ async function resolveSlugFromCwd(cwd) {
32
+ try {
33
+ const pkgPath = join(cwd, "package.json");
34
+ const raw = await readFile(pkgPath, "utf-8");
35
+ const pkg = JSON.parse(raw);
36
+ if (!pkg.name) throw new Error("package.json has no 'name' field");
37
+ return siteSlug(pkg.name);
38
+ } catch (e) {
39
+ throw new Error(
40
+ `Could not derive site slug from ${cwd}/package.json: ${e.message}. Pass --write-airtable=<slug> explicitly.`,
41
+ { cause: e }
42
+ );
43
+ }
44
+ }
45
+
46
+ export {
47
+ hasRealScores,
48
+ lighthouseScoresFromResult,
49
+ resolveSlugFromCwd
50
+ };
51
+ //# sourceMappingURL=chunk-IMIR7SC2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/audits/lighthouse-airtable.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport type { LighthouseScores } from \"../reports/types.js\";\nimport { siteSlug } from \"../reports/airtable/websites.js\";\n\nconst LIGHTHOUSE_CATEGORIES = [\"performance\", \"accessibility\", \"best-practices\", \"seo\"] as const;\n\n/**\n * True when the result carries real lighthouse scores worth persisting.\n *\n * The `audit --write-airtable` policy used to refuse on any `status: \"fail\"`,\n * but that conflates two very different failure modes:\n * 1. Infrastructure failure (no lhr-*.json written, spawn timeout, etc.)\n * → `details.summary` empty → all-zeros would corrupt the dashboard\n * 2. Assertion failure (scores below threshold, e.g. best-practices < 0.9)\n * → `details.summary` has real numbers → tracking these IS the point\n *\n * The dashboard exists to surface drift over time. Refusing to write the\n * very numbers the dashboard wants to plot — just because one assertion\n * tripped — defeats the purpose. Write whenever real scores exist.\n */\nexport function hasRealScores(result: AuditResult): boolean {\n if (result.audit !== \"lighthouse\") return false;\n const details = (result.details ?? {}) as { summary?: Record<string, number> };\n const summary = details.summary ?? {};\n return LIGHTHOUSE_CATEGORIES.some(\n (k) => typeof summary[k] === \"number\" && !Number.isNaN(summary[k]),\n );\n}\n\n/**\n * Extract the four Lighthouse scores (as integer percentages) from a\n * `lighthouse` AuditResult. LHCI manifest summaries are floats in [0,1];\n * multiply + round.\n */\nexport function lighthouseScoresFromResult(result: AuditResult): LighthouseScores {\n if (result.audit !== \"lighthouse\") {\n throw new Error(`Expected a 'lighthouse' AuditResult, got '${result.audit}'`);\n }\n const details = (result.details ?? {}) as { summary?: Record<string, number> };\n const summary = details.summary ?? {};\n const toPct = (n: number | undefined) =>\n typeof n === \"number\" && !Number.isNaN(n) ? Math.round(n * 100) : 0;\n return {\n performance: toPct(summary[\"performance\"]),\n accessibility: toPct(summary[\"accessibility\"]),\n bestPractices: toPct(summary[\"best-practices\"]),\n seo: toPct(summary[\"seo\"]),\n };\n}\n\n/**\n * Derive a site slug from the cwd's package.json#name. Used by\n * `audit lighthouse --write-airtable` when the operator doesn't pass an\n * explicit slug — the cwd is the site checkout, so package.json#name is\n * usually the canonical site name.\n */\nexport async function resolveSlugFromCwd(cwd: string): Promise<string> {\n try {\n const pkgPath = join(cwd, \"package.json\");\n const raw = await readFile(pkgPath, \"utf-8\");\n const pkg = JSON.parse(raw) as { name?: string };\n if (!pkg.name) throw new Error(\"package.json has no 'name' field\");\n return siteSlug(pkg.name);\n } catch (e) {\n throw new Error(\n `Could not derive site slug from ${cwd}/package.json: ${(e as Error).message}. ` +\n `Pass --write-airtable=<slug> explicitly.`,\n { cause: e },\n );\n }\n}\n"],"mappings":";;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,YAAY;AAKrB,IAAM,wBAAwB,CAAC,eAAe,iBAAiB,kBAAkB,KAAK;AAgB/E,SAAS,cAAc,QAA8B;AAC1D,MAAI,OAAO,UAAU,aAAc,QAAO;AAC1C,QAAM,UAAW,OAAO,WAAW,CAAC;AACpC,QAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,SAAO,sBAAsB;AAAA,IAC3B,CAAC,MAAM,OAAO,QAAQ,CAAC,MAAM,YAAY,CAAC,OAAO,MAAM,QAAQ,CAAC,CAAC;AAAA,EACnE;AACF;AAOO,SAAS,2BAA2B,QAAuC;AAChF,MAAI,OAAO,UAAU,cAAc;AACjC,UAAM,IAAI,MAAM,6CAA6C,OAAO,KAAK,GAAG;AAAA,EAC9E;AACA,QAAM,UAAW,OAAO,WAAW,CAAC;AACpC,QAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,QAAM,QAAQ,CAAC,MACb,OAAO,MAAM,YAAY,CAAC,OAAO,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,GAAG,IAAI;AACpE,SAAO;AAAA,IACL,aAAa,MAAM,QAAQ,aAAa,CAAC;AAAA,IACzC,eAAe,MAAM,QAAQ,eAAe,CAAC;AAAA,IAC7C,eAAe,MAAM,QAAQ,gBAAgB,CAAC;AAAA,IAC9C,KAAK,MAAM,QAAQ,KAAK,CAAC;AAAA,EAC3B;AACF;AAQA,eAAsB,mBAAmB,KAA8B;AACrE,MAAI;AACF,UAAM,UAAU,KAAK,KAAK,cAAc;AACxC,UAAM,MAAM,MAAM,SAAS,SAAS,OAAO;AAC3C,UAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,QAAI,CAAC,IAAI,KAAM,OAAM,IAAI,MAAM,kCAAkC;AACjE,WAAO,SAAS,IAAI,IAAI;AAAA,EAC1B,SAAS,GAAG;AACV,UAAM,IAAI;AAAA,MACR,mCAAmC,GAAG,kBAAmB,EAAY,OAAO;AAAA,MAE5E,EAAE,OAAO,EAAE;AAAA,IACb;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,64 @@
1
+ import {
2
+ fromJsonFile,
3
+ localPath
4
+ } from "./chunk-FDIFTIXC.js";
5
+ import {
6
+ isHttpUrl
7
+ } from "./chunk-U5SZ4FOP.js";
8
+
9
+ // src/cli/fleet/resolve-sites.ts
10
+ import { pathToFileURL } from "url";
11
+ import { resolve, extname } from "path";
12
+ function sanitizeDynamicSites(sites) {
13
+ return sites.map((s) => {
14
+ if (s.deployedUrl !== void 0 && !isHttpUrl(s.deployedUrl)) {
15
+ console.warn(
16
+ `[inventory] dynamic inventory: ignoring deployedUrl that is not http(s) for ${s.name ?? s.path}: ${JSON.stringify(s.deployedUrl)}`
17
+ );
18
+ const copy = { ...s };
19
+ delete copy.deployedUrl;
20
+ return copy;
21
+ }
22
+ return s;
23
+ });
24
+ }
25
+ async function resolveSites(input) {
26
+ if (input.site && input.fleet) {
27
+ throw Object.assign(new Error("cannot combine a positional [site] with --fleet"), {
28
+ exitCode: 2
29
+ });
30
+ }
31
+ if (input.fleet === "airtable") {
32
+ const { openBase, readAirtableConfig } = await import("./client-EPVO3FEH.js");
33
+ const { fromAirtableBase } = await import("./airtable-2BJX4ATJ.js");
34
+ const base = openBase(readAirtableConfig());
35
+ const provider = fromAirtableBase(base, input.workdir ? { workdir: input.workdir } : {});
36
+ return provider();
37
+ }
38
+ if (input.fleet) {
39
+ const fleetPath = resolve(input.cwd, input.fleet);
40
+ const ext = extname(fleetPath).toLowerCase();
41
+ if (ext === ".json") {
42
+ return fromJsonFile(fleetPath)();
43
+ }
44
+ if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
45
+ const mod = await import(pathToFileURL(fleetPath).href);
46
+ if (!mod.default || typeof mod.default !== "function") {
47
+ throw Object.assign(new Error(`--fleet ${input.fleet}: default export is not a function`), {
48
+ exitCode: 2
49
+ });
50
+ }
51
+ return sanitizeDynamicSites(await mod.default());
52
+ }
53
+ throw Object.assign(
54
+ new Error(`--fleet ${input.fleet}: unsupported extension ${ext || "(none)"}`),
55
+ { exitCode: 2 }
56
+ );
57
+ }
58
+ return localPath(resolve(input.cwd, input.site ?? input.cwd))();
59
+ }
60
+
61
+ export {
62
+ resolveSites
63
+ };
64
+ //# sourceMappingURL=chunk-J3OPWFZE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/fleet/resolve-sites.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { resolve, extname } from \"node:path\";\nimport type { InventoryProvider, Site } from \"../../types.js\";\nimport { localPath } from \"../../inventory/local.js\";\nimport { fromJsonFile } from \"../../inventory/json.js\";\nimport { isHttpUrl } from \"../../util/url.js\";\n\n/** A dynamic .js/.mjs/.cjs inventory returns arbitrary Site objects; apply the\n * same `deployedUrl` scheme-allowlist the JSON + Airtable providers enforce, so\n * a module returning `file:///…`/`gopher://…` can't reach Chrome/lhci. Drop the\n * offending value (and warn) rather than throw — matching the JSON provider. */\nexport function sanitizeDynamicSites(sites: Site[]): Site[] {\n return sites.map((s) => {\n if (s.deployedUrl !== undefined && !isHttpUrl(s.deployedUrl)) {\n console.warn(\n `[inventory] dynamic inventory: ignoring deployedUrl that is not http(s) for ` +\n `${s.name ?? s.path}: ${JSON.stringify(s.deployedUrl)}`,\n );\n const copy = { ...s };\n delete copy.deployedUrl;\n return copy;\n }\n return s;\n });\n}\n\nexport type ResolveSitesInput = {\n site?: string;\n fleet?: string;\n /** Optional workdir for the `--fleet airtable` keyword path (computes site.path as {workdir}/{slug}). */\n workdir?: string;\n cwd: string;\n};\n\nexport async function resolveSites(input: ResolveSitesInput): Promise<Site[]> {\n if (input.site && input.fleet) {\n throw Object.assign(new Error(\"cannot combine a positional [site] with --fleet\"), {\n exitCode: 2,\n });\n }\n\n if (input.fleet === \"airtable\") {\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const { fromAirtableBase } = await import(\"../../inventory/airtable.js\");\n const base = openBase(readAirtableConfig());\n const provider = fromAirtableBase(base, input.workdir ? { workdir: input.workdir } : {});\n return provider();\n }\n\n if (input.fleet) {\n const fleetPath = resolve(input.cwd, input.fleet);\n const ext = extname(fleetPath).toLowerCase();\n if (ext === \".json\") {\n // fromJsonFile already scheme-allowlists deployedUrl internally.\n return fromJsonFile(fleetPath)();\n }\n if (ext === \".js\" || ext === \".mjs\" || ext === \".cjs\") {\n const mod = (await import(pathToFileURL(fleetPath).href)) as {\n default?: InventoryProvider;\n };\n if (!mod.default || typeof mod.default !== \"function\") {\n throw Object.assign(new Error(`--fleet ${input.fleet}: default export is not a function`), {\n exitCode: 2,\n });\n }\n // A dynamic module's deployedUrl is unvalidated — sanitize before it can\n // reach a deployed audit (the JSON/Airtable providers already do this).\n return sanitizeDynamicSites(await mod.default());\n }\n throw Object.assign(\n new Error(`--fleet ${input.fleet}: unsupported extension ${ext || \"(none)\"}`),\n { exitCode: 2 },\n );\n }\n\n return localPath(resolve(input.cwd, input.site ?? input.cwd))();\n}\n"],"mappings":";;;;;;;;;AAAA,SAAS,qBAAqB;AAC9B,SAAS,SAAS,eAAe;AAU1B,SAAS,qBAAqB,OAAuB;AAC1D,SAAO,MAAM,IAAI,CAAC,MAAM;AACtB,QAAI,EAAE,gBAAgB,UAAa,CAAC,UAAU,EAAE,WAAW,GAAG;AAC5D,cAAQ;AAAA,QACN,+EACK,EAAE,QAAQ,EAAE,IAAI,KAAK,KAAK,UAAU,EAAE,WAAW,CAAC;AAAA,MACzD;AACA,YAAM,OAAO,EAAE,GAAG,EAAE;AACpB,aAAO,KAAK;AACZ,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,CAAC;AACH;AAUA,eAAsB,aAAa,OAA2C;AAC5E,MAAI,MAAM,QAAQ,MAAM,OAAO;AAC7B,UAAM,OAAO,OAAO,IAAI,MAAM,iDAAiD,GAAG;AAAA,MAChF,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AAEA,MAAI,MAAM,UAAU,YAAY;AAC9B,UAAM,EAAE,UAAU,mBAAmB,IAAI,MAAM,OAAO,sBAAkC;AACxF,UAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,wBAA6B;AACvE,UAAM,OAAO,SAAS,mBAAmB,CAAC;AAC1C,UAAM,WAAW,iBAAiB,MAAM,MAAM,UAAU,EAAE,SAAS,MAAM,QAAQ,IAAI,CAAC,CAAC;AACvF,WAAO,SAAS;AAAA,EAClB;AAEA,MAAI,MAAM,OAAO;AACf,UAAM,YAAY,QAAQ,MAAM,KAAK,MAAM,KAAK;AAChD,UAAM,MAAM,QAAQ,SAAS,EAAE,YAAY;AAC3C,QAAI,QAAQ,SAAS;AAEnB,aAAO,aAAa,SAAS,EAAE;AAAA,IACjC;AACA,QAAI,QAAQ,SAAS,QAAQ,UAAU,QAAQ,QAAQ;AACrD,YAAM,MAAO,MAAM,OAAO,cAAc,SAAS,EAAE;AAGnD,UAAI,CAAC,IAAI,WAAW,OAAO,IAAI,YAAY,YAAY;AACrD,cAAM,OAAO,OAAO,IAAI,MAAM,WAAW,MAAM,KAAK,oCAAoC,GAAG;AAAA,UACzF,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAGA,aAAO,qBAAqB,MAAM,IAAI,QAAQ,CAAC;AAAA,IACjD;AACA,UAAM,OAAO;AAAA,MACX,IAAI,MAAM,WAAW,MAAM,KAAK,2BAA2B,OAAO,QAAQ,EAAE;AAAA,MAC5E,EAAE,UAAU,EAAE;AAAA,IAChB;AAAA,EACF;AAEA,SAAO,UAAU,QAAQ,MAAM,KAAK,MAAM,QAAQ,MAAM,GAAG,CAAC,EAAE;AAChE;","names":[]}
@@ -0,0 +1,211 @@
1
+ import {
2
+ ALL_TEMPLATES,
3
+ templatesByName
4
+ } from "./chunk-CASCWQQX.js";
5
+ import {
6
+ withRecipe
7
+ } from "./chunk-XKOZUVK2.js";
8
+ import {
9
+ listTrackedFiles,
10
+ removeFromIndex
11
+ } from "./chunk-XTK5VIQB.js";
12
+
13
+ // src/recipes/sync-configs.ts
14
+ import { readFile, writeFile, mkdir } from "fs/promises";
15
+ import { join, dirname } from "path";
16
+
17
+ // src/recipes/sync-configs/gitignore.ts
18
+ var MANAGED_MARKER = "# canonical entries from @reddoorla/maintenance sync-configs";
19
+ var CANONICAL_GITIGNORE_ENTRIES = [
20
+ "node_modules/",
21
+ "build/",
22
+ "dist/",
23
+ ".svelte-kit/",
24
+ "coverage/",
25
+ ".vitest-cache/",
26
+ "playwright-report/",
27
+ "test-results/",
28
+ ".lighthouseci/",
29
+ ".tsbuildinfo",
30
+ ".env",
31
+ ".env.*",
32
+ "!.env.example",
33
+ ".DS_Store",
34
+ "*.log",
35
+ ".vercel/",
36
+ ".netlify/",
37
+ ".reddoor-a11y/",
38
+ // The a11y audit's transient spec dir, written inside the checkout and
39
+ // normally cleaned, but a timeout-SIGKILL of the parent orphans it. Ignored
40
+ // fleet-wide so it never dirties a self-updating repo's tree (2026-06-10 M-D).
41
+ ".reddoor-a11y-spec-*/"
42
+ ];
43
+ function stripLeadingSlash(s) {
44
+ return s.startsWith("/") ? s.slice(1) : s;
45
+ }
46
+ function stripTrailingSlash(s) {
47
+ return s.endsWith("/") ? s.slice(0, -1) : s;
48
+ }
49
+ function normalizePresence(line) {
50
+ return stripTrailingSlash(stripLeadingSlash(line.trim()));
51
+ }
52
+ function presentSet(existing) {
53
+ const set = /* @__PURE__ */ new Set();
54
+ for (const raw of existing.split(/\r?\n/)) {
55
+ const trimmed = raw.trim();
56
+ if (!trimmed) continue;
57
+ if (trimmed.startsWith("#")) continue;
58
+ set.add(normalizePresence(trimmed));
59
+ }
60
+ return set;
61
+ }
62
+ function mergeGitignore(existing, canonical) {
63
+ if (existing === null) {
64
+ const body = [MANAGED_MARKER, ...canonical].join("\n") + "\n";
65
+ return { content: body, added: [...canonical] };
66
+ }
67
+ const present = presentSet(existing);
68
+ const added = [];
69
+ for (const entry of canonical) {
70
+ const norm = normalizePresence(entry);
71
+ if (!present.has(norm)) {
72
+ added.push(entry);
73
+ present.add(norm);
74
+ }
75
+ }
76
+ if (added.length === 0) {
77
+ return { content: existing, added: [] };
78
+ }
79
+ let base = existing;
80
+ if (!base.endsWith("\n")) base += "\n";
81
+ const block = ["", MANAGED_MARKER, ...added].join("\n") + "\n";
82
+ return { content: base + block, added };
83
+ }
84
+ function findTrackedArtifacts(tracked, canonical) {
85
+ const dirEntries = [];
86
+ for (const raw of canonical) {
87
+ const t = raw.trim();
88
+ if (!t) continue;
89
+ if (t.startsWith("!")) continue;
90
+ if (/[*?[]/.test(t)) continue;
91
+ const noLead = stripLeadingSlash(t);
92
+ if (!noLead.endsWith("/")) continue;
93
+ const name = stripTrailingSlash(noLead);
94
+ if (!name) continue;
95
+ dirEntries.push(name);
96
+ }
97
+ const matched = [];
98
+ for (const path of tracked) {
99
+ for (const dir of dirEntries) {
100
+ if (path === dir || path.startsWith(dir + "/")) {
101
+ matched.push(path);
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ return matched;
107
+ }
108
+
109
+ // src/recipes/sync-configs.ts
110
+ var GITIGNORE_CONFIG = "gitignore";
111
+ var SVELTE_CONFIG = "svelte";
112
+ var NETLIFY_CONFIG = "netlify";
113
+ function isSvelteConfigCompliant(contents) {
114
+ return contents.includes("createSvelteConfig") && contents.includes("@sveltejs/adapter-netlify");
115
+ }
116
+ var SECURITY_HEADER_RE = /Strict-Transport-Security|Content-Security-Policy|X-Frame-Options|X-Content-Type-Options|Referrer-Policy|Permissions-Policy|Cross-Origin-Opener-Policy/i;
117
+ function isNetlifyConfigCompliant(contents) {
118
+ return contents.includes("[[headers]]") && SECURITY_HEADER_RE.test(contents);
119
+ }
120
+ var ALL_CONFIG_NAMES = [
121
+ "lighthouse",
122
+ "eslint",
123
+ "prettier",
124
+ "prettier-ignore",
125
+ "playwright-a11y",
126
+ "svelte",
127
+ "gitignore",
128
+ "ci",
129
+ "renovate-action",
130
+ "renovate-config",
131
+ "netlify"
132
+ ];
133
+ function isConfigName(value) {
134
+ return ALL_CONFIG_NAMES.includes(value);
135
+ }
136
+ async function readMaybe(path) {
137
+ try {
138
+ return await readFile(path, "utf-8");
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ async function planTemplateDiffs(cwd, templates) {
144
+ const diffs = [];
145
+ for (const t of templates) {
146
+ const existing = await readMaybe(join(cwd, t.path));
147
+ if (existing === t.contents) continue;
148
+ if (t.config === SVELTE_CONFIG && existing !== null && isSvelteConfigCompliant(existing)) {
149
+ continue;
150
+ }
151
+ if (t.config === NETLIFY_CONFIG && existing !== null && isNetlifyConfigCompliant(existing)) {
152
+ continue;
153
+ }
154
+ diffs.push(t);
155
+ }
156
+ return diffs;
157
+ }
158
+ async function planGitignore(cwd) {
159
+ const existing = await readMaybe(join(cwd, ".gitignore"));
160
+ const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
161
+ const tracked = await listTrackedFiles(cwd);
162
+ const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);
163
+ if (merge.added.length === 0 && toUntrack.length === 0) return { kind: "noop" };
164
+ return { kind: "apply", content: merge.content, toUntrack, added: merge.added };
165
+ }
166
+ async function applyGitignore(cwd, plan) {
167
+ await writeFile(join(cwd, ".gitignore"), plan.content, "utf-8");
168
+ if (plan.toUntrack.length > 0) {
169
+ await removeFromIndex(cwd, plan.toUntrack);
170
+ }
171
+ }
172
+ async function syncConfigs(site, opts = {}) {
173
+ const requested = opts.which ?? ALL_TEMPLATES.map((t) => t.config).concat(GITIGNORE_CONFIG);
174
+ const templateNames = requested.filter((c) => c !== GITIGNORE_CONFIG);
175
+ const templates = templatesByName(templateNames);
176
+ const includeGitignore = requested.includes(GITIGNORE_CONFIG);
177
+ return withRecipe({
178
+ name: "sync-configs",
179
+ site,
180
+ plan: async () => {
181
+ const templateDiffs = await planTemplateDiffs(site.path, templates);
182
+ const gitignorePlan = includeGitignore ? await planGitignore(site.path) : { kind: "noop" };
183
+ if (templateDiffs.length === 0 && gitignorePlan.kind === "noop") {
184
+ return { kind: "noop", notes: "all targeted configs already match" };
185
+ }
186
+ return { kind: "apply", plan: { templateDiffs, gitignorePlan } };
187
+ },
188
+ apply: async ({ templateDiffs, gitignorePlan }, { commit }) => {
189
+ for (const t of templateDiffs) {
190
+ const dest = join(site.path, t.path);
191
+ await mkdir(dirname(dest), { recursive: true });
192
+ await writeFile(dest, t.contents, "utf-8");
193
+ await commit(`chore: sync ${t.config} config from @reddoorla/maintenance`);
194
+ }
195
+ if (gitignorePlan.kind === "apply") {
196
+ await applyGitignore(site.path, gitignorePlan);
197
+ await commit(`chore: sync gitignore from @reddoorla/maintenance`);
198
+ }
199
+ return { kind: "ok" };
200
+ }
201
+ });
202
+ }
203
+
204
+ export {
205
+ CANONICAL_GITIGNORE_ENTRIES,
206
+ mergeGitignore,
207
+ ALL_CONFIG_NAMES,
208
+ isConfigName,
209
+ syncConfigs
210
+ };
211
+ //# sourceMappingURL=chunk-JNXCDUYB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recipes/sync-configs.ts","../src/recipes/sync-configs/gitignore.ts"],"sourcesContent":["import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport type { RecipeResult, Site, ConfigName } from \"../types.js\";\nimport { ALL_TEMPLATES, templatesByName, type ConfigTemplate } from \"./sync-configs/templates.js\";\nimport {\n CANONICAL_GITIGNORE_ENTRIES,\n mergeGitignore,\n findTrackedArtifacts,\n} from \"./sync-configs/gitignore.js\";\nimport { listTrackedFiles, removeFromIndex } from \"../util/git.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type SyncConfigsOptions = {\n which?: ConfigName[];\n};\n\nconst GITIGNORE_CONFIG: ConfigName = \"gitignore\";\nconst SVELTE_CONFIG: ConfigName = \"svelte\";\nconst NETLIFY_CONFIG: ConfigName = \"netlify\";\n\n/** A site's `svelte.config.js` is \"compliant\" — and left untouched by sync —\n * once it builds on the canonical helpers (createSvelteConfig + adapter-netlify).\n *\n * Unlike the other exact-match templates, svelte.config legitimately carries\n * site-specific `kit.alias` and `compilerOptions`; an exact overwrite would\n * clobber those on every sync (it silently dropped MSOT's $utils alias,\n * 2026-06-04). So once a config is on the canonical pattern we preserve it as-is\n * and only rewrite a genuinely off-pattern (or missing) config. `createSvelteConfig`\n * now provides the canonical `$lib` aliases itself, and a site's own `kit.alias`\n * overrides per key (and may add more), so a site's additive customization is safe\n * to preserve. */\nfunction isSvelteConfigCompliant(contents: string): boolean {\n return contents.includes(\"createSvelteConfig\") && contents.includes(\"@sveltejs/adapter-netlify\");\n}\n\n/** Any of the baseline security headers — the marker that a netlify.toml is\n * deliberately hardened (vs. e.g. a cache-control-only `[[headers]]` block). */\nconst SECURITY_HEADER_RE =\n /Strict-Transport-Security|Content-Security-Policy|X-Frame-Options|X-Content-Type-Options|Referrer-Policy|Permissions-Policy|Cross-Origin-Opener-Policy/i;\n\n/** A site's `netlify.toml` is \"compliant\" — and left untouched by sync — once it\n * carries a `[[headers]]` block AND a security header (HSTS/CSP/X-Frame-Options/…).\n *\n * Like svelte.config, netlify.toml legitimately holds site-specific config\n * (custom CSP, redirects, per-route headers). The canonical template ships the\n * baseline security headers, but an exact overwrite would CLOBBER a site's own\n * hardening — that bug stripped gallerysonder's headers on a routine sync\n * (2026-06-10). So a genuinely-hardened file is left alone, while a missing,\n * header-less (previously-stripped), OR merely cache-header file is non-compliant\n * and gets the canonical template, which backfills the security baseline. */\nfunction isNetlifyConfigCompliant(contents: string): boolean {\n return contents.includes(\"[[headers]]\") && SECURITY_HEADER_RE.test(contents);\n}\n\n/** Runtime enumeration of every `ConfigName`. Mirror of the union in\n * `src/types.ts`. Used by CLI `--only` validation; a missing entry would\n * silently accept typos. The type-test in `tests/types.test.ts` guards\n * against drift between this array and the union. */\nexport const ALL_CONFIG_NAMES: ConfigName[] = [\n \"lighthouse\",\n \"eslint\",\n \"prettier\",\n \"prettier-ignore\",\n \"playwright-a11y\",\n \"svelte\",\n \"gitignore\",\n \"ci\",\n \"renovate-action\",\n \"renovate-config\",\n \"netlify\",\n];\n\nexport function isConfigName(value: string): value is ConfigName {\n return (ALL_CONFIG_NAMES as string[]).includes(value);\n}\n\nasync function readMaybe(path: string): Promise<string | null> {\n try {\n return await readFile(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nasync function planTemplateDiffs(\n cwd: string,\n templates: ConfigTemplate[],\n): Promise<ConfigTemplate[]> {\n const diffs: ConfigTemplate[] = [];\n for (const t of templates) {\n const existing = await readMaybe(join(cwd, t.path));\n if (existing === t.contents) continue;\n // svelte.config is compliance-checked, not exact-matched: an existing config\n // already on the canonical pattern is left alone so its aliases/compilerOptions\n // survive. A missing (null) or off-pattern config still gets the canonical template.\n if (t.config === SVELTE_CONFIG && existing !== null && isSvelteConfigCompliant(existing)) {\n continue;\n }\n // netlify.toml is likewise compliance-checked: a file that already carries\n // `[[headers]]` is hardened and left alone (an exact overwrite would strip\n // its security headers). A header-less / missing file gets the template.\n if (t.config === NETLIFY_CONFIG && existing !== null && isNetlifyConfigCompliant(existing)) {\n continue;\n }\n diffs.push(t);\n }\n return diffs;\n}\n\ntype GitignorePlan =\n | { kind: \"noop\" }\n | { kind: \"apply\"; content: string; toUntrack: string[]; added: string[] };\n\nasync function planGitignore(cwd: string): Promise<GitignorePlan> {\n const existing = await readMaybe(join(cwd, \".gitignore\"));\n const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);\n const tracked = await listTrackedFiles(cwd);\n const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);\n if (merge.added.length === 0 && toUntrack.length === 0) return { kind: \"noop\" };\n return { kind: \"apply\", content: merge.content, toUntrack, added: merge.added };\n}\n\nasync function applyGitignore(\n cwd: string,\n plan: Extract<GitignorePlan, { kind: \"apply\" }>,\n): Promise<void> {\n await writeFile(join(cwd, \".gitignore\"), plan.content, \"utf-8\");\n if (plan.toUntrack.length > 0) {\n await removeFromIndex(cwd, plan.toUntrack);\n }\n}\n\nexport async function syncConfigs(\n site: Site,\n opts: SyncConfigsOptions = {},\n): Promise<RecipeResult> {\n const requested = opts.which ?? ALL_TEMPLATES.map((t) => t.config).concat(GITIGNORE_CONFIG);\n const templateNames = requested.filter((c): c is ConfigName => c !== GITIGNORE_CONFIG);\n const templates = templatesByName(templateNames);\n const includeGitignore = requested.includes(GITIGNORE_CONFIG);\n\n return withRecipe({\n name: \"sync-configs\",\n site,\n plan: async () => {\n const templateDiffs = await planTemplateDiffs(site.path, templates);\n const gitignorePlan: GitignorePlan = includeGitignore\n ? await planGitignore(site.path)\n : { kind: \"noop\" };\n if (templateDiffs.length === 0 && gitignorePlan.kind === \"noop\") {\n return { kind: \"noop\", notes: \"all targeted configs already match\" };\n }\n return { kind: \"apply\", plan: { templateDiffs, gitignorePlan } };\n },\n apply: async ({ templateDiffs, gitignorePlan }, { commit }) => {\n for (const t of templateDiffs) {\n const dest = join(site.path, t.path);\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, t.contents, \"utf-8\");\n await commit(`chore: sync ${t.config} config from @reddoorla/maintenance`);\n }\n if (gitignorePlan.kind === \"apply\") {\n await applyGitignore(site.path, gitignorePlan);\n await commit(`chore: sync gitignore from @reddoorla/maintenance`);\n }\n return { kind: \"ok\" };\n },\n });\n}\n","/**\n * Comment line written above the appended block so future runs (and humans)\n * can recognize the managed section. Presence of this line is incidental —\n * the merge logic is keyed on each entry's normalized form, not on the marker.\n */\nexport const MANAGED_MARKER = \"# canonical entries from @reddoorla/maintenance sync-configs\";\n\n/**\n * Build artifacts, test outputs, deploy caches, and secrets that should never\n * be tracked across the reddoor fleet. Sites may keep additional site-specific\n * entries — they are preserved on merge.\n */\nexport const CANONICAL_GITIGNORE_ENTRIES: readonly string[] = [\n \"node_modules/\",\n \"build/\",\n \"dist/\",\n \".svelte-kit/\",\n \"coverage/\",\n \".vitest-cache/\",\n \"playwright-report/\",\n \"test-results/\",\n \".lighthouseci/\",\n \".tsbuildinfo\",\n \".env\",\n \".env.*\",\n \"!.env.example\",\n \".DS_Store\",\n \"*.log\",\n \".vercel/\",\n \".netlify/\",\n \".reddoor-a11y/\",\n // The a11y audit's transient spec dir, written inside the checkout and\n // normally cleaned, but a timeout-SIGKILL of the parent orphans it. Ignored\n // fleet-wide so it never dirties a self-updating repo's tree (2026-06-10 M-D).\n \".reddoor-a11y-spec-*/\",\n];\n\nexport type MergeResult = { content: string; added: string[] };\n\nfunction stripLeadingSlash(s: string): string {\n return s.startsWith(\"/\") ? s.slice(1) : s;\n}\n\nfunction stripTrailingSlash(s: string): string {\n return s.endsWith(\"/\") ? s.slice(0, -1) : s;\n}\n\n/**\n * Normalize for presence comparison only: strip leading `/`, trailing `/`,\n * and surrounding whitespace. `build`, `/build`, `build/`, and `/build/` all\n * collapse to the same key.\n */\nfunction normalizePresence(line: string): string {\n return stripTrailingSlash(stripLeadingSlash(line.trim()));\n}\n\nfunction presentSet(existing: string): Set<string> {\n const set = new Set<string>();\n for (const raw of existing.split(/\\r?\\n/)) {\n const trimmed = raw.trim();\n if (!trimmed) continue;\n if (trimmed.startsWith(\"#\")) continue;\n set.add(normalizePresence(trimmed));\n }\n return set;\n}\n\n/**\n * Merge `canonical` entries into `existing` .gitignore content.\n *\n * - Missing entries are appended under a managed marker comment.\n * - Existing entries (in any normalized variant — `/build`, `build/`, etc.)\n * are preserved as-is; we never rewrite the site's own lines.\n * - When every canonical entry is already present, returns the original\n * content unchanged with `added: []` — the recipe can treat that as noop.\n */\nexport function mergeGitignore(existing: string | null, canonical: readonly string[]): MergeResult {\n if (existing === null) {\n const body = [MANAGED_MARKER, ...canonical].join(\"\\n\") + \"\\n\";\n return { content: body, added: [...canonical] };\n }\n const present = presentSet(existing);\n const added: string[] = [];\n for (const entry of canonical) {\n const norm = normalizePresence(entry);\n if (!present.has(norm)) {\n added.push(entry);\n present.add(norm);\n }\n }\n if (added.length === 0) {\n return { content: existing, added: [] };\n }\n let base = existing;\n if (!base.endsWith(\"\\n\")) base += \"\\n\";\n const block = [\"\", MANAGED_MARKER, ...added].join(\"\\n\") + \"\\n\";\n return { content: base + block, added };\n}\n\n/**\n * Of the tracked paths, return those that fall under a canonical *directory*\n * entry — i.e., paths that the freshly-synced .gitignore now wants ignored\n * but which git currently has in the index.\n *\n * File-pattern entries (`.env`, `*.log`, `.DS_Store`) are intentionally\n * skipped: they may contain user-meaningful data, and `git rm --cached`\n * cannot scrub secrets from history anyway. Surfaced for manual review\n * instead of auto-removing.\n */\nexport function findTrackedArtifacts(\n tracked: readonly string[],\n canonical: readonly string[],\n): string[] {\n const dirEntries: string[] = [];\n for (const raw of canonical) {\n const t = raw.trim();\n if (!t) continue;\n if (t.startsWith(\"!\")) continue;\n if (/[*?[]/.test(t)) continue;\n const noLead = stripLeadingSlash(t);\n if (!noLead.endsWith(\"/\")) continue;\n const name = stripTrailingSlash(noLead);\n if (!name) continue;\n dirEntries.push(name);\n }\n const matched: string[] = [];\n for (const path of tracked) {\n for (const dir of dirEntries) {\n if (path === dir || path.startsWith(dir + \"/\")) {\n matched.push(path);\n break;\n }\n }\n }\n return matched;\n}\n"],"mappings":"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","names":[]}