@reddoorla/maintenance 0.66.0 → 0.67.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 (84) hide show
  1. package/dist/airtable-33SY2UBZ.js +9 -0
  2. package/dist/{announce-YPXVHNNP.js → announce-74ZIAV26.js} +4 -4
  3. package/dist/{bump-deps-INDDTDSG.js → bump-deps-HYBYNPMY.js} +2 -2
  4. package/dist/{chunk-LISJ3K3R.js → chunk-7MJJEEII.js} +2 -2
  5. package/dist/{chunk-4P6UQG5L.js → chunk-CL5DYUWQ.js} +4 -1
  6. package/dist/chunk-CL5DYUWQ.js.map +1 -0
  7. package/dist/{chunk-XKKTK6WA.js → chunk-HIN3P4ZN.js} +23 -2
  8. package/dist/chunk-HIN3P4ZN.js.map +1 -0
  9. package/dist/{chunk-CH6K2VMM.js → chunk-JDGIO54P.js} +2 -2
  10. package/dist/{chunk-JD3X3QMP.js → chunk-KN5OC5YH.js} +3 -3
  11. package/dist/{chunk-XKSKHHSX.js → chunk-OAMJINM5.js} +3 -3
  12. package/dist/{chunk-G2YRXV26.js → chunk-OTBA7DON.js} +2 -2
  13. package/dist/{chunk-PTPTVVY7.js → chunk-PH3AGF4Z.js} +2 -2
  14. package/dist/{chunk-YBYWNUT3.js → chunk-T34EUM5B.js} +2 -2
  15. package/dist/{chunk-G35B5U6F.js → chunk-ULL4AD3Y.js} +2 -2
  16. package/dist/{chunk-NMGGP5WN.js → chunk-Y4O44LZB.js} +2 -2
  17. package/dist/{chunk-CWSQZ27M.js → chunk-YKOOAWVL.js} +3 -3
  18. package/dist/cli/bin.js +15 -15
  19. package/dist/cli/commands/audit.js +8 -8
  20. package/dist/client-NVVQK3HH.js +11 -0
  21. package/dist/{convert-to-pnpm-4GD4667L.js → convert-to-pnpm-5MPZWFHM.js} +2 -2
  22. package/dist/{db-YBLQR2YY.js → db-3QW2XEXA.js} +3 -3
  23. package/dist/{digest-SIZUXT67.js → digest-MECMZPFE.js} +3 -3
  24. package/dist/forms/index.d.ts +13 -0
  25. package/dist/forms/index.js +28 -2
  26. package/dist/forms/index.js.map +1 -1
  27. package/dist/{github-signals-3O64PFWP.js → github-signals-VWBMQNLH.js} +7 -7
  28. package/dist/index.d.ts +12 -1
  29. package/dist/index.js +27 -15
  30. package/dist/index.js.map +1 -1
  31. package/dist/{init-KMWXJZEX.js → init-X3BCJAUG.js} +2 -2
  32. package/dist/{launch-5KRCKFYM.js → launch-NQCL5K5H.js} +5 -5
  33. package/dist/{lighthouse-airtable-5JICCZND.js → lighthouse-airtable-QUNOSDT5.js} +3 -3
  34. package/dist/migrate-35DNNF3H.js +7 -0
  35. package/dist/{onboard-RCBXS5B3.js → onboard-7JOBZY35.js} +2 -2
  36. package/dist/{orchestrate-O4KR6JME.js → orchestrate-JQX72RJE.js} +6 -6
  37. package/dist/{renovate-dispatch-ABRSIT77.js → renovate-dispatch-PWXFZNHN.js} +2 -2
  38. package/dist/{report-LSGTMXQJ.js → report-QUKNKUUE.js} +5 -5
  39. package/dist/{self-updating-CPMY3HFY.js → self-updating-OKJKU2FE.js} +2 -2
  40. package/dist/{selftest-7Y5K4PYL.js → selftest-6JVYR24F.js} +8 -8
  41. package/dist/{svelte-codemods-GJRTBLJE.js → svelte-codemods-GG6EEES5.js} +2 -2
  42. package/dist/{sync-configs-NYJNSIRN.js → sync-configs-UFHZZPL7.js} +2 -2
  43. package/dist/{upgrade-LVYRDKDL.js → upgrade-S25N3473.js} +2 -2
  44. package/dist/{websites-5ETIXB7F.js → websites-U3UZS5VG.js} +2 -2
  45. package/dist/{write-audits-to-airtable-GHR337N7.js → write-audits-to-airtable-GAJUCSTY.js} +4 -4
  46. package/package.json +1 -1
  47. package/dist/airtable-M4AQHRNV.js +0 -9
  48. package/dist/chunk-4P6UQG5L.js.map +0 -1
  49. package/dist/chunk-XKKTK6WA.js.map +0 -1
  50. package/dist/client-C2GOZD2D.js +0 -11
  51. package/dist/migrate-BFOG2OCP.js +0 -7
  52. /package/dist/{airtable-M4AQHRNV.js.map → airtable-33SY2UBZ.js.map} +0 -0
  53. /package/dist/{announce-YPXVHNNP.js.map → announce-74ZIAV26.js.map} +0 -0
  54. /package/dist/{bump-deps-INDDTDSG.js.map → bump-deps-HYBYNPMY.js.map} +0 -0
  55. /package/dist/{chunk-LISJ3K3R.js.map → chunk-7MJJEEII.js.map} +0 -0
  56. /package/dist/{chunk-CH6K2VMM.js.map → chunk-JDGIO54P.js.map} +0 -0
  57. /package/dist/{chunk-JD3X3QMP.js.map → chunk-KN5OC5YH.js.map} +0 -0
  58. /package/dist/{chunk-XKSKHHSX.js.map → chunk-OAMJINM5.js.map} +0 -0
  59. /package/dist/{chunk-G2YRXV26.js.map → chunk-OTBA7DON.js.map} +0 -0
  60. /package/dist/{chunk-PTPTVVY7.js.map → chunk-PH3AGF4Z.js.map} +0 -0
  61. /package/dist/{chunk-YBYWNUT3.js.map → chunk-T34EUM5B.js.map} +0 -0
  62. /package/dist/{chunk-G35B5U6F.js.map → chunk-ULL4AD3Y.js.map} +0 -0
  63. /package/dist/{chunk-NMGGP5WN.js.map → chunk-Y4O44LZB.js.map} +0 -0
  64. /package/dist/{chunk-CWSQZ27M.js.map → chunk-YKOOAWVL.js.map} +0 -0
  65. /package/dist/{client-C2GOZD2D.js.map → client-NVVQK3HH.js.map} +0 -0
  66. /package/dist/{convert-to-pnpm-4GD4667L.js.map → convert-to-pnpm-5MPZWFHM.js.map} +0 -0
  67. /package/dist/{db-YBLQR2YY.js.map → db-3QW2XEXA.js.map} +0 -0
  68. /package/dist/{digest-SIZUXT67.js.map → digest-MECMZPFE.js.map} +0 -0
  69. /package/dist/{github-signals-3O64PFWP.js.map → github-signals-VWBMQNLH.js.map} +0 -0
  70. /package/dist/{init-KMWXJZEX.js.map → init-X3BCJAUG.js.map} +0 -0
  71. /package/dist/{launch-5KRCKFYM.js.map → launch-NQCL5K5H.js.map} +0 -0
  72. /package/dist/{lighthouse-airtable-5JICCZND.js.map → lighthouse-airtable-QUNOSDT5.js.map} +0 -0
  73. /package/dist/{migrate-BFOG2OCP.js.map → migrate-35DNNF3H.js.map} +0 -0
  74. /package/dist/{onboard-RCBXS5B3.js.map → onboard-7JOBZY35.js.map} +0 -0
  75. /package/dist/{orchestrate-O4KR6JME.js.map → orchestrate-JQX72RJE.js.map} +0 -0
  76. /package/dist/{renovate-dispatch-ABRSIT77.js.map → renovate-dispatch-PWXFZNHN.js.map} +0 -0
  77. /package/dist/{report-LSGTMXQJ.js.map → report-QUKNKUUE.js.map} +0 -0
  78. /package/dist/{self-updating-CPMY3HFY.js.map → self-updating-OKJKU2FE.js.map} +0 -0
  79. /package/dist/{selftest-7Y5K4PYL.js.map → selftest-6JVYR24F.js.map} +0 -0
  80. /package/dist/{svelte-codemods-GJRTBLJE.js.map → svelte-codemods-GG6EEES5.js.map} +0 -0
  81. /package/dist/{sync-configs-NYJNSIRN.js.map → sync-configs-UFHZZPL7.js.map} +0 -0
  82. /package/dist/{upgrade-LVYRDKDL.js.map → upgrade-S25N3473.js.map} +0 -0
  83. /package/dist/{websites-5ETIXB7F.js.map → websites-U3UZS5VG.js.map} +0 -0
  84. /package/dist/{write-audits-to-airtable-GHR337N7.js.map → write-audits-to-airtable-GAJUCSTY.js.map} +0 -0
@@ -0,0 +1,9 @@
1
+ import {
2
+ fromAirtableBase
3
+ } from "./chunk-Y4O44LZB.js";
4
+ import "./chunk-U5SZ4FOP.js";
5
+ import "./chunk-CL5DYUWQ.js";
6
+ export {
7
+ fromAirtableBase
8
+ };
9
+ //# sourceMappingURL=airtable-33SY2UBZ.js.map
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  scoresFromRow
3
- } from "./chunk-XKSKHHSX.js";
3
+ } from "./chunk-OAMJINM5.js";
4
4
  import {
5
5
  fetchGaUsers,
6
6
  fetchSearch,
7
7
  readGaConfig
8
- } from "./chunk-G35B5U6F.js";
8
+ } from "./chunk-ULL4AD3Y.js";
9
9
  import {
10
10
  queueDraft
11
11
  } from "./chunk-6LIWDIXU.js";
@@ -28,7 +28,7 @@ import {
28
28
  listWebsites,
29
29
  siteSlug,
30
30
  updateAnalyticsHealth
31
- } from "./chunk-4P6UQG5L.js";
31
+ } from "./chunk-CL5DYUWQ.js";
32
32
  import {
33
33
  openBase,
34
34
  readAirtableConfig
@@ -185,4 +185,4 @@ export {
185
185
  formatAnnounceResult,
186
186
  runAnnounceCommand
187
187
  };
188
- //# sourceMappingURL=announce-YPXVHNNP.js.map
188
+ //# sourceMappingURL=announce-74ZIAV26.js.map
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-4NAOR44V.js";
9
9
  import {
10
10
  resolveSites
11
- } from "./chunk-LISJ3K3R.js";
11
+ } from "./chunk-7MJJEEII.js";
12
12
  import {
13
13
  bumpDeps
14
14
  } from "./chunk-D3DQFC32.js";
@@ -56,4 +56,4 @@ async function runBumpDepsCommand(site, opts) {
56
56
  export {
57
57
  runBumpDepsCommand
58
58
  };
59
- //# sourceMappingURL=bump-deps-INDDTDSG.js.map
59
+ //# sourceMappingURL=bump-deps-HYBYNPMY.js.map
@@ -30,7 +30,7 @@ async function resolveSites(input) {
30
30
  }
31
31
  if (input.fleet === "airtable") {
32
32
  const { openBase, readAirtableConfig } = await import("./client-EPVO3FEH.js");
33
- const { fromAirtableBase } = await import("./airtable-M4AQHRNV.js");
33
+ const { fromAirtableBase } = await import("./airtable-33SY2UBZ.js");
34
34
  const base = openBase(readAirtableConfig());
35
35
  const provider = fromAirtableBase(base, input.workdir ? { workdir: input.workdir } : {});
36
36
  return provider();
@@ -61,4 +61,4 @@ async function resolveSites(input) {
61
61
  export {
62
62
  resolveSites
63
63
  };
64
- //# sourceMappingURL=chunk-LISJ3K3R.js.map
64
+ //# sourceMappingURL=chunk-7MJJEEII.js.map
@@ -100,6 +100,9 @@ function mapRow(rec) {
100
100
  notifyRouting: parseNotifyRouting(f["Notify Routing"]),
101
101
  mailchimpApiKey: trimToNull(f["Mailchimp API Key"]),
102
102
  mailchimpAudienceId: trimToNull(f["Mailchimp Audience ID"]),
103
+ // Boolean guard like crossbrowserOk, but defaults FALSE (not null) when absent: an
104
+ // unset/unknown column must read as "not required" so the feature ships dark.
105
+ requireTurnstile: typeof f["Require Turnstile"] === "boolean" ? f["Require Turnstile"] : false,
103
106
  renovateFailingCis: f["Renovate Failing CIs"] ?? null,
104
107
  defaultBranchCi: f["Default Branch CI"] ?? null,
105
108
  lastCommitAt: f["Last Commit At"] ?? null,
@@ -312,4 +315,4 @@ export {
312
315
  updateGitHubSignals,
313
316
  updateLaunched
314
317
  };
315
- //# sourceMappingURL=chunk-4P6UQG5L.js.map
318
+ //# sourceMappingURL=chunk-CL5DYUWQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/airtable/websites.ts"],"sourcesContent":["import type { FieldSet } from \"airtable\";\nimport type { AirtableBase } from \"./client.js\";\nimport type { LighthouseScores, LighthouseScoreWriteback } from \"../types.js\";\n\nexport const WEBSITES_TABLE = \"Websites\";\n\nexport type Frequency = \"None\" | \"Monthly\" | \"Quarterly\" | \"Yearly\";\n\nexport type Status =\n | \"in development\"\n | \"launch period\"\n | \"maintenance\"\n | \"hosting\"\n | \"probably not our problem\"\n | \"deprecated\";\n\n/**\n * Per-site notification routing. When present on a `maintenance` site, the form\n * notification is addressed by the value of a submission field (`field`, read from\n * `extraFields`) — e.g. route a contact form's `interest` to a different recipient\n * per option, always CC-ing a shared address. Absent (`null`) → the site keeps the\n * default single-POC behavior. Recipients live HERE (server-side Airtable config),\n * never supplied by the submitting site, so the ingest can't be turned into an open\n * relay.\n */\nexport type NotifyRouting = {\n /** The `extraFields` key whose value selects a route, e.g. \"interest\". */\n field: string;\n /** Field-value → recipient address(es). */\n routes: Record<string, string | string[]>;\n /** Recipient(s) when the value matches no route. */\n default?: string | string[];\n /** Address(es) CC'd on every routed (maintenance) send. */\n cc?: string[];\n};\n\nexport type WebsiteRow = {\n id: string;\n name: string;\n url: string;\n status: Status | null;\n pointOfContact: string | null;\n maintenanceFreq: Frequency;\n testingFreq: Frequency;\n /** Last manually-recorded maintenance day (used as fallback when no Reports row exists). */\n maintenanceDay: string | null;\n testingDay: string | null;\n ga4PropertyId: string | null;\n /** Operator-supplied query for the Google search-presence check (e.g. the business name).\n * Null = no query set → the check is skipped for this site. */\n searchQuery: string | null;\n /** Explicit Search Console property for this site (`sc-domain:...` or `https://.../`).\n * Null = auto-resolve from the SA's visible properties by host. */\n searchConsoleProperty: string | null;\n /** ISO timestamp of the last draft run where THIS site's GA/Search enrichment\n * ERRORED (vs a legitimate \"not configured\" skip). Set by drafting on a soft-fail,\n * cleared (null) on a clean enrichment, so the per-site analytics-failure signal\n * self-heals. Null when the operator-added `Analytics soft-fail at` column is absent. */\n analyticsSoftFailAt: string | null;\n /** GitHub repo identity as `owner/repo`. Null = no git wiring → self-update ops skip\n * (or, for local runs, fall back to the checkout's origin remote). */\n gitRepo: string | null;\n reportRecipientsTo: string | null;\n reportRecipientsCc: string | null;\n acceptedWatchConditions: string[];\n /** First attachment in the Header image field (Airtable's signed URL — fetch before expiry). */\n headerImage: { url: string; filename: string; type: string } | null;\n /** Lighthouse \"current state\" snapshot, kept fresh by `audit lighthouse --write-airtable`. */\n pScore: number | null;\n rScore: number | null;\n bpScore: number | null;\n seoScore: number | null;\n /** ISO timestamp set by `audit lighthouse --write-airtable` when scores were last refreshed. */\n lastLighthouseAuditAt: string | null;\n /** Last-known counts from non-lighthouse audits, written by\n * `audit --write-airtable`. `null` = never audited (or this audit\n * type was skipped on the last run). 0 = audited, clean. */\n a11yViolations: number | null;\n /** Declared-range drift vs the Reddoor baseline (what package.json asks for). */\n depsDrifted: number | null;\n depsMajorBehind: number | null;\n /** Real installed-version drift: deps behind the registry's latest, from the\n * committed lockfile (`pnpm outdated`). Null = not determined this run. */\n depsOutdated: number | null;\n /** Of {@link depsOutdated}, how many are a *major* version behind the\n * registry's latest — the \"majors available on npm\" signal, distinct from the\n * baseline-drift {@link depsMajorBehind}. Null = not determined this run. */\n depsMajorOutdated: number | null;\n securityVulnsCritical: number | null;\n securityVulnsHigh: number | null;\n securityVulnsModerate: number | null;\n securityVulnsLow: number | null;\n /** Count of consecutive nightly Renovate auto-fix dispatches for the CURRENT\n * critical/high vuln episode that have NOT yet cleared it. Owned by\n * `renovate-dispatch`: +1 per real dispatch, reset to 0 when vulns clear.\n * Null = field absent / never dispatched → reads as 0. At/above\n * AUTO_FIX_EXHAUSTED_CYCLES the vuln renders as \"auto-fix failed\". */\n securityAutoFixAttempts: number | null;\n /** ISO timestamp the security audit last ran — gates freshness of the Security Updates auto-tick\n * (clean counts only auto-tick when recent). */\n lastSecurityAuditAt: string | null;\n /** The known advisories behind the counts (severity-sorted, capped), so the dashboard can show\n * WHICH packages are vulnerable, not just the totals. null = never audited / unparseable;\n * empty array = audited clean. Written alongside the counts by the security audit. */\n securityAdvisories: SecurityAdvisory[] | null;\n /** Domain/DNS/SSL probe (the `domain` audit). `certDaysRemaining` is days until the TLS cert\n * expires (null = unresolved or no usable cert); `domainCheckedAt` is when it last ran. */\n certDaysRemaining: number | null;\n domainCheckedAt: string | null;\n /** Netlify site id — the IDENTITY the `netlify-deploy` audit needs to query the\n * Netlify API. Read-only input (operator-set in Airtable); null = not on Netlify\n * (or not wired) → that audit skips. Never derived from the URL. */\n netlifyId: string | null;\n /** Latest PRODUCTION deploy health (the `netlify-deploy` audit). `deployStatus` is\n * Netlify's deploy state lower-cased (`ready`/`error`/`building`/…), null = none / not\n * checked; `lastDeployAt` is when it deployed; `deployLogUrl` links to the deploy/log. */\n deployStatus: string | null;\n lastDeployAt: string | null;\n deployLogUrl: string | null;\n /** Deployed-URL browser probe (the `browser` audit): cross-engine render OK, mobile render OK,\n * internal-links OK + broken count, and when it last ran (one timestamp gates all three). */\n crossbrowserOk: boolean | null;\n mobileOk: boolean | null;\n linksOk: boolean | null;\n brokenLinks: number | null;\n browserCheckedAt: string | null;\n /** Per-site copy overrides (M6a). Blank → null → the DEFAULT_COPY value. */\n copyIntro: string | null;\n copyContact: string | null;\n copyFooter: string | null;\n /** Go-live timestamp, stamped when a Launch report sends (M6b). Null = not yet launched. */\n launchedAt: string | null;\n /** Optional per-site webhook (e.g. Zapier Catch Hook). When set, the ingest\n * POSTs newsletter-formType submissions here (best-effort). Blank → null. */\n newsletterWebhook: string | null;\n /** Per-site Mailchimp (newsletter). Both must be set for the direct add;\n * blank → skipped. The API key is `key-dc` format; dc is derived from it. */\n mailchimpApiKey: string | null;\n mailchimpAudienceId: string | null;\n /** Per-site Cloudflare Turnstile gate. When true, a submission whose Turnstile\n * token verifies as \"fail\" is escalated to auto-spam regardless of content score\n * (an absent/unverifiable token stays neutral — fail-open). Airtable checkbox. */\n requireTurnstile: boolean;\n /** GitHub-signals sweep (slice 2a), written nightly by `github-signals --fleet`. */\n renovateFailingCis: number | null;\n defaultBranchCi: string | null; // \"passing\" | \"failing\" | \"pending\" | \"none\"\n lastCommitAt: string | null;\n githubSignalsAt: string | null;\n notifyRouting: NotifyRouting | null;\n};\n\nexport function siteSlug(name: string): string {\n return name\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\n/** Blank-trim-to-null: a non-string or whitespace-only value becomes null,\n * otherwise the trimmed string. */\nfunction trimToNull(raw: unknown): string | null {\n if (typeof raw !== \"string\") return null;\n const trimmed = raw.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\n/**\n * Parse the Websites `Notify Routing` JSON into a NotifyRouting, defensively: a\n * non-string, blank, malformed-JSON, or wrong-shape value yields null (the site\n * then keeps default single-POC routing) — never throws. Mirrors the pipeline's\n * \"a bad Airtable string degrades quietly\" rule.\n */\nexport function parseNotifyRouting(raw: unknown): NotifyRouting | null {\n if (typeof raw !== \"string\") return null;\n const trimmed = raw.trim();\n if (!trimmed) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n const o = parsed as Record<string, unknown>;\n if (typeof o.field !== \"string\" || !o.field.trim()) return null;\n if (!o.routes || typeof o.routes !== \"object\" || Array.isArray(o.routes)) return null;\n const routing: NotifyRouting = {\n field: o.field,\n routes: o.routes as Record<string, string | string[]>,\n };\n if (o.default !== undefined) routing.default = o.default as string | string[];\n if (Array.isArray(o.cc)) routing.cc = o.cc.filter((x): x is string => typeof x === \"string\");\n return routing;\n}\n\n/**\n * Active sites: actively-maintained or pre-launch. Single source of truth for\n * \"is this a live site\" — the operator cockpit shows these, and the fleet\n * audit/report path runs against these. A `null` status (not-yet-active) is\n * deliberately excluded.\n */\nexport const ACTIVE_STATUSES: ReadonlySet<Status> = new Set<Status>([\n \"maintenance\",\n \"launch period\",\n]);\n\nexport function isDashboardVisible(site: WebsiteRow): boolean {\n return site.status !== null && ACTIVE_STATUSES.has(site.status);\n}\n\nconst FREQUENCIES: readonly Frequency[] = [\"None\", \"Monthly\", \"Quarterly\", \"Yearly\"];\n\n/** Coerce an Airtable single-select value to a known Frequency. An unrecognized value — a\n * renamed / typo'd / whitespace option — falls back to \"None\" (its section is simply omitted)\n * rather than flowing a bogus string downstream, which the announcement would otherwise render\n * as \"We do this undefined.\" into a client email. */\nfunction toFrequency(raw: unknown): Frequency {\n return typeof raw === \"string\" && (FREQUENCIES as readonly string[]).includes(raw)\n ? (raw as Frequency)\n : \"None\";\n}\n\n// NOTE: every `f[\"...\"]` key below is a load-bearing magic string that must match\n// the live Airtable \"Websites\" column name EXACTLY — including the legacy\n// misspelling `\"maintenence freq\"`, the mixed-case `\"GA4 property ID\"`, and the\n// lowercase `\"url\"` / `\"point of contact\"`. A column rename in Airtable silently\n// returns undefined here (→ null), which degrades quietly (GA skipped, recipients\n// empty) with no error. If you rename a column, change it here too.\nexport function mapRow(rec: { id: string; fields: Record<string, unknown> }): WebsiteRow {\n const f = rec.fields;\n const attachments =\n (f[\"Header image\"] as Array<{ url: string; filename: string; type: string }> | undefined) ?? [];\n const header = attachments[0] ?? null;\n return {\n id: rec.id,\n name: String(f[\"Name\"] ?? \"\"),\n url: String(f[\"url\"] ?? \"\"),\n status: (f[\"Status\"] as Status | undefined) ?? null,\n pointOfContact: (f[\"point of contact\"] as string | undefined) ?? null,\n maintenanceFreq: toFrequency(f[\"maintenence freq\"]),\n testingFreq: toFrequency(f[\"testing freq\"]),\n maintenanceDay: (f[\"maintenance day\"] as string | undefined) ?? null,\n testingDay: (f[\"testing day\"] as string | undefined) ?? null,\n ga4PropertyId: (f[\"GA4 property ID\"] as string | undefined) ?? null,\n searchQuery: (f[\"Search query\"] as string | undefined) ?? null,\n searchConsoleProperty: (f[\"Search Console property\"] as string | undefined) ?? null,\n analyticsSoftFailAt: (f[\"Analytics soft-fail at\"] as string | undefined) ?? null,\n gitRepo: (f[\"Git repo\"] as string | undefined) ?? null,\n reportRecipientsTo: (f[\"Report recipients (To)\"] as string | undefined) ?? null,\n reportRecipientsCc: (f[\"Report recipients (CC)\"] as string | undefined) ?? null,\n acceptedWatchConditions: Array.isArray(f[\"Accepted Watch Conditions\"])\n ? (f[\"Accepted Watch Conditions\"] as string[])\n : [],\n headerImage: header,\n pScore: (f[\"pScore\"] as number | undefined) ?? null,\n rScore: (f[\"rScore\"] as number | undefined) ?? null,\n bpScore: (f[\"bpScore\"] as number | undefined) ?? null,\n seoScore: (f[\"seoScore\"] as number | undefined) ?? null,\n lastLighthouseAuditAt: (f[\"Last lighthouse audit at\"] as string | undefined) ?? null,\n a11yViolations: (f[\"A11y Violations\"] as number | undefined) ?? null,\n depsDrifted: (f[\"Deps Drifted\"] as number | undefined) ?? null,\n depsMajorBehind: (f[\"Deps Major Behind\"] as number | undefined) ?? null,\n depsOutdated: (f[\"Deps Outdated\"] as number | undefined) ?? null,\n depsMajorOutdated: (f[\"Deps Major Outdated\"] as number | undefined) ?? null,\n securityVulnsCritical: (f[\"Security Vulns Critical\"] as number | undefined) ?? null,\n securityVulnsHigh: (f[\"Security Vulns High\"] as number | undefined) ?? null,\n securityVulnsModerate: (f[\"Security Vulns Moderate\"] as number | undefined) ?? null,\n securityVulnsLow: (f[\"Security Vulns Low\"] as number | undefined) ?? null,\n securityAutoFixAttempts: (f[\"Security Auto-Fix Attempts\"] as number | undefined) ?? null,\n lastSecurityAuditAt: (f[\"Last security audit at\"] as string | undefined) ?? null,\n securityAdvisories: parseSecurityAdvisories(f[\"Security advisories\"]),\n certDaysRemaining: (f[\"Cert days remaining\"] as number | undefined) ?? null,\n domainCheckedAt: (f[\"Domain checked at\"] as string | undefined) ?? null,\n netlifyId: trimToNull(f[\"Netlify ID\"]),\n deployStatus: (f[\"Deploy status\"] as string | undefined) ?? null,\n lastDeployAt: (f[\"Last deploy at\"] as string | undefined) ?? null,\n deployLogUrl: (f[\"Deploy log URL\"] as string | undefined) ?? null,\n crossbrowserOk:\n typeof f[\"Crossbrowser OK\"] === \"boolean\" ? (f[\"Crossbrowser OK\"] as boolean) : null,\n mobileOk: typeof f[\"Mobile OK\"] === \"boolean\" ? (f[\"Mobile OK\"] as boolean) : null,\n linksOk: typeof f[\"Links OK\"] === \"boolean\" ? (f[\"Links OK\"] as boolean) : null,\n brokenLinks: typeof f[\"Broken links\"] === \"number\" ? (f[\"Broken links\"] as number) : null,\n browserCheckedAt: (f[\"Browser checked at\"] as string | undefined) ?? null,\n copyIntro: trimToNull(f[\"Copy — Intro\"]),\n copyContact: trimToNull(f[\"Copy — Contact\"]),\n copyFooter: trimToNull(f[\"Copy — Footer\"]),\n launchedAt: (f[\"Launched at\"] as string | undefined) ?? null,\n newsletterWebhook: trimToNull(f[\"Newsletter Webhook\"]),\n notifyRouting: parseNotifyRouting(f[\"Notify Routing\"]),\n mailchimpApiKey: trimToNull(f[\"Mailchimp API Key\"]),\n mailchimpAudienceId: trimToNull(f[\"Mailchimp Audience ID\"]),\n // Boolean guard like crossbrowserOk, but defaults FALSE (not null) when absent: an\n // unset/unknown column must read as \"not required\" so the feature ships dark.\n requireTurnstile:\n typeof f[\"Require Turnstile\"] === \"boolean\" ? (f[\"Require Turnstile\"] as boolean) : false,\n renovateFailingCis: (f[\"Renovate Failing CIs\"] as number | undefined) ?? null,\n defaultBranchCi: (f[\"Default Branch CI\"] as string | undefined) ?? null,\n lastCommitAt: (f[\"Last Commit At\"] as string | undefined) ?? null,\n githubSignalsAt: (f[\"GitHub Signals At\"] as string | undefined) ?? null,\n };\n}\n\nexport async function listWebsites(base: AirtableBase): Promise<WebsiteRow[]> {\n const out: WebsiteRow[] = [];\n await base(WEBSITES_TABLE)\n .select({ pageSize: 100 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) out.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n return out;\n}\n\nexport async function getWebsiteBySlug(\n base: AirtableBase,\n slug: string,\n): Promise<WebsiteRow | null> {\n // Slugs are siteSlug() output: [a-z0-9] segments joined by single hyphens.\n // Reject anything else — it can't match a real row, and it keeps URL-supplied\n // input out of the filter formula below (formula-injection guard).\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) return null;\n\n // Narrow the fetch to the slug-matching row server-side instead of paging the\n // whole table per request (MEDIUM-H). The formula replicates siteSlug() on\n // {Name} — lowercase → non-alnum runs to \"-\" → strip leading/trailing \"-\" —\n // verified against the live base. maxRecords caps it (slug collisions keep the\n // prior first-match-wins behavior).\n const formula = `REGEX_REPLACE(REGEX_REPLACE(LOWER({Name}),\"[^a-z0-9]+\",\"-\"),\"^-|-$\",\"\")=${JSON.stringify(\n slug,\n )}`;\n const rows: WebsiteRow[] = [];\n await base(WEBSITES_TABLE)\n .select({ filterByFormula: formula, maxRecords: 1 })\n .eachPage((records, fetchNextPage) => {\n for (const rec of records) rows.push(mapRow({ id: rec.id, fields: rec.fields }));\n fetchNextPage();\n });\n // Confirm the match in JS too: keeps the function correct if the formula and\n // siteSlug() ever drift, and under test fakes that don't evaluate the formula.\n return rows.find((w) => siteSlug(w.name) === slug) ?? null;\n}\n\n// ── audit-field builders ─────────────────────────────────────────────────────\n// One source of truth for the column-name → value mappings of each audit type.\n// The per-audit `updateXxxCounts` writers delegate to these (for their other\n// callers), and `updateAuditFields` merges whichever are present into ONE write —\n// so the field-name magic strings live in exactly one place.\n\nexport type A11yCounts = { violations: number };\nexport type DepsCounts = {\n drifted: number;\n majorBehind: number;\n outdated: number | null;\n majorOutdated: number | null;\n};\nexport type SecurityCounts = { critical: number; high: number; moderate: number; low: number };\nexport type Severity = \"low\" | \"moderate\" | \"high\" | \"critical\";\n/** One known vulnerability behind the security counts, as persisted/rendered. */\nexport type SecurityAdvisory = {\n module: string;\n severity: Severity;\n title: string;\n cves: string[];\n url: string | null;\n /** GitHub Dependabot dependency scope (\"runtime\" | \"development\"), when known. Lets the\n * dashboard flag build-time-only (\"development\") vulns. Omitted for advisories from the\n * lockfile `pnpm audit` fallback, which carries no per-package graph scope. */\n scope?: \"runtime\" | \"development\";\n};\nexport type DomainResult = { certDaysRemaining: number | null; checkedAt: string };\nexport type NetlifyDeployResult = {\n /** Netlify deploy state lower-cased (`ready`/`error`/`building`/…), null = none. */\n state: string | null;\n /** When the latest production deploy was published/created (ISO), null = unknown. */\n deployedAt: string | null;\n /** Link to the deploy / its build log, null = unknown. */\n logUrl: string | null;\n /** When the audit last ran. */\n checkedAt: string;\n};\nexport type BrowserAuditFields = {\n desktopOk: boolean;\n mobileOk: boolean;\n linksOk: boolean;\n brokenLinks: number;\n checkedAt: string;\n};\n\nfunction scoreFields(scores: LighthouseScoreWriteback): FieldSet {\n // A null score CLEARS the cell (→ dashboard \"—\"), distinguishing a metric that\n // errored this run (e.g. NO_LCP → null performance) from a real low score. This\n // intentionally overwrites a prior value: a run that couldn't measure the metric\n // shouldn't keep showing a stale number. Writing null to clear mirrors\n // updateAnalyticsHealth; the `as FieldSet` cast is needed because airtable's\n // FieldSet type omits null.\n const fields: Record<string, number | string | null> = {\n pScore: scores.performance,\n rScore: scores.accessibility,\n bpScore: scores.bestPractices,\n seoScore: scores.seo,\n \"Last lighthouse audit at\": new Date().toISOString(),\n };\n return fields as FieldSet;\n}\n\nfunction a11yFields(counts: A11yCounts): FieldSet {\n return { \"A11y Violations\": counts.violations };\n}\n\nfunction depsFields(counts: DepsCounts): FieldSet {\n const fields: FieldSet = {\n \"Deps Drifted\": counts.drifted,\n \"Deps Major Behind\": counts.majorBehind,\n };\n // Only write the outdated count when it was determined — a null (no/stale\n // lockfile this run) must not clobber a previously-good value.\n if (counts.outdated !== null) {\n fields[\"Deps Outdated\"] = counts.outdated;\n }\n // Same null-guard for the registry-major breakdown (same source signal).\n if (counts.majorOutdated !== null) {\n fields[\"Deps Major Outdated\"] = counts.majorOutdated;\n }\n return fields;\n}\n\n/** Critical first → low last; the persist cap and the dashboard list both lean on this. */\nexport const SEVERITY_RANK: Record<Severity, number> = {\n critical: 0,\n high: 1,\n moderate: 2,\n low: 3,\n};\n/** Cap persisted advisories so the JSON field can't blow up on a badly-neglected site. */\nconst MAX_PERSISTED_ADVISORIES = 25;\n\n/** Coerce one untrusted value into a SecurityAdvisory, or null if it isn't shaped like one.\n * Shared by the read path (`parseSecurityAdvisories`) and the write path so both validate\n * identically. A missing module or unrecognized severity drops the entry entirely. */\nexport function normalizeSecurityAdvisory(raw: unknown): SecurityAdvisory | null {\n if (!raw || typeof raw !== \"object\") return null;\n const e = raw as Record<string, unknown>;\n const module = typeof e[\"module\"] === \"string\" ? (e[\"module\"] as string) : null;\n const severity = e[\"severity\"];\n if (module === null) return null;\n if (\n severity !== \"low\" &&\n severity !== \"moderate\" &&\n severity !== \"high\" &&\n severity !== \"critical\"\n )\n return null;\n const cves = Array.isArray(e[\"cves\"])\n ? (e[\"cves\"] as unknown[]).filter((c): c is string => typeof c === \"string\")\n : [];\n const scope =\n e[\"scope\"] === \"runtime\" || e[\"scope\"] === \"development\"\n ? (e[\"scope\"] as \"runtime\" | \"development\")\n : undefined;\n return {\n module,\n severity,\n title: typeof e[\"title\"] === \"string\" ? (e[\"title\"] as string) : \"\",\n cves,\n url: typeof e[\"url\"] === \"string\" ? (e[\"url\"] as string) : null,\n ...(scope ? { scope } : {}),\n };\n}\n\n/** Parse the `Security advisories` JSON cell. null when absent/blank/unparseable/not-an-array\n * (treated as \"never audited\"); a valid array (possibly empty = audited clean) otherwise. */\nexport function parseSecurityAdvisories(raw: unknown): SecurityAdvisory[] | null {\n if (typeof raw !== \"string\" || raw.trim() === \"\") return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!Array.isArray(parsed)) return null;\n return parsed.map(normalizeSecurityAdvisory).filter((a): a is SecurityAdvisory => a !== null);\n}\n\nfunction securityAdvisoryFields(advisories: SecurityAdvisory[]): FieldSet {\n const capped = [...advisories]\n .sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])\n .slice(0, MAX_PERSISTED_ADVISORIES);\n // Always write — an empty array (\"[]\") clears a stale list when a once-vulnerable site goes clean.\n return { \"Security advisories\": JSON.stringify(capped) };\n}\n\nfunction securityFields(counts: SecurityCounts): FieldSet {\n return {\n \"Security Vulns Critical\": counts.critical,\n \"Security Vulns High\": counts.high,\n \"Security Vulns Moderate\": counts.moderate,\n \"Security Vulns Low\": counts.low,\n // Stamp freshness alongside the counts so the Security Updates auto-tick can require a recent\n // audit (a clean count from months ago must not silently keep ticking the box).\n \"Last security audit at\": new Date().toISOString(),\n };\n}\n\nfunction domainFields(result: DomainResult): FieldSet {\n const fields: FieldSet = { \"Domain checked at\": result.checkedAt };\n // Write the cert days UNCONDITIONALLY: a null (unresolved / no usable cert) must CLEAR any\n // previously-good value. Leaving a stale number in place — next to a freshly-stamped \"Domain\n // checked at\" — false-passes the Domain/DNS/SSL auto-tick (domainEvidence reads the stale\n // non-null value as a current pass) for a site that's actually down. null clears the cell in\n // Airtable; FieldSet's type doesn't model null, hence the cast through a widened record.\n (fields as Record<string, number | null>)[\"Cert days remaining\"] = result.certDaysRemaining;\n return fields;\n}\n\nfunction netlifyDeployFields(result: NetlifyDeployResult): FieldSet {\n // Write all three deploy fields UNCONDITIONALLY (null clears the cell): a null state /\n // deployedAt / logUrl this run means \"couldn't read it\" and must not leave a STALE value\n // sitting next to a fresh \"Deploy checked at\" — the dashboard would otherwise render a green\n // \"ready\" badge for a site whose latest deploy actually errored. FieldSet's type doesn't model\n // null, hence the cast through a widened record (same approach as domainFields).\n const fields: Record<string, string | null> = {\n \"Deploy status\": result.state,\n \"Last deploy at\": result.deployedAt,\n \"Deploy log URL\": result.logUrl,\n \"Deploy checked at\": result.checkedAt,\n };\n return fields as FieldSet;\n}\n\nfunction browserFields(r: BrowserAuditFields): FieldSet {\n return {\n \"Crossbrowser OK\": r.desktopOk,\n \"Mobile OK\": r.mobileOk,\n \"Links OK\": r.linksOk,\n \"Broken links\": r.brokenLinks,\n \"Browser checked at\": r.checkedAt,\n };\n}\n\n/**\n * Write the four Lighthouse scores + a refreshed-at timestamp onto a Websites row.\n * Called by `audit lighthouse --write-airtable` after a successful audit run, so\n * the operator never has to paste numbers manually before drafting a report.\n */\nexport async function updateScores(\n base: AirtableBase,\n recordId: string,\n scores: LighthouseScores,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: scoreFields(scores) }]);\n}\n\n/**\n * Record (or clear) the per-site GA/Search enrichment health on the `Analytics\n * soft-fail at` column. `at` is an ISO timestamp when the site's last draft had a\n * GA/Search soft-failure, or `null` to clear it after a clean enrichment. The\n * caller (drafting) swallows errors: this column is operator-added, so until it\n * exists Airtable throws UNKNOWN_FIELD_NAME — which must not break a draft.\n */\nexport async function updateAnalyticsHealth(\n base: AirtableBase,\n recordId: string,\n at: string | null,\n): Promise<void> {\n const fields: Record<string, string | null> = { \"Analytics soft-fail at\": at };\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: fields as FieldSet }]);\n}\n\n/** Persist a11y violation count. */\nexport async function updateA11yCounts(\n base: AirtableBase,\n recordId: string,\n counts: A11yCounts,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: a11yFields(counts) }]);\n}\n\n/** Persist deps drift counts (declared-range drift + real outdated installs). */\nexport async function updateDepsCounts(\n base: AirtableBase,\n recordId: string,\n counts: DepsCounts,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: depsFields(counts) }]);\n}\n\n/** Persist security vulnerability counts by severity. */\nexport async function updateSecurityCounts(\n base: AirtableBase,\n recordId: string,\n counts: SecurityCounts,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: securityFields(counts) }]);\n}\n\n/** Persist a site's auto-fix attempt counter. Its own one-field writer so the\n * nightly Renovate dispatch can update it without touching the audit's counts. */\nexport async function updateAutoFixAttempts(\n base: AirtableBase,\n recordId: string,\n attempts: number,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([\n { id: recordId, fields: { \"Security Auto-Fix Attempts\": attempts } },\n ]);\n}\n\n/**\n * Persist the code-computed next-due dates (date-only `YYYY-MM-DD`, or `null` to\n * clear) for the maintenance + testing schedules. Owned by the nightly `--due` sweep\n * so the \"next\" dates shown in Airtable come from the SAME logic as the scheduler\n * (`nextDueDate`) — no Airtable-side formula or automation. Best-effort at the call\n * site: the `Next … at` columns are operator-added, so until they exist Airtable\n * throws UNKNOWN_FIELD_NAME, which must not break the nightly draft run.\n */\nexport async function updateNextDueDates(\n base: AirtableBase,\n recordId: string,\n dates: { maintenanceAt: string | null; testingAt: string | null },\n): Promise<void> {\n const fields: Record<string, string | null> = {\n \"Next maintenance at\": dates.maintenanceAt,\n \"Next testing at\": dates.testingAt,\n };\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: fields as FieldSet }]);\n}\n\n/** Generic single-field writer for the dashboard site-details editor. The caller\n * (setSiteDetail) restricts `column` to the EDITABLE_SITE_FIELDS allowlist, so this\n * never writes an arbitrary column from request input. */\nexport async function updateSiteField(\n base: AirtableBase,\n recordId: string,\n column: string,\n value: string,\n): Promise<void> {\n await base(WEBSITES_TABLE).update([{ id: recordId, fields: { [column]: value } }]);\n}\n\n/**\n * Persist all of a single audit run's results to one Websites row in ONE atomic\n * `update()` — instead of up to four sequential updates on the same id (which left\n * a row half-written on a mid-sequence failure and quadrupled the request volume).\n * Pass only the audit slices that produced real values; each present slice is merged\n * via the SAME field mappings the per-audit writers use. Omit a slice (or pass\n * undefined) to leave those columns untouched. Returns the merged FieldSet so the\n * caller can enumerate what was written.\n */\nexport async function updateAuditFields(\n base: AirtableBase,\n recordId: string,\n audits: {\n scores?: LighthouseScoreWriteback;\n a11y?: A11yCounts;\n deps?: DepsCounts;\n security?: SecurityCounts;\n securityAdvisories?: SecurityAdvisory[];\n domain?: DomainResult;\n browser?: BrowserAuditFields;\n netlifyDeploy?: NetlifyDeployResult;\n },\n): Promise<FieldSet> {\n const fields: FieldSet = {};\n if (audits.scores) Object.assign(fields, scoreFields(audits.scores));\n if (audits.a11y) Object.assign(fields, a11yFields(audits.a11y));\n if (audits.deps) Object.assign(fields, depsFields(audits.deps));\n if (audits.security) Object.assign(fields, securityFields(audits.security));\n // Separate slice (not folded into `security`) so the advisory list and the counts can be\n // written independently, but in practice the security audit supplies both together.\n if (audits.securityAdvisories)\n Object.assign(fields, securityAdvisoryFields(audits.securityAdvisories));\n if (audits.domain) Object.assign(fields, domainFields(audits.domain));\n if (audits.browser) Object.assign(fields, browserFields(audits.browser));\n if (audits.netlifyDeploy) Object.assign(fields, netlifyDeployFields(audits.netlifyDeploy));\n await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);\n return fields;\n}\n\n/** Persist the GitHub-signals sweep onto a Websites row (slice 2a). A null\n * `lastCommitAt` is OMITTED so a not-determined-this-run value never clobbers a\n * previously-good timestamp (mirrors updateDepsCounts' outdated handling). */\nexport async function updateGitHubSignals(\n base: AirtableBase,\n recordId: string,\n signals: {\n renovateFailingCis: number;\n ciState: string;\n lastCommitAt: string | null;\n sweptAt: string;\n },\n): Promise<void> {\n const fields: FieldSet = {\n \"Renovate Failing CIs\": signals.renovateFailingCis,\n \"Default Branch CI\": signals.ciState,\n \"GitHub Signals At\": signals.sweptAt,\n };\n if (signals.lastCommitAt !== null) {\n fields[\"Last Commit At\"] = signals.lastCommitAt;\n }\n await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);\n}\n\n/** Mark a site launched: flip Status → maintenance + stamp Launched at (M6b).\n * The first code that writes Status. Called after a Launch report sends. */\nexport async function updateLaunched(\n base: AirtableBase,\n recordId: string,\n at: string,\n): Promise<void> {\n const fields: FieldSet = { Status: \"maintenance\", \"Launched at\": at };\n await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);\n}\n"],"mappings":"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","names":[]}
@@ -49,10 +49,27 @@ var MIGRATIONS = [
49
49
  );
50
50
  CREATE INDEX IF NOT EXISTS idx_fleet_events_ts ON fleet_events (ts);
51
51
  `
52
+ },
53
+ {
54
+ id: "0003_add_spam_score",
55
+ // SQLite `ADD COLUMN` has no `IF NOT EXISTS`, so it is NOT self-idempotent: if the
56
+ // `_migrations` marker is lost after the column is added (crash between statement
57
+ // and marker write), a re-run would throw `duplicate column name`. `migrate.ts`
58
+ // recognizes that error as already-applied and records the marker, keeping the
59
+ // runner idempotent (see `isAlreadyAppliedError`).
60
+ sql: `ALTER TABLE submissions ADD COLUMN spam_score REAL;`
61
+ },
62
+ {
63
+ id: "0004_add_spam_reason",
64
+ sql: `ALTER TABLE submissions ADD COLUMN spam_reason TEXT;`
52
65
  }
53
66
  ];
54
67
 
55
68
  // src/db/migrate.ts
69
+ function isAlreadyAppliedError(err) {
70
+ const message = err instanceof Error ? err.message : String(err);
71
+ return /duplicate column name/i.test(message);
72
+ }
56
73
  async function runMigrations(client) {
57
74
  await client.execute(
58
75
  "CREATE TABLE IF NOT EXISTS _migrations (id TEXT PRIMARY KEY, applied_at TEXT NOT NULL)"
@@ -62,7 +79,11 @@ async function runMigrations(client) {
62
79
  const ran = [];
63
80
  for (const m of MIGRATIONS) {
64
81
  if (applied.has(m.id)) continue;
65
- await client.executeMultiple(m.sql);
82
+ try {
83
+ await client.executeMultiple(m.sql);
84
+ } catch (err) {
85
+ if (!isAlreadyAppliedError(err)) throw err;
86
+ }
66
87
  await client.execute({
67
88
  sql: "INSERT INTO _migrations (id, applied_at) VALUES (?, ?)",
68
89
  args: [m.id, (/* @__PURE__ */ new Date()).toISOString()]
@@ -75,4 +96,4 @@ async function runMigrations(client) {
75
96
  export {
76
97
  runMigrations
77
98
  };
78
- //# sourceMappingURL=chunk-XKKTK6WA.js.map
99
+ //# sourceMappingURL=chunk-HIN3P4ZN.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 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","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":";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;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaP;AAAA,EACA;AAAA,IACE,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMJ,KAAK;AAAA,EACP;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,KAAK;AAAA,EACP;AACF;;;ACjEA,SAAS,sBAAsB,KAAuB;AACpD,QAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,SAAO,yBAAyB,KAAK,OAAO;AAC9C;AASA,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,QAAI;AACF,YAAM,OAAO,gBAAgB,EAAE,GAAG;AAAA,IACpC,SAAS,KAAK;AAIZ,UAAI,CAAC,sBAAsB,GAAG,EAAG,OAAM;AAAA,IACzC;AACA,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":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  siteSlug
3
- } from "./chunk-4P6UQG5L.js";
3
+ } from "./chunk-CL5DYUWQ.js";
4
4
 
5
5
  // src/alerts/digest-collectors.ts
6
6
  function dashboardUrl(baseUrl, siteName) {
@@ -208,4 +208,4 @@ export {
208
208
  readDigestState,
209
209
  writeDigestState
210
210
  };
211
- //# sourceMappingURL=chunk-CH6K2VMM.js.map
211
+ //# sourceMappingURL=chunk-JDGIO54P.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  hasRealScores,
3
3
  lighthouseScoresFromResult
4
- } from "./chunk-YBYWNUT3.js";
4
+ } from "./chunk-T34EUM5B.js";
5
5
  import {
6
6
  detectAuditEvents
7
7
  } from "./chunk-5L3FPQIJ.js";
@@ -9,7 +9,7 @@ import {
9
9
  normalizeSecurityAdvisory,
10
10
  siteSlug,
11
11
  updateAuditFields
12
- } from "./chunk-4P6UQG5L.js";
12
+ } from "./chunk-CL5DYUWQ.js";
13
13
 
14
14
  // src/audits/a11y-airtable.ts
15
15
  function hasA11yCounts(result) {
@@ -237,4 +237,4 @@ export {
237
237
  formatFleetWriteSummary,
238
238
  writeFleetAuditsToAirtable
239
239
  };
240
- //# sourceMappingURL=chunk-JD3X3QMP.js.map
240
+ //# sourceMappingURL=chunk-KN5OC5YH.js.map
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  fetchGaUsers,
3
3
  fetchSearch
4
- } from "./chunk-G35B5U6F.js";
4
+ } from "./chunk-ULL4AD3Y.js";
5
5
  import {
6
6
  announcementSiteExtras,
7
7
  resolveCopy
8
8
  } from "./chunk-SYTPVYIK.js";
9
9
  import {
10
10
  siteSlug
11
- } from "./chunk-4P6UQG5L.js";
11
+ } from "./chunk-CL5DYUWQ.js";
12
12
 
13
13
  // src/reports/report-data.ts
14
14
  var PREVIEW_WINDOW_DAYS = 30;
@@ -60,4 +60,4 @@ export {
60
60
  scoresFromRow,
61
61
  buildReportDataForSite
62
62
  };
63
- //# sourceMappingURL=chunk-XKSKHHSX.js.map
63
+ //# sourceMappingURL=chunk-OAMJINM5.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  openDb,
3
3
  readDbConfig
4
- } from "./chunk-PTPTVVY7.js";
4
+ } from "./chunk-PH3AGF4Z.js";
5
5
 
6
6
  // src/db/fleet-events.ts
7
7
  async function recordFleetEvent(db, e) {
@@ -42,4 +42,4 @@ async function recordFleetEventsBestEffort(events, now, open = () => openDb(read
42
42
  export {
43
43
  recordFleetEventsBestEffort
44
44
  };
45
- //# sourceMappingURL=chunk-G2YRXV26.js.map
45
+ //# sourceMappingURL=chunk-OTBA7DON.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runMigrations
3
- } from "./chunk-XKKTK6WA.js";
3
+ } from "./chunk-HIN3P4ZN.js";
4
4
  import {
5
5
  defaultCredentialsPath
6
6
  } from "./chunk-6VAL7XJT.js";
@@ -48,4 +48,4 @@ export {
48
48
  readDbConfig,
49
49
  openDb
50
50
  };
51
- //# sourceMappingURL=chunk-PTPTVVY7.js.map
51
+ //# sourceMappingURL=chunk-PH3AGF4Z.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  siteSlug
3
- } from "./chunk-4P6UQG5L.js";
3
+ } from "./chunk-CL5DYUWQ.js";
4
4
 
5
5
  // src/audits/lighthouse-airtable.ts
6
6
  import { readFile } from "fs/promises";
@@ -48,4 +48,4 @@ export {
48
48
  lighthouseScoresFromResult,
49
49
  resolveSlugFromCwd
50
50
  };
51
- //# sourceMappingURL=chunk-YBYWNUT3.js.map
51
+ //# sourceMappingURL=chunk-T34EUM5B.js.map
@@ -17,7 +17,7 @@ import {
17
17
  import {
18
18
  siteSlug,
19
19
  updateAnalyticsHealth
20
- } from "./chunk-4P6UQG5L.js";
20
+ } from "./chunk-CL5DYUWQ.js";
21
21
  import {
22
22
  defaultCredentialsPath
23
23
  } from "./chunk-6VAL7XJT.js";
@@ -441,4 +441,4 @@ export {
441
441
  fetchGaUsers,
442
442
  fetchSearch
443
443
  };
444
- //# sourceMappingURL=chunk-G35B5U6F.js.map
444
+ //# sourceMappingURL=chunk-ULL4AD3Y.js.map
@@ -5,7 +5,7 @@ import {
5
5
  ACTIVE_STATUSES,
6
6
  listWebsites,
7
7
  siteSlug
8
- } from "./chunk-4P6UQG5L.js";
8
+ } from "./chunk-CL5DYUWQ.js";
9
9
 
10
10
  // src/inventory/airtable.ts
11
11
  function fromAirtableBase(base, opts = {}) {
@@ -47,4 +47,4 @@ function fromAirtableBase(base, opts = {}) {
47
47
  export {
48
48
  fromAirtableBase
49
49
  };
50
- //# sourceMappingURL=chunk-NMGGP5WN.js.map
50
+ //# sourceMappingURL=chunk-Y4O44LZB.js.map
@@ -19,12 +19,12 @@ import {
19
19
  } from "./chunk-OCS7SZUZ.js";
20
20
  import {
21
21
  recordFleetEventsBestEffort
22
- } from "./chunk-G2YRXV26.js";
22
+ } from "./chunk-OTBA7DON.js";
23
23
  import {
24
24
  listWebsites,
25
25
  siteSlug,
26
26
  updateLaunched
27
- } from "./chunk-4P6UQG5L.js";
27
+ } from "./chunk-CL5DYUWQ.js";
28
28
  import {
29
29
  openBase,
30
30
  readAirtableConfig
@@ -303,4 +303,4 @@ export {
303
303
  parseAddresses,
304
304
  isProbablyEmail
305
305
  };
306
- //# sourceMappingURL=chunk-CWSQZ27M.js.map
306
+ //# sourceMappingURL=chunk-YKOOAWVL.js.map
package/dist/cli/bin.js CHANGED
@@ -105,7 +105,7 @@ cli.command("sync-configs [site]", "Sync canonical configs into a site.").option
105
105
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
106
106
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
107
107
  async (site, opts) => runOrExit(
108
- async () => (await import("../sync-configs-NYJNSIRN.js")).runSyncConfigsCommand(site, opts),
108
+ async () => (await import("../sync-configs-UFHZZPL7.js")).runSyncConfigsCommand(site, opts),
109
109
  opts
110
110
  )
111
111
  );
@@ -114,7 +114,7 @@ cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>",
114
114
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
115
115
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
116
116
  async (site, opts) => runOrExit(
117
- async () => (await import("../bump-deps-INDDTDSG.js")).runBumpDepsCommand(site, opts),
117
+ async () => (await import("../bump-deps-HYBYNPMY.js")).runBumpDepsCommand(site, opts),
118
118
  opts
119
119
  )
120
120
  );
@@ -123,7 +123,7 @@ cli.command(
123
123
  "Bootstrap a repo to keep itself updated (CI + Renovate + auto-merge)."
124
124
  ).option("--dry", "List what would be enabled without writing or opening PRs").option("--fleet <inventory>", 'Inventory file (.json or .mjs/.js), or "airtable"').option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
125
125
  async (site, opts) => runOrExit(
126
- async () => (await import("../self-updating-CPMY3HFY.js")).runSelfUpdatingCommand(site, opts),
126
+ async () => (await import("../self-updating-OKJKU2FE.js")).runSelfUpdatingCommand(site, opts),
127
127
  opts
128
128
  )
129
129
  );
@@ -132,7 +132,7 @@ cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to
132
132
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
133
133
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
134
134
  async (upgrade, site, opts) => runOrExit(
135
- async () => (await import("../upgrade-LVYRDKDL.js")).runUpgradeCommand(upgrade, site, opts),
135
+ async () => (await import("../upgrade-S25N3473.js")).runUpgradeCommand(upgrade, site, opts),
136
136
  opts
137
137
  )
138
138
  );
@@ -144,7 +144,7 @@ cli.command(
144
144
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
145
145
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
146
146
  async (site, opts) => runOrExit(
147
- async () => (await import("../convert-to-pnpm-4GD4667L.js")).runConvertToPnpmCommand(site, opts),
147
+ async () => (await import("../convert-to-pnpm-5MPZWFHM.js")).runConvertToPnpmCommand(site, opts),
148
148
  opts
149
149
  )
150
150
  );
@@ -153,7 +153,7 @@ cli.command("svelte-codemods [site]", "Apply Svelte 5 gotcha codemods to an alre
153
153
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
154
154
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
155
155
  async (site, opts) => runOrExit(
156
- async () => (await import("../svelte-codemods-GJRTBLJE.js")).runSvelteCodemodsCommand(site, opts),
156
+ async () => (await import("../svelte-codemods-GG6EEES5.js")).runSvelteCodemodsCommand(site, opts),
157
157
  opts
158
158
  )
159
159
  );
@@ -165,7 +165,7 @@ cli.command(
165
165
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
166
166
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
167
167
  async (site, opts) => runOrExit(
168
- async () => (await import("../onboard-RCBXS5B3.js")).runOnboardCommand(site, opts),
168
+ async () => (await import("../onboard-7JOBZY35.js")).runOnboardCommand(site, opts),
169
169
  opts
170
170
  )
171
171
  );
@@ -176,14 +176,14 @@ cli.command(
176
176
  "--fleet <inventory>",
177
177
  'Inventory file (.json or .mjs/.js), or "airtable" to read from Websites table'
178
178
  ).option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
179
- async (site, opts) => runOrExit(async () => (await import("../init-KMWXJZEX.js")).runInitCommand(site, opts), opts)
179
+ async (site, opts) => runOrExit(async () => (await import("../init-X3BCJAUG.js")).runInitCommand(site, opts), opts)
180
180
  );
181
181
  cli.command(
182
182
  "launch <site>",
183
183
  "Bootstrap + first-audit a site, then draft its launch email for approval."
184
184
  ).action(
185
185
  async (site, opts) => runOrExit(
186
- async () => (await import("../launch-5KRCKFYM.js")).runLaunchCommand(site, opts),
186
+ async () => (await import("../launch-NQCL5K5H.js")).runLaunchCommand(site, opts),
187
187
  opts
188
188
  )
189
189
  );
@@ -192,7 +192,7 @@ cli.command(
192
192
  "Draft the monthly-report announcement email for maintenance sites (all, or one) for approval."
193
193
  ).action(
194
194
  async (site, opts) => runOrExit(
195
- async () => (await import("../announce-YPXVHNNP.js")).runAnnounceCommand(site, opts),
195
+ async () => (await import("../announce-74ZIAV26.js")).runAnnounceCommand(site, opts),
196
196
  opts
197
197
  )
198
198
  );
@@ -201,7 +201,7 @@ cli.command(
201
201
  "Operator self-tests. kind=email: preview a report email for a site (or --all) to yourself."
202
202
  ).option("--type <type>", "Report type: announcement (default) | maintenance | testing | launch").option("--to <addr>", "Recipient(s), comma-separated. Default: OPERATOR_EMAIL.").option("--all", "Send a preview for every maintenance site (to --to/operator).").option("--dry-run", "Render only; write reports/<slug>/selftest-<type>.html; do not send.").action(
203
203
  async (kind, site, opts) => runOrExit(
204
- async () => (await import("../selftest-7Y5K4PYL.js")).runSelftestCommand(kind, site, opts),
204
+ async () => (await import("../selftest-6JVYR24F.js")).runSelftestCommand(kind, site, opts),
205
205
  opts
206
206
  )
207
207
  );
@@ -216,7 +216,7 @@ cli.command("report [site]", "Draft or send maintenance/testing reports.").optio
216
216
  "Email the operator one daily digest of reports ready for approval (skips when empty)."
217
217
  ).action(
218
218
  async (site, opts) => runOrExit(
219
- async () => (await import("../report-LSGTMXQJ.js")).runReportCommand(site, opts),
219
+ async () => (await import("../report-QUKNKUUE.js")).runReportCommand(site, opts),
220
220
  opts
221
221
  )
222
222
  );
@@ -225,7 +225,7 @@ cli.command(
225
225
  "Sweep the fleet for GitHub signals (Renovate-failing/CI/last-commit) and write Airtable."
226
226
  ).option("--fleet", "Run across every site in the Airtable inventory.").option("--write-airtable", "Write each site's signals back to its Websites row.").action(
227
227
  async (opts) => runOrExit(
228
- async () => (await import("../github-signals-3O64PFWP.js")).runGitHubSignalsCommand({
228
+ async () => (await import("../github-signals-VWBMQNLH.js")).runGitHubSignalsCommand({
229
229
  fleet: opts.fleet,
230
230
  writeAirtable: opts.writeAirtable
231
231
  }),
@@ -236,14 +236,14 @@ cli.command(
236
236
  "db <action>",
237
237
  "Migrate / backfill / reconcile the libSQL store (migrate | backfill | reconcile)."
238
238
  ).action(
239
- async (action, opts) => runOrExit(async () => (await import("../db-YBLQR2YY.js")).runDbCommand(action, opts), opts)
239
+ async (action, opts) => runOrExit(async () => (await import("../db-3QW2XEXA.js")).runDbCommand(action, opts), opts)
240
240
  );
241
241
  cli.command(
242
242
  "renovate-dispatch",
243
243
  "Trigger Renovate on fleet sites the security sweep flagged with critical/high vulns."
244
244
  ).option("--fleet", "Run across every active, repo-backed site in the Airtable inventory.").action(
245
245
  async (opts) => runOrExit(
246
- async () => (await import("../renovate-dispatch-ABRSIT77.js")).runRenovateDispatchCommand({
246
+ async () => (await import("../renovate-dispatch-PWXFZNHN.js")).runRenovateDispatchCommand({
247
247
  fleet: opts.fleet
248
248
  }),
249
249
  opts
@@ -8,7 +8,7 @@ import {
8
8
  } from "../../chunk-4NAOR44V.js";
9
9
  import {
10
10
  resolveSites
11
- } from "../../chunk-LISJ3K3R.js";
11
+ } from "../../chunk-7MJJEEII.js";
12
12
  import {
13
13
  ALL_AUDIT_NAMES,
14
14
  runOneAudit
@@ -16,9 +16,9 @@ import {
16
16
  import "../../chunk-LKGVSM2O.js";
17
17
  import {
18
18
  recordFleetEventsBestEffort
19
- } from "../../chunk-G2YRXV26.js";
20
- import "../../chunk-PTPTVVY7.js";
21
- import "../../chunk-XKKTK6WA.js";
19
+ } from "../../chunk-OTBA7DON.js";
20
+ import "../../chunk-PH3AGF4Z.js";
21
+ import "../../chunk-HIN3P4ZN.js";
22
22
  import "../../chunk-GASBX52O.js";
23
23
  import "../../chunk-YEGHUODU.js";
24
24
  import "../../chunk-FDIFTIXC.js";
@@ -231,9 +231,9 @@ ${skipNotice}`;
231
231
  let writeBackFailed = false;
232
232
  if (opts.writeAirtable !== void 0) {
233
233
  const { openBase, readAirtableConfig } = await import("../../client-EPVO3FEH.js");
234
- const { listWebsites } = await import("../../websites-5ETIXB7F.js");
234
+ const { listWebsites } = await import("../../websites-U3UZS5VG.js");
235
235
  if (opts.fleet !== void 0) {
236
- const { writeFleetAuditsToAirtable, formatFleetWriteSummary } = await import("../../write-audits-to-airtable-GHR337N7.js");
236
+ const { writeFleetAuditsToAirtable, formatFleetWriteSummary } = await import("../../write-audits-to-airtable-GAJUCSTY.js");
237
237
  const base = openBase(readAirtableConfig());
238
238
  const websites = await listWebsites(base);
239
239
  const fleetWrite = await writeFleetAuditsToAirtable({ base, websites, results });
@@ -249,8 +249,8 @@ ${skipNotice}`;
249
249
 
250
250
  ${formatFleetWriteSummary(fleetWrite)}`;
251
251
  } else {
252
- const { resolveSlugFromCwd } = await import("../../lighthouse-airtable-5JICCZND.js");
253
- const { writeAuditsToAirtable } = await import("../../write-audits-to-airtable-GHR337N7.js");
252
+ const { resolveSlugFromCwd } = await import("../../lighthouse-airtable-QUNOSDT5.js");
253
+ const { writeAuditsToAirtable } = await import("../../write-audits-to-airtable-GAJUCSTY.js");
254
254
  const slug = typeof opts.writeAirtable === "string" && opts.writeAirtable.length > 0 ? opts.writeAirtable : await resolveSlugFromCwd(cwd);
255
255
  let writeSummary = null;
256
256
  await new Listr(
@@ -0,0 +1,11 @@
1
+ import {
2
+ openDb,
3
+ readDbConfig
4
+ } from "./chunk-PH3AGF4Z.js";
5
+ import "./chunk-HIN3P4ZN.js";
6
+ import "./chunk-6VAL7XJT.js";
7
+ export {
8
+ openDb,
9
+ readDbConfig
10
+ };
11
+ //# sourceMappingURL=client-NVVQK3HH.js.map
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-4NAOR44V.js";
9
9
  import {
10
10
  resolveSites
11
- } from "./chunk-LISJ3K3R.js";
11
+ } from "./chunk-7MJJEEII.js";
12
12
  import {
13
13
  convertToPnpm
14
14
  } from "./chunk-2X322ZES.js";
@@ -50,4 +50,4 @@ async function runConvertToPnpmCommand(site, opts) {
50
50
  export {
51
51
  runConvertToPnpmCommand
52
52
  };
53
- //# sourceMappingURL=convert-to-pnpm-4GD4667L.js.map
53
+ //# sourceMappingURL=convert-to-pnpm-5MPZWFHM.js.map
@@ -1,9 +1,9 @@
1
1
  // src/cli/commands/db.ts
2
2
  async function runDbCommand(action, opts) {
3
3
  if (action === "migrate") {
4
- const { readDbConfig } = await import("./client-C2GOZD2D.js");
4
+ const { readDbConfig } = await import("./client-NVVQK3HH.js");
5
5
  const cfg = opts.url ? { url: opts.url } : readDbConfig();
6
- const { runMigrations } = await import("./migrate-BFOG2OCP.js");
6
+ const { runMigrations } = await import("./migrate-35DNNF3H.js");
7
7
  const { createClient } = await import("@libsql/client");
8
8
  const client = createClient(cfg.url === ":memory:" ? { url: ":memory:" } : cfg);
9
9
  const ran = await runMigrations(client);
@@ -20,4 +20,4 @@ async function runDbCommand(action, opts) {
20
20
  export {
21
21
  runDbCommand
22
22
  };
23
- //# sourceMappingURL=db-YBLQR2YY.js.map
23
+ //# sourceMappingURL=db-3QW2XEXA.js.map