@reddoorla/maintenance 0.78.0 → 0.80.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.
- package/dist/{announce-RUT247HS.js → announce-KZDU65IF.js} +6 -5
- package/dist/{announce-RUT247HS.js.map → announce-KZDU65IF.js.map} +1 -1
- package/dist/{chunk-FVVGDIGX.js → chunk-42A4H6SF.js} +2 -2
- package/dist/{chunk-SEQA7MC3.js → chunk-62VF32IC.js} +2 -2
- package/dist/{chunk-6SZBNALD.js → chunk-6M2IXJ3E.js} +3 -3
- package/dist/{chunk-MNE374CZ.js → chunk-6QRLRT3P.js} +4 -16
- package/dist/chunk-6QRLRT3P.js.map +1 -0
- package/dist/{chunk-QMXQUL5Z.js → chunk-7ADQL5Y3.js} +2 -2
- package/dist/chunk-A3WDWNN5.js +70 -0
- package/dist/chunk-A3WDWNN5.js.map +1 -0
- package/dist/{chunk-QWFHC3VH.js → chunk-AKRUYPLU.js} +3 -3
- package/dist/{chunk-YSPA5AUT.js → chunk-JMAFXEQE.js} +36 -8
- package/dist/{chunk-YSPA5AUT.js.map → chunk-JMAFXEQE.js.map} +1 -1
- package/dist/{chunk-KPQXB5CR.js → chunk-KKIKJWM2.js} +2 -2
- package/dist/chunk-N4MOEF32.js +62 -0
- package/dist/{chunk-LJPCMXR7.js.map → chunk-N4MOEF32.js.map} +1 -1
- package/dist/{chunk-EYODPUHV.js → chunk-NMAWXBJA.js} +13 -2
- package/dist/chunk-NMAWXBJA.js.map +1 -0
- package/dist/{chunk-YIQ7IWE3.js → chunk-R5KAXGSP.js} +3 -3
- package/dist/chunk-SEEA6RXF.js +18 -0
- package/dist/chunk-SEEA6RXF.js.map +1 -0
- package/dist/{chunk-GWAOX3CU.js → chunk-TEOENEJH.js} +2 -2
- package/dist/{chunk-VSHC4HOI.js → chunk-YAMRVXOD.js} +93 -1
- package/dist/chunk-YAMRVXOD.js.map +1 -0
- package/dist/cli/bin.js +22 -10
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +6 -4
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/configs/lighthouse.d.ts +1 -1
- package/dist/configs/lighthouse.js +1 -1
- package/dist/configs/playwright-a11y.js +1 -1
- package/dist/{digest-NQAQI34P.js → digest-N232AYVG.js} +8 -6
- package/dist/{digest-NQAQI34P.js.map → digest-N232AYVG.js.map} +1 -1
- package/dist/forms/index.js +6 -62
- package/dist/forms/index.js.map +1 -1
- package/dist/forms-notify-target-ZDQUEEEY.js +253 -0
- package/dist/forms-notify-target-ZDQUEEEY.js.map +1 -0
- package/dist/{github-signals-4LWQGD5V.js → github-signals-2BFMZLPV.js} +2 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +20 -17
- package/dist/index.js.map +1 -1
- package/dist/{init-KF4QHMWQ.js → init-RYKQFSZI.js} +8 -6
- package/dist/{init-KF4QHMWQ.js.map → init-RYKQFSZI.js.map} +1 -1
- package/dist/{launch-CVCIP5NU.js → launch-EGKYS3BD.js} +11 -8
- package/dist/{launch-CVCIP5NU.js.map → launch-EGKYS3BD.js.map} +1 -1
- package/dist/{orchestrate-QOQXOJLI.js → orchestrate-NY6L4Q7Q.js} +4 -3
- package/dist/{preflight-2PWVVIRR.js → preflight-SRDQS7Y6.js} +5 -4
- package/dist/{preflight-2PWVVIRR.js.map → preflight-SRDQS7Y6.js.map} +1 -1
- package/dist/{protection-audit-X3BHT2MB.js → protection-audit-2UR45SOC.js} +30 -2
- package/dist/protection-audit-2UR45SOC.js.map +1 -0
- package/dist/{renovate-dispatch-2VYCEH42.js → renovate-dispatch-7D7FMMJP.js} +2 -2
- package/dist/{report-IU4CLJS5.js → report-BGKMN5GU.js} +9 -7
- package/dist/{report-IU4CLJS5.js.map → report-BGKMN5GU.js.map} +1 -1
- package/dist/{self-updating-ALMUWJNX.js → self-updating-WJEHPASK.js} +3 -3
- package/dist/{selftest-PJFABZDM.js → selftest-ZJDB4JQB.js} +7 -6
- package/dist/{selftest-PJFABZDM.js.map → selftest-ZJDB4JQB.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-EYODPUHV.js.map +0 -1
- package/dist/chunk-LJPCMXR7.js +0 -44
- package/dist/chunk-MNE374CZ.js.map +0 -1
- package/dist/chunk-VSHC4HOI.js.map +0 -1
- package/dist/protection-audit-X3BHT2MB.js.map +0 -1
- /package/dist/{chunk-FVVGDIGX.js.map → chunk-42A4H6SF.js.map} +0 -0
- /package/dist/{chunk-SEQA7MC3.js.map → chunk-62VF32IC.js.map} +0 -0
- /package/dist/{chunk-6SZBNALD.js.map → chunk-6M2IXJ3E.js.map} +0 -0
- /package/dist/{chunk-QMXQUL5Z.js.map → chunk-7ADQL5Y3.js.map} +0 -0
- /package/dist/{chunk-QWFHC3VH.js.map → chunk-AKRUYPLU.js.map} +0 -0
- /package/dist/{chunk-KPQXB5CR.js.map → chunk-KKIKJWM2.js.map} +0 -0
- /package/dist/{chunk-YIQ7IWE3.js.map → chunk-R5KAXGSP.js.map} +0 -0
- /package/dist/{chunk-GWAOX3CU.js.map → chunk-TEOENEJH.js.map} +0 -0
- /package/dist/{github-signals-4LWQGD5V.js.map → github-signals-2BFMZLPV.js.map} +0 -0
- /package/dist/{orchestrate-QOQXOJLI.js.map → orchestrate-NY6L4Q7Q.js.map} +0 -0
- /package/dist/{renovate-dispatch-2VYCEH42.js.map → renovate-dispatch-7D7FMMJP.js.map} +0 -0
- /package/dist/{self-updating-ALMUWJNX.js.map → self-updating-WJEHPASK.js.map} +0 -0
package/dist/cli/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/bin.ts","../../src/cli/version.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { cac } from \"cac\";\nimport type { AuditName, RecipeName } from \"../types.js\";\nimport { loadCredentialsIntoEnv } from \"../util/credentials.js\";\nimport { resolvePackageVersion } from \"./version.js\";\n\n// Command modules are loaded LAZILY (dynamic `import()` inside each `.action()`),\n// never eagerly at the top. An eager `import { runReportCommand } from\n// \"./commands/report.js\"` would pull EVERY command's transitive dependency chain\n// into the CLI's startup graph — report/announce/launch drag in mjml + resend +\n// @google-analytics/data, `db` drags in the libSQL/kysely stack, etc. Those heavy\n// packages are `devDependencies` (this repo's CLI/functions/audits use them); a\n// consuming fleet site installs @reddoorla/maintenance only for `./forms` +\n// `./configs/*` and runs just `reddoor-maint audit --only a11y` in CI. Lazy\n// loading keeps that path (and `--help`/`--version`) free of the report/db\n// chains, so those packages never land in a consumer's node_modules. The\n// smoke-dist gate asserts bin.js's STATIC import closure stays free of them.\n\n// Load credentials from ~/.config/reddoor-maint/credentials.env before any\n// command runs, so AIRTABLE_PAT/AIRTABLE_BASE_ID/RESEND_API_KEY/etc. are\n// available from any cwd. Shell-exported env vars still win. Silent on\n// missing file — commands that need the credentials will fail with their\n// own clear error.\nloadCredentialsIntoEnv();\n\nconst here = dirname(fileURLToPath(import.meta.url));\nconst version = resolvePackageVersion(here);\n\nconst AUDIT_DESCRIPTIONS: Record<AuditName, string> = {\n deps: \"Diff site package.json against the bundled baseline version map.\",\n lighthouse: \"Run @lhci/cli autorun using the canonical lighthouserc.\",\n a11y: \"Playwright + axe against the canonical a11y routes.\",\n security:\n \"GitHub Dependabot alerts (prod+dev) for repo-backed sites; falls back to pnpm/npm audit (prod-deps) with no repo/token.\",\n lint: \"ESLint + Prettier using the canonical configs.\",\n domain: \"DNS resolve + TLS cert expiry against the deployed URL (checkout-free).\",\n browser:\n \"Playwright across desktop engines + mobile devices + link-check against the deployed URL (checkout-free).\",\n \"netlify-deploy\":\n \"Latest production deploy health via the Netlify API by site id (checkout-free; needs NETLIFY_PAT).\",\n \"function-health\":\n \"GET {deployedUrl}/health and read its ok/prismic/forms status (checkout-free).\",\n smoke: \"Run the site's own `pnpm test:smoke` suite in its checkout (clone-based).\",\n \"form-e2e\":\n \"Submit the real production contact form in test-mode against the deployed URL (checkout-free).\",\n};\n\nconst RECIPE_DESCRIPTIONS: Record<RecipeName, string> = {\n \"sync-configs\": \"Overwrite a site's canonical configs to match @reddoorla/maintenance.\",\n \"bump-deps\": \"Bump dependencies and commit the lockfile change.\",\n \"svelte-4-to-5\": \"Run the 7-commit Svelte 4 → 5 upgrade recipe.\",\n \"svelte-codemods\":\n \"Apply Svelte 5 gotcha codemods to an already-migrated site (state_referenced_locally, etc.).\",\n \"convert-to-pnpm\": \"Convert an npm/yarn site to pnpm (lockfile, packageManager, scripts).\",\n onboard: \"Install @reddoorla/maintenance + audit deps on a site (preferred first step).\",\n \"a11y-fixtures-page\":\n \"Write src/routes/dev/a11y-fixtures/+page.svelte (stub for lhci + axe targets).\",\n \"health-endpoint\":\n \"Write src/routes/health/+server.ts (function-health probe for the report gate).\",\n \"smoke-suite\": \"Add the smoke suite (test:smoke + playwright config + /health smoke routes).\",\n \"self-updating\":\n \"Bootstrap Renovate + repo protection (writes renovate configs via PR, wires ruleset, disables platform auto-merge; ci.yml comes from the starter and is deliberately not managed).\",\n init: \"Run the full onboarding chain (convert-to-pnpm → onboard → sync-configs → svelte-codemods → a11y-fixtures-page → health-endpoint → smoke-suite → audit).\",\n};\n\n/** Run a command thunk and surface its result, falling back to a clean error\n * message on throw. Wraps the ~10-line try/catch every `.action()` used to\n * duplicate. `verbose` flips between full stack and message-only.\n *\n * On success it sets `process.exitCode` and RETURNS rather than calling\n * `process.exit()` right after `console.log()`. `process.exit()` does not wait\n * for stdout to flush when stdout is a pipe, so a large `--json` payload piped\n * to another process would get truncated mid-write. Setting `exitCode` and\n * returning lets Node drain stdout and exit naturally with the right code. A\n * non-zero `code` still yields a non-zero process exit.\n *\n * The error path keeps `process.exit()` — error messages are small (one line),\n * always go to stderr, and exiting immediately is the desired fail-fast. */\nexport async function runOrExit(\n fn: () => Promise<{ output: string; code: number }>,\n opts: { verbose?: boolean },\n): Promise<void> {\n try {\n const { output, code } = await fn();\n console.log(output);\n process.exitCode = code;\n return;\n } catch (err) {\n const e = err as { exitCode?: number; message?: string; stack?: string };\n console.error(opts.verbose ? (e.stack ?? e.message) : (e.message ?? String(err)));\n process.exit(e.exitCode ?? 1);\n }\n}\n\nconst cli = cac(\"reddoor-maint\");\n\ncli.option(\"--cwd <path>\", \"Override working directory (default: process.cwd())\");\ncli.option(\"--verbose\", \"Verbose output (full stack on errors)\");\n\ncli.command(\"list-audits\", \"Print the available audits.\").action(() => {\n for (const [name, desc] of Object.entries(AUDIT_DESCRIPTIONS)) {\n console.log(`${name.padEnd(12)} ${desc}`);\n }\n});\n\ncli.command(\"list-recipes\", \"Print the available recipes.\").action(() => {\n for (const [name, desc] of Object.entries(RECIPE_DESCRIPTIONS)) {\n console.log(`${name.padEnd(16)} ${desc}`);\n }\n});\n\ncli\n .command(\"audit [site]\", \"Run audits against a site (default: cwd).\")\n .option(\"--only <names>\", \"Comma-separated audit names (e.g. deps,lighthouse)\")\n .option(\"--json\", \"Machine-readable JSON output\")\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .option(\n \"--write-airtable [slug]\",\n \"After lighthouse runs, write pScore/rScore/bpScore/seoScore + timestamp to the matching Websites row. Slug defaults to cwd's package.json#name.\",\n )\n .option(\"--fail-on-violations\", \"Exit non-zero if any a11y violations are found (for CI gates)\")\n .option(\n \"--url <url>\",\n \"Audit this deployed URL with lighthouse (no dev server); single-site. Pair with --only lighthouse — other audits still use the local checkout.\",\n )\n .option(\n \"--concurrency <n>\",\n \"Max sites to audit in parallel in --fleet mode (default: all at once). Use 1 for sequential (CI).\",\n )\n .action(\n async (\n site,\n opts: {\n only?: string;\n json?: boolean;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n verbose?: boolean;\n writeAirtable?: string | boolean;\n failOnViolations?: boolean;\n url?: string;\n concurrency?: string;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/audit.js\")).runAuditCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"sync-configs [site]\", \"Sync canonical configs into a site.\")\n .option(\"--only <names>\", \"Comma-separated config names (e.g. eslint,prettier)\")\n .option(\"--dry\", \"Print diff without writing\")\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (\n site,\n opts: {\n only?: string;\n dry?: boolean;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/sync-configs.js\")).runSyncConfigsCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"bump-deps [site]\", \"Bump dependencies.\")\n .option(\"--group <group>\", \"patch | minor | major\", { default: \"minor\" })\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (\n site,\n opts: {\n group?: string;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/bump-deps.js\")).runBumpDepsCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"self-updating [site]\",\n \"Bootstrap a repo to keep itself updated (Renovate + protection; platform auto-merge off; ci.yml is the starter's, not managed here).\",\n )\n .option(\"--dry\", \"List what would be enabled without writing or opening PRs\")\n .option(\"--fleet <inventory>\", 'Inventory file (.json or .mjs/.js), or \"airtable\"')\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (\n site,\n opts: {\n dry?: boolean;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () =>\n (await import(\"./commands/self-updating.js\")).runSelfUpdatingCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"upgrade <upgrade> [site]\", \"Run a named upgrade recipe (svelte-4-to-5).\")\n .example(\"reddoor-maint upgrade svelte-4-to-5 ./my-site\")\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (\n upgrade: string,\n site: string | undefined,\n opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/upgrade.js\")).runUpgradeCommand(upgrade, site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"convert-to-pnpm [site]\",\n \"Convert an npm/yarn site to pnpm (lockfile, packageManager, scripts).\",\n )\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (site, opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/convert-to-pnpm.js\")).runConvertToPnpmCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"svelte-codemods [site]\", \"Apply Svelte 5 gotcha codemods to an already-migrated site.\")\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (site, opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/svelte-codemods.js\")).runSvelteCodemodsCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"health-endpoint [site]\",\n \"Write src/routes/health/+server.ts (function-health probe for the report gate).\",\n )\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (site, opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/health-endpoint.js\")).runHealthEndpointCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"smoke-suite [site]\",\n \"Add the smoke suite (test:smoke + playwright config + /health smoke routes).\",\n )\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (site, opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () => (await import(\"./commands/smoke-suite.js\")).runSmokeSuiteCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"onboard [site]\",\n \"Install @reddoorla/maintenance + audit deps on a site (run after convert-to-pnpm).\",\n )\n .option(\"--audits <names>\", \"Comma-separated audit subset: lighthouse,a11y (default: both)\")\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (\n site,\n opts: {\n audits?: string;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/onboard.js\")).runOnboardCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"init [site]\",\n \"One-shot guided onboarding: convert-to-pnpm → onboard → sync-configs → svelte-codemods → a11y-fixtures-page → health-endpoint → smoke-suite → audit.\",\n )\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (site, opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(async () => (await import(\"./commands/init.js\")).runInitCommand(site, opts), opts),\n );\n\ncli\n .command(\n \"launch <site>\",\n \"Bootstrap + first-audit a site, then draft its launch email for approval.\",\n )\n .action(async (site: string, opts: { cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () => (await import(\"./commands/launch.js\")).runLaunchCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"header-image [site]\", \"Generate a site's report header image from its live homepage.\")\n .option(\"--all\", \"Every live site with no Header image yet (backfill)\")\n .option(\"--force\", \"With --all, regenerate sites that already have one\")\n .option(\"--write-airtable\", \"Upload to the Websites row instead of writing a local file\")\n .option(\"--out-dir <path>\", \"Directory for local output (default: reports/)\")\n .option(\"--settle-ms <ms>\", \"Override the post-load settle delay for slow/animated homepages\")\n .action(\n async (\n site: string | undefined,\n opts: {\n all?: boolean;\n force?: boolean;\n writeAirtable?: boolean;\n outDir?: string;\n settleMs?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/header-image.js\")).runHeaderImageCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"announce [site]\",\n \"Draft the monthly-report announcement email for maintenance sites (all, or one) for approval.\",\n )\n .action(async (site: string | undefined, opts: { cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () => (await import(\"./commands/announce.js\")).runAnnounceCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"selftest <kind> [site]\",\n \"Operator self-tests. kind=email: preview a report email for a site (or --all) to yourself.\",\n )\n .option(\"--type <type>\", \"Report type: announcement (default) | maintenance | testing | launch\")\n .option(\"--to <addr>\", \"Recipient(s), comma-separated. Default: OPERATOR_EMAIL.\")\n .option(\"--all\", \"Send a preview for every maintenance site (to --to/operator).\")\n .option(\"--dry-run\", \"Render only; write reports/<slug>/selftest-<type>.html; do not send.\")\n .action(\n async (\n kind: string,\n site: string | undefined,\n opts: {\n type?: string;\n to?: string;\n all?: boolean;\n dryRun?: boolean;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/selftest.js\")).runSelftestCommand(kind, site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"preflight [site]\",\n \"Read-only pre-send checks: recipients, header image, pending drafts, schedule hygiene.\",\n )\n .option(\"--type <type>\", \"Report type: announcement (default) | maintenance | testing\")\n .option(\n \"--all\",\n \"Fleet mode: announcement→maintenance sites; maintenance/testing→all schedulable sites.\",\n )\n .action(\n async (\n site: string | undefined,\n opts: { type?: string; all?: boolean; cwd?: string; verbose?: boolean },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/preflight.js\")).runPreflightCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"ensure-site <slug>\", \"Create/verify the Airtable Websites row for a new site.\")\n .option(\"--name <display name>\", \"Human Name for the row (client-facing copy uses it verbatim).\")\n .option(\"--url <url>\", \"Deployed URL (e.g. the Netlify site URL).\")\n .option(\"--contact <email>\", \"point of contact — the client address reports resolve to.\")\n .option(\"--git-repo <owner/repo>\", \"GitHub identity. Default on create: reddoorla/<slug>.\")\n .action(\n async (\n slug: string,\n opts: {\n name?: string;\n url?: string;\n contact?: string;\n gitRepo?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/ensure-site.js\")).runEnsureSiteCommand(slug, opts),\n opts,\n ),\n );\n\ncli\n .command(\"report [site]\", \"Draft or send maintenance/testing reports.\")\n .option(\"--due\", \"Scan all Websites and draft overdue reports.\")\n .option(\"--type <type>\", \"Single-site draft report type: Maintenance (default) or Testing.\")\n .option(\n \"--preview\",\n \"Single-site dry run; writes reports/<slug>/draft.html, never touches Airtable.\",\n )\n .option(\n \"--send-ready\",\n \"Send all Reports with Draft ready=true AND Approved to send=true AND Sent at IS NULL.\",\n )\n .option(\n \"--digest\",\n \"Email the operator one daily digest of reports ready for approval (skips when empty).\",\n )\n .action(\n async (\n site,\n opts: {\n due?: boolean;\n type?: string;\n preview?: boolean;\n sendReady?: boolean;\n digest?: boolean;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/report.js\")).runReportCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"github-signals\",\n \"Sweep the fleet for GitHub signals (Renovate-failing/CI/last-commit) and write Airtable.\",\n )\n .option(\"--fleet\", \"Run across every site in the Airtable inventory.\")\n .option(\"--write-airtable\", \"Write each site's signals back to its Websites row.\")\n .action(\n async (opts: { fleet?: boolean; writeAirtable?: boolean; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/github-signals.js\")).runGitHubSignalsCommand({\n fleet: opts.fleet,\n writeAirtable: opts.writeAirtable,\n }),\n opts,\n ),\n );\n\ncli\n .command(\n \"protection-audit\",\n \"Verify every public org repo carries a sound branch ruleset (empty bypass, PR-required).\",\n )\n .option(\"--org <org>\", \"GitHub org to sweep (enumerated from the API, never a hand list).\")\n .action(async (opts: { org?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/protection-audit.js\")).runProtectionAuditCommand({\n org: opts.org,\n }),\n opts,\n ),\n );\n\ncli\n .command(\n \"db <action>\",\n \"Migrate / backfill / reconcile the libSQL store (migrate | backfill | reconcile).\",\n )\n .action(async (action: string, opts: { cwd?: string; verbose?: boolean }) =>\n runOrExit(async () => (await import(\"./commands/db.js\")).runDbCommand(action, opts), opts),\n );\n\ncli\n .command(\n \"submissions <action>\",\n \"Operate on stored form submissions. rescore: re-run the CURRENT spam classifier over the status='new' backlog (dry-run by default; only 'new' rows are ever touched).\",\n )\n .option(\"--dry-run\", \"Print the would-be re-buckets without writing (the default).\")\n .option(\n \"--apply\",\n \"Write the re-buckets: status → spam_auto, spam_score/spam_reason replaced with the new verdict + 'retro-rescore' marker.\",\n )\n .action(\n async (\n action: string,\n opts: { dryRun?: boolean; apply?: boolean; cwd?: string; verbose?: boolean },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/submissions.js\")).runSubmissionsCommand(action, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"renovate-dispatch\",\n \"Trigger Renovate on fleet sites the security sweep flagged with critical/high vulns.\",\n )\n .option(\"--fleet\", \"Run across every active, repo-backed site in the Airtable inventory.\")\n .action(async (opts: { fleet?: boolean; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/renovate-dispatch.js\")).runRenovateDispatchCommand({\n fleet: opts.fleet,\n }),\n opts,\n ),\n );\n\ncli\n .command(\n \"blux <action> [dir]\",\n \"Blux conversion pipeline. emit: export dir → migration plan + custom types + theme + review manifest. migrate: emitted plan → live Prismic (needs PRISMIC_REPOSITORY_NAME + PRISMIC_WRITE_TOKEN). validate: content coverage of a rendered site against the export answer key. grid: parse rendered index.html → grid-tree.json (layout tree). convert: parse+classify index.html + assemble IR → migration-plan.json + blux-presentation.json + theme.css (+ map-config.json), offline. catalog: parse each page + classify every band to a blux_section → migration-plan.json only (no sidecar), offline.\",\n )\n .option(\n \"--out <dir>\",\n \"Output directory for emit/grid/convert/catalog (default: <exportDir>/blux-out)\",\n )\n .option(\"--converted-base <url>\", \"Converted-site base URL for the review manifest\")\n .option(\"--blux-base <url>\", \"Original Blux site base URL (default: https://<site.json domain>)\")\n .option(\n \"--probe\",\n \"Reconstruct + HEAD-probe CDN URLs for used assets the HTML scrape missed (network)\",\n )\n .option(\n \"--against <target>\",\n \"validate: rendered HTML of the converted site — a file path or http(s) URL\",\n )\n .option(\n \"--site <slug>\",\n \"freeze/migrate-frozen: site slug naming <slug>.slots.json (default: slugified dir name)\",\n )\n .option(\n \"--extra-slots <path>\",\n \"freeze: JSON declaring slots the template has no token for (video poster, a panel rebuilt from a baked image). Keys must start with 'x.'\",\n )\n .action(\n async (\n action: string,\n dir: string | undefined,\n opts: {\n out?: string;\n convertedBase?: string;\n bluxBase?: string;\n probe?: boolean;\n against?: string;\n site?: string;\n extraSlots?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/blux.js\")).runBluxCommand(action, dir, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"webflow <action> [target]\",\n \"Webflow import pipeline. capture: live-crawl a base url → <out>/ir.json, progress on stderr. docs: convert a saved ir.json → docs.json + assets.json (offline). migrate: push a capture dir's docs.json + assets.json to Prismic via the shared runner (needs PRISMIC_REPOSITORY_NAME + PRISMIC_WRITE_TOKEN).\",\n )\n .option(\n \"--out <dir>\",\n \"Output directory for capture (default: webflow-out) / docs (default: dirname of the ir.json)\",\n )\n .action(\n async (\n action: string,\n target: string | undefined,\n opts: { out?: string; cwd?: string; verbose?: boolean },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/webflow.js\")).runWebflowCommand(action, target, opts),\n opts,\n ),\n );\n\ncli.help();\ncli.version(version);\n\n// A typo'd / unrecognized subcommand (e.g. `reddoor-maint auditt`) otherwise\n// falls through cac with no matched command and exits 0 — a cron/CI typo would\n// \"succeed\" silently. cac emits `command:*` at the end of parse() exactly when\n// a positional arg was given but matched no command (a bare `reddoor-maint`,\n// `--help`, and `--version` do NOT trigger it: they have no leading positional\n// or are handled before this fires). Turn that into a clear stderr error +\n// non-zero exit. process.argv[2] is the first positional, i.e. the bad command.\ncli.on(\"command:*\", () => {\n const unknown = cli.args[0] ?? process.argv[2] ?? \"\";\n console.error(\n `error: unknown command '${unknown}'. Run 'reddoor-maint --help' to see available commands.`,\n );\n process.exit(1);\n});\n\ncli.parse();\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Read the @reddoorla/maintenance package version, given the directory of a\n * file that lives inside the package. Walks UP looking for the first\n * `package.json` whose `name` matches. Defensive against bundling-layout\n * changes — the older \"two levels up\" assumption held for `dist/cli/bin.js`\n * but would silently mis-read (or return \"unknown\") if tsup ever moved bin\n * to a different depth. Same walk-up pattern as the self-version helper.\n *\n * Returns \"unknown\" when no matching package.json is reachable (Yarn PnP\n * setups stash manifests inside .zip caches; the readFileSync there fails\n * before any name check).\n */\nexport function resolvePackageVersion(fromDir: string): string {\n try {\n let dir = fromDir;\n while (true) {\n const candidate = join(dir, \"package.json\");\n if (existsSync(candidate)) {\n const raw = readFileSync(candidate, \"utf-8\");\n const pkg = JSON.parse(raw) as { name?: string; version?: string };\n if (pkg.name === \"@reddoorla/maintenance\") {\n return pkg.version ?? \"unknown\";\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return \"unknown\";\n dir = parent;\n }\n } catch {\n return \"unknown\";\n }\n}\n"],"mappings":"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","names":["dirname","dirname"]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/bin.ts","../../src/cli/version.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { cac } from \"cac\";\nimport type { AuditName, RecipeName } from \"../types.js\";\nimport { loadCredentialsIntoEnv } from \"../util/credentials.js\";\nimport { resolvePackageVersion } from \"./version.js\";\n\n// Command modules are loaded LAZILY (dynamic `import()` inside each `.action()`),\n// never eagerly at the top. An eager `import { runReportCommand } from\n// \"./commands/report.js\"` would pull EVERY command's transitive dependency chain\n// into the CLI's startup graph — report/announce/launch drag in mjml + resend +\n// @google-analytics/data, `db` drags in the libSQL/kysely stack, etc. Those heavy\n// packages are `devDependencies` (this repo's CLI/functions/audits use them); a\n// consuming fleet site installs @reddoorla/maintenance only for `./forms` +\n// `./configs/*` and runs just `reddoor-maint audit --only a11y` in CI. Lazy\n// loading keeps that path (and `--help`/`--version`) free of the report/db\n// chains, so those packages never land in a consumer's node_modules. The\n// smoke-dist gate asserts bin.js's STATIC import closure stays free of them.\n\n// Load credentials from ~/.config/reddoor-maint/credentials.env before any\n// command runs, so AIRTABLE_PAT/AIRTABLE_BASE_ID/RESEND_API_KEY/etc. are\n// available from any cwd. Shell-exported env vars still win. Silent on\n// missing file — commands that need the credentials will fail with their\n// own clear error.\nloadCredentialsIntoEnv();\n\nconst here = dirname(fileURLToPath(import.meta.url));\nconst version = resolvePackageVersion(here);\n\nconst AUDIT_DESCRIPTIONS: Record<AuditName, string> = {\n deps: \"Diff site package.json against the bundled baseline version map.\",\n lighthouse: \"Run @lhci/cli autorun using the canonical lighthouserc.\",\n a11y: \"Playwright + axe against the canonical a11y routes.\",\n security:\n \"GitHub Dependabot alerts (prod+dev) for repo-backed sites; falls back to pnpm/npm audit (prod-deps) with no repo/token.\",\n lint: \"ESLint + Prettier using the canonical configs.\",\n domain: \"DNS resolve + TLS cert expiry against the deployed URL (checkout-free).\",\n browser:\n \"Playwright across desktop engines + mobile devices + link-check against the deployed URL (checkout-free).\",\n \"netlify-deploy\":\n \"Latest production deploy health via the Netlify API by site id (checkout-free; needs NETLIFY_PAT).\",\n \"function-health\":\n \"GET {deployedUrl}/health and read its ok/prismic/forms status (checkout-free).\",\n smoke: \"Run the site's own `pnpm test:smoke` suite in its checkout (clone-based).\",\n \"form-e2e\":\n \"Submit the real production contact form in test-mode against the deployed URL (checkout-free).\",\n};\n\nconst RECIPE_DESCRIPTIONS: Record<RecipeName, string> = {\n \"sync-configs\": \"Overwrite a site's canonical configs to match @reddoorla/maintenance.\",\n \"bump-deps\": \"Bump dependencies and commit the lockfile change.\",\n \"svelte-4-to-5\": \"Run the 7-commit Svelte 4 → 5 upgrade recipe.\",\n \"svelte-codemods\":\n \"Apply Svelte 5 gotcha codemods to an already-migrated site (state_referenced_locally, etc.).\",\n \"convert-to-pnpm\": \"Convert an npm/yarn site to pnpm (lockfile, packageManager, scripts).\",\n onboard: \"Install @reddoorla/maintenance + audit deps on a site (preferred first step).\",\n \"a11y-fixtures-page\":\n \"Write src/routes/dev/a11y-fixtures/+page.svelte (stub for lhci + axe targets).\",\n \"health-endpoint\":\n \"Write src/routes/health/+server.ts (function-health probe for the report gate).\",\n \"smoke-suite\": \"Add the smoke suite (test:smoke + playwright config + /health smoke routes).\",\n \"self-updating\":\n \"Bootstrap Renovate + repo protection (writes renovate configs via PR, wires ruleset, disables platform auto-merge; ci.yml comes from the starter and is deliberately not managed).\",\n init: \"Run the full onboarding chain (convert-to-pnpm → onboard → sync-configs → svelte-codemods → a11y-fixtures-page → health-endpoint → smoke-suite → audit).\",\n};\n\n/** Run a command thunk and surface its result, falling back to a clean error\n * message on throw. Wraps the ~10-line try/catch every `.action()` used to\n * duplicate. `verbose` flips between full stack and message-only.\n *\n * On success it sets `process.exitCode` and RETURNS rather than calling\n * `process.exit()` right after `console.log()`. `process.exit()` does not wait\n * for stdout to flush when stdout is a pipe, so a large `--json` payload piped\n * to another process would get truncated mid-write. Setting `exitCode` and\n * returning lets Node drain stdout and exit naturally with the right code. A\n * non-zero `code` still yields a non-zero process exit.\n *\n * The error path keeps `process.exit()` — error messages are small (one line),\n * always go to stderr, and exiting immediately is the desired fail-fast. */\nexport async function runOrExit(\n fn: () => Promise<{ output: string; code: number }>,\n opts: { verbose?: boolean },\n): Promise<void> {\n try {\n const { output, code } = await fn();\n console.log(output);\n process.exitCode = code;\n return;\n } catch (err) {\n const e = err as { exitCode?: number; message?: string; stack?: string };\n console.error(opts.verbose ? (e.stack ?? e.message) : (e.message ?? String(err)));\n process.exit(e.exitCode ?? 1);\n }\n}\n\nconst cli = cac(\"reddoor-maint\");\n\ncli.option(\"--cwd <path>\", \"Override working directory (default: process.cwd())\");\ncli.option(\"--verbose\", \"Verbose output (full stack on errors)\");\n\ncli.command(\"list-audits\", \"Print the available audits.\").action(() => {\n for (const [name, desc] of Object.entries(AUDIT_DESCRIPTIONS)) {\n console.log(`${name.padEnd(12)} ${desc}`);\n }\n});\n\ncli.command(\"list-recipes\", \"Print the available recipes.\").action(() => {\n for (const [name, desc] of Object.entries(RECIPE_DESCRIPTIONS)) {\n console.log(`${name.padEnd(16)} ${desc}`);\n }\n});\n\ncli\n .command(\"audit [site]\", \"Run audits against a site (default: cwd).\")\n .option(\"--only <names>\", \"Comma-separated audit names (e.g. deps,lighthouse)\")\n .option(\"--json\", \"Machine-readable JSON output\")\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .option(\n \"--write-airtable [slug]\",\n \"After lighthouse runs, write pScore/rScore/bpScore/seoScore + timestamp to the matching Websites row. Slug defaults to cwd's package.json#name.\",\n )\n .option(\"--fail-on-violations\", \"Exit non-zero if any a11y violations are found (for CI gates)\")\n .option(\n \"--url <url>\",\n \"Audit this deployed URL with lighthouse (no dev server); single-site. Pair with --only lighthouse — other audits still use the local checkout.\",\n )\n .option(\n \"--concurrency <n>\",\n \"Max sites to audit in parallel in --fleet mode (default: all at once). Use 1 for sequential (CI).\",\n )\n .action(\n async (\n site,\n opts: {\n only?: string;\n json?: boolean;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n verbose?: boolean;\n writeAirtable?: string | boolean;\n failOnViolations?: boolean;\n url?: string;\n concurrency?: string;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/audit.js\")).runAuditCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"sync-configs [site]\", \"Sync canonical configs into a site.\")\n .option(\"--only <names>\", \"Comma-separated config names (e.g. eslint,prettier)\")\n .option(\"--dry\", \"Print diff without writing\")\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (\n site,\n opts: {\n only?: string;\n dry?: boolean;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/sync-configs.js\")).runSyncConfigsCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"bump-deps [site]\", \"Bump dependencies.\")\n .option(\"--group <group>\", \"patch | minor | major\", { default: \"minor\" })\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (\n site,\n opts: {\n group?: string;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/bump-deps.js\")).runBumpDepsCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"self-updating [site]\",\n \"Bootstrap a repo to keep itself updated (Renovate + protection; platform auto-merge off; ci.yml is the starter's, not managed here).\",\n )\n .option(\"--dry\", \"List what would be enabled without writing or opening PRs\")\n .option(\"--fleet <inventory>\", 'Inventory file (.json or .mjs/.js), or \"airtable\"')\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (\n site,\n opts: {\n dry?: boolean;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () =>\n (await import(\"./commands/self-updating.js\")).runSelfUpdatingCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"upgrade <upgrade> [site]\", \"Run a named upgrade recipe (svelte-4-to-5).\")\n .example(\"reddoor-maint upgrade svelte-4-to-5 ./my-site\")\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (\n upgrade: string,\n site: string | undefined,\n opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/upgrade.js\")).runUpgradeCommand(upgrade, site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"convert-to-pnpm [site]\",\n \"Convert an npm/yarn site to pnpm (lockfile, packageManager, scripts).\",\n )\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (site, opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/convert-to-pnpm.js\")).runConvertToPnpmCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"svelte-codemods [site]\", \"Apply Svelte 5 gotcha codemods to an already-migrated site.\")\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (site, opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/svelte-codemods.js\")).runSvelteCodemodsCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"health-endpoint [site]\",\n \"Write src/routes/health/+server.ts (function-health probe for the report gate).\",\n )\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (site, opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/health-endpoint.js\")).runHealthEndpointCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"smoke-suite [site]\",\n \"Add the smoke suite (test:smoke + playwright config + /health smoke routes).\",\n )\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (site, opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () => (await import(\"./commands/smoke-suite.js\")).runSmokeSuiteCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"onboard [site]\",\n \"Install @reddoorla/maintenance + audit deps on a site (run after convert-to-pnpm).\",\n )\n .option(\"--audits <names>\", \"Comma-separated audit subset: lighthouse,a11y (default: both)\")\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (\n site,\n opts: {\n audits?: string;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/onboard.js\")).runOnboardCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"init [site]\",\n \"One-shot guided onboarding: convert-to-pnpm → onboard → sync-configs → svelte-codemods → a11y-fixtures-page → health-endpoint → smoke-suite → audit.\",\n )\n .option(\n \"--fleet <inventory>\",\n 'Inventory file (.json or .mjs/.js), or \"airtable\" to read from Websites table',\n )\n .option(\"--workdir <path>\", \"Clone target for fleet mode (default ~/.reddoor-maint/sites)\")\n .action(\n async (site, opts: { fleet?: string; workdir?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(async () => (await import(\"./commands/init.js\")).runInitCommand(site, opts), opts),\n );\n\ncli\n .command(\n \"launch <site>\",\n \"Bootstrap + first-audit a site, then draft its launch email for approval.\",\n )\n .action(async (site: string, opts: { cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () => (await import(\"./commands/launch.js\")).runLaunchCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"header-image [site]\", \"Generate a site's report header image from its live homepage.\")\n .option(\"--all\", \"Every live site with no Header image yet (backfill)\")\n .option(\"--force\", \"With --all, regenerate sites that already have one\")\n .option(\"--write-airtable\", \"Upload to the Websites row instead of writing a local file\")\n .option(\"--out-dir <path>\", \"Directory for local output (default: reports/)\")\n .option(\"--settle-ms <ms>\", \"Override the post-load settle delay for slow/animated homepages\")\n .action(\n async (\n site: string | undefined,\n opts: {\n all?: boolean;\n force?: boolean;\n writeAirtable?: boolean;\n outDir?: string;\n settleMs?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/header-image.js\")).runHeaderImageCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"announce [site]\",\n \"Draft the monthly-report announcement email for maintenance sites (all, or one) for approval.\",\n )\n .action(async (site: string | undefined, opts: { cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () => (await import(\"./commands/announce.js\")).runAnnounceCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"forms-notify-target [site]\",\n \"Show who a form submission would email, and optionally flip the pre-launch guard (read-back confirmed).\",\n )\n .option(\"--set <on|off>\", \"Flip the guard: on = route to operator, off = restore.\")\n .option(\"--restore <status>\", \"Status to restore with --set off. Required; never inferred.\")\n .action(\n async (\n site: string | undefined,\n opts: { set?: string; restore?: string; cwd?: string; verbose?: boolean },\n ) =>\n runOrExit(\n async () =>\n (await import(\"./commands/forms-notify-target.js\")).runFormsNotifyTargetCommand(\n site,\n opts,\n ),\n opts,\n ),\n );\n\ncli\n .command(\n \"selftest <kind> [site]\",\n \"Operator self-tests. kind=email: preview a report email for a site (or --all) to yourself.\",\n )\n .option(\"--type <type>\", \"Report type: announcement (default) | maintenance | testing | launch\")\n .option(\"--to <addr>\", \"Recipient(s), comma-separated. Default: OPERATOR_EMAIL.\")\n .option(\"--all\", \"Send a preview for every maintenance site (to --to/operator).\")\n .option(\"--dry-run\", \"Render only; write reports/<slug>/selftest-<type>.html; do not send.\")\n .action(\n async (\n kind: string,\n site: string | undefined,\n opts: {\n type?: string;\n to?: string;\n all?: boolean;\n dryRun?: boolean;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/selftest.js\")).runSelftestCommand(kind, site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"preflight [site]\",\n \"Read-only pre-send checks: recipients, header image, pending drafts, schedule hygiene.\",\n )\n .option(\"--type <type>\", \"Report type: announcement (default) | maintenance | testing\")\n .option(\n \"--all\",\n \"Fleet mode: announcement→maintenance sites; maintenance/testing→all schedulable sites.\",\n )\n .action(\n async (\n site: string | undefined,\n opts: { type?: string; all?: boolean; cwd?: string; verbose?: boolean },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/preflight.js\")).runPreflightCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\"ensure-site <slug>\", \"Create/verify the Airtable Websites row for a new site.\")\n .option(\"--name <display name>\", \"Human Name for the row (client-facing copy uses it verbatim).\")\n .option(\"--url <url>\", \"Deployed URL (e.g. the Netlify site URL).\")\n .option(\"--contact <email>\", \"point of contact — the client address reports resolve to.\")\n .option(\"--git-repo <owner/repo>\", \"GitHub identity. Default on create: reddoorla/<slug>.\")\n .action(\n async (\n slug: string,\n opts: {\n name?: string;\n url?: string;\n contact?: string;\n gitRepo?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/ensure-site.js\")).runEnsureSiteCommand(slug, opts),\n opts,\n ),\n );\n\ncli\n .command(\"report [site]\", \"Draft or send maintenance/testing reports.\")\n .option(\"--due\", \"Scan all Websites and draft overdue reports.\")\n .option(\"--type <type>\", \"Single-site draft report type: Maintenance (default) or Testing.\")\n .option(\n \"--preview\",\n \"Single-site dry run; writes reports/<slug>/draft.html, never touches Airtable.\",\n )\n .option(\n \"--send-ready\",\n \"Send all Reports with Draft ready=true AND Approved to send=true AND Sent at IS NULL.\",\n )\n .option(\n \"--digest\",\n \"Email the operator one daily digest of reports ready for approval (skips when empty).\",\n )\n .action(\n async (\n site,\n opts: {\n due?: boolean;\n type?: string;\n preview?: boolean;\n sendReady?: boolean;\n digest?: boolean;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/report.js\")).runReportCommand(site, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"github-signals\",\n \"Sweep the fleet for GitHub signals (Renovate-failing/CI/last-commit) and write Airtable.\",\n )\n .option(\"--fleet\", \"Run across every site in the Airtable inventory.\")\n .option(\"--write-airtable\", \"Write each site's signals back to its Websites row.\")\n .action(\n async (opts: { fleet?: boolean; writeAirtable?: boolean; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/github-signals.js\")).runGitHubSignalsCommand({\n fleet: opts.fleet,\n writeAirtable: opts.writeAirtable,\n }),\n opts,\n ),\n );\n\ncli\n .command(\n \"protection-audit\",\n \"Verify every public org repo carries a sound branch ruleset (empty bypass, PR-required).\",\n )\n .option(\"--org <org>\", \"GitHub org to sweep (enumerated from the API, never a hand list).\")\n .action(async (opts: { org?: string; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/protection-audit.js\")).runProtectionAuditCommand({\n org: opts.org,\n }),\n opts,\n ),\n );\n\ncli\n .command(\n \"db <action>\",\n \"Migrate / backfill / reconcile the libSQL store (migrate | backfill | reconcile).\",\n )\n .action(async (action: string, opts: { cwd?: string; verbose?: boolean }) =>\n runOrExit(async () => (await import(\"./commands/db.js\")).runDbCommand(action, opts), opts),\n );\n\ncli\n .command(\n \"submissions <action>\",\n \"Operate on stored form submissions. rescore: re-run the CURRENT spam classifier over the status='new' backlog (dry-run by default; only 'new' rows are ever touched).\",\n )\n .option(\"--dry-run\", \"Print the would-be re-buckets without writing (the default).\")\n .option(\n \"--apply\",\n \"Write the re-buckets: status → spam_auto, spam_score/spam_reason replaced with the new verdict + 'retro-rescore' marker.\",\n )\n .action(\n async (\n action: string,\n opts: { dryRun?: boolean; apply?: boolean; cwd?: string; verbose?: boolean },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/submissions.js\")).runSubmissionsCommand(action, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"renovate-dispatch\",\n \"Trigger Renovate on fleet sites the security sweep flagged with critical/high vulns.\",\n )\n .option(\"--fleet\", \"Run across every active, repo-backed site in the Airtable inventory.\")\n .action(async (opts: { fleet?: boolean; cwd?: string; verbose?: boolean }) =>\n runOrExit(\n async () =>\n (await import(\"./commands/renovate-dispatch.js\")).runRenovateDispatchCommand({\n fleet: opts.fleet,\n }),\n opts,\n ),\n );\n\ncli\n .command(\n \"blux <action> [dir]\",\n \"Blux conversion pipeline. emit: export dir → migration plan + custom types + theme + review manifest. migrate: emitted plan → live Prismic (needs PRISMIC_REPOSITORY_NAME + PRISMIC_WRITE_TOKEN). validate: content coverage of a rendered site against the export answer key. grid: parse rendered index.html → grid-tree.json (layout tree). convert: parse+classify index.html + assemble IR → migration-plan.json + blux-presentation.json + theme.css (+ map-config.json), offline. catalog: parse each page + classify every band to a blux_section → migration-plan.json only (no sidecar), offline.\",\n )\n .option(\n \"--out <dir>\",\n \"Output directory for emit/grid/convert/catalog (default: <exportDir>/blux-out)\",\n )\n .option(\"--converted-base <url>\", \"Converted-site base URL for the review manifest\")\n .option(\"--blux-base <url>\", \"Original Blux site base URL (default: https://<site.json domain>)\")\n .option(\n \"--probe\",\n \"Reconstruct + HEAD-probe CDN URLs for used assets the HTML scrape missed (network)\",\n )\n .option(\n \"--against <target>\",\n \"validate: rendered HTML of the converted site — a file path or http(s) URL\",\n )\n .option(\n \"--site <slug>\",\n \"freeze/migrate-frozen: site slug naming <slug>.slots.json (default: slugified dir name)\",\n )\n .option(\n \"--extra-slots <path>\",\n \"freeze: JSON declaring slots the template has no token for (video poster, a panel rebuilt from a baked image). Keys must start with 'x.'\",\n )\n .action(\n async (\n action: string,\n dir: string | undefined,\n opts: {\n out?: string;\n convertedBase?: string;\n bluxBase?: string;\n probe?: boolean;\n against?: string;\n site?: string;\n extraSlots?: string;\n cwd?: string;\n verbose?: boolean;\n },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/blux.js\")).runBluxCommand(action, dir, opts),\n opts,\n ),\n );\n\ncli\n .command(\n \"webflow <action> [target]\",\n \"Webflow import pipeline. capture: live-crawl a base url → <out>/ir.json, progress on stderr. docs: convert a saved ir.json → docs.json + assets.json (offline). migrate: push a capture dir's docs.json + assets.json to Prismic via the shared runner (needs PRISMIC_REPOSITORY_NAME + PRISMIC_WRITE_TOKEN).\",\n )\n .option(\n \"--out <dir>\",\n \"Output directory for capture (default: webflow-out) / docs (default: dirname of the ir.json)\",\n )\n .action(\n async (\n action: string,\n target: string | undefined,\n opts: { out?: string; cwd?: string; verbose?: boolean },\n ) =>\n runOrExit(\n async () => (await import(\"./commands/webflow.js\")).runWebflowCommand(action, target, opts),\n opts,\n ),\n );\n\ncli.help();\ncli.version(version);\n\n// A typo'd / unrecognized subcommand (e.g. `reddoor-maint auditt`) otherwise\n// falls through cac with no matched command and exits 0 — a cron/CI typo would\n// \"succeed\" silently. cac emits `command:*` at the end of parse() exactly when\n// a positional arg was given but matched no command (a bare `reddoor-maint`,\n// `--help`, and `--version` do NOT trigger it: they have no leading positional\n// or are handled before this fires). Turn that into a clear stderr error +\n// non-zero exit. process.argv[2] is the first positional, i.e. the bad command.\ncli.on(\"command:*\", () => {\n const unknown = cli.args[0] ?? process.argv[2] ?? \"\";\n console.error(\n `error: unknown command '${unknown}'. Run 'reddoor-maint --help' to see available commands.`,\n );\n process.exit(1);\n});\n\ncli.parse();\n","import { readFileSync, existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/**\n * Read the @reddoorla/maintenance package version, given the directory of a\n * file that lives inside the package. Walks UP looking for the first\n * `package.json` whose `name` matches. Defensive against bundling-layout\n * changes — the older \"two levels up\" assumption held for `dist/cli/bin.js`\n * but would silently mis-read (or return \"unknown\") if tsup ever moved bin\n * to a different depth. Same walk-up pattern as the self-version helper.\n *\n * Returns \"unknown\" when no matching package.json is reachable (Yarn PnP\n * setups stash manifests inside .zip caches; the readFileSync there fails\n * before any name check).\n */\nexport function resolvePackageVersion(fromDir: string): string {\n try {\n let dir = fromDir;\n while (true) {\n const candidate = join(dir, \"package.json\");\n if (existsSync(candidate)) {\n const raw = readFileSync(candidate, \"utf-8\");\n const pkg = JSON.parse(raw) as { name?: string; version?: string };\n if (pkg.name === \"@reddoorla/maintenance\") {\n return pkg.version ?? \"unknown\";\n }\n }\n const parent = dirname(dir);\n if (parent === dir) return \"unknown\";\n dir = parent;\n }\n } catch {\n return \"unknown\";\n }\n}\n"],"mappings":"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","names":["dirname","dirname"]}
|
|
@@ -12,9 +12,9 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
ALL_AUDIT_NAMES,
|
|
14
14
|
runOneAudit
|
|
15
|
-
} from "../../chunk-
|
|
15
|
+
} from "../../chunk-JMAFXEQE.js";
|
|
16
16
|
import "../../chunk-GASBX52O.js";
|
|
17
|
-
import "../../chunk-
|
|
17
|
+
import "../../chunk-YAMRVXOD.js";
|
|
18
18
|
import "../../chunk-LKGVSM2O.js";
|
|
19
19
|
import {
|
|
20
20
|
recordFleetEventsBestEffort
|
|
@@ -23,14 +23,16 @@ import "../../chunk-DC5H7KEQ.js";
|
|
|
23
23
|
import "../../chunk-4SOCPK7A.js";
|
|
24
24
|
import "../../chunk-FDIFTIXC.js";
|
|
25
25
|
import "../../chunk-HVOOCK6L.js";
|
|
26
|
+
import "../../chunk-A3WDWNN5.js";
|
|
27
|
+
import "../../chunk-2OP4JKIQ.js";
|
|
26
28
|
import "../../chunk-XXTZBPUY.js";
|
|
27
29
|
import "../../chunk-XTK5VIQB.js";
|
|
28
30
|
import {
|
|
29
31
|
isHttpUrl
|
|
30
32
|
} from "../../chunk-U5SZ4FOP.js";
|
|
31
33
|
import "../../chunk-6VAL7XJT.js";
|
|
32
|
-
import "../../chunk-
|
|
33
|
-
import "../../chunk-
|
|
34
|
+
import "../../chunk-NMAWXBJA.js";
|
|
35
|
+
import "../../chunk-N4MOEF32.js";
|
|
34
36
|
|
|
35
37
|
// src/cli/commands/audit.ts
|
|
36
38
|
import { resolve } from "path";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/cli/commands/audit.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { Listr } from \"listr2\";\nimport { runOneAudit, ALL_AUDIT_NAMES } from \"../../audits/index.js\";\nimport type { AuditName, AuditResult, Site } from \"../../types.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport {\n prepareFleetSites,\n formatSkippedNotice,\n type SkippedSite,\n} from \"../fleet/prepare-sites.js\";\nimport { isHttpUrl } from \"../../util/url.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\nimport { recordFleetEventsBestEffort } from \"../../audits/fleet-events-writer.js\";\nimport { fleetSweptEvent } from \"../../audits/fleet-event-detectors.js\";\n\nexport type AuditCommandOptions = {\n only?: string;\n json?: boolean;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n /**\n * After running, push the lighthouse scores to the matching Websites row\n * in Airtable. `true` (no value) = derive slug from cwd/package.json#name;\n * string = explicit slug (e.g. \"med-solutions-of-texas\").\n */\n writeAirtable?: string | boolean;\n /** Exit non-zero if any a11y violations are found (overrides warn). For CI gates. */\n failOnViolations?: boolean;\n /** Audit this deployed URL directly (lighthouse only; single-site). */\n url?: string;\n /** Max sites to audit in parallel (fleet mode). Unset = all at once;\n * `1` = sequential (used by the nightly CI workflow). */\n concurrency?: string;\n};\n\nfunction parseOnly(value: string | undefined): AuditName[] | undefined {\n if (!value) return undefined;\n const names = value.split(\",\").map((s) => s.trim());\n for (const n of names) {\n if (!ALL_AUDIT_NAMES.includes(n as AuditName)) {\n throw Object.assign(new Error(`unknown audit in --only: ${n}`), { exitCode: 2 });\n }\n }\n return names as AuditName[];\n}\n\nfunction formatTable(results: AuditResult[]): string {\n return results\n .map((r) => `${r.audit.padEnd(12)} ${r.status.padEnd(5)} ${r.site}\\n ${r.summary}`)\n .join(\"\\n\");\n}\n\nexport function auditExitCode(results: AuditResult[], failOnViolations: boolean): number {\n if (results.some((r) => r.status === \"fail\")) return 1;\n if (failOnViolations) {\n const a11yViolations = results\n .filter((r) => r.audit === \"a11y\")\n .reduce(\n (n, r) =>\n n + ((r.details as { totalViolations?: number } | undefined)?.totalViolations ?? 0),\n 0,\n );\n if (a11yViolations > 0) return 1;\n }\n return 0;\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1_000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1_000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const m = Math.floor(totalSeconds / 60);\n const s = totalSeconds % 60;\n return `${m}m${s.toString().padStart(2, \"0\")}s`;\n}\n\ntype Renderer = \"default\" | \"silent\";\n\n/** Parse the `--concurrency <n>` flag into a Listr `concurrent` value. Unset →\n * `true` (all sites in parallel, the interactive default). A positive integer\n * bounds how many sites audit at once — `--concurrency 1` runs sequentially,\n * which is what the nightly CI workflow uses so ~10 deployed-Lighthouse runs\n * don't saturate a 2-core runner (one flaked locally at full parallelism). */\nexport function parseConcurrency(value: string | undefined): boolean | number {\n if (value === undefined) return true;\n const n = Number(value);\n if (!Number.isInteger(n) || n < 1) {\n throw Object.assign(new Error(`--concurrency must be a positive integer, got \"${value}\"`), {\n exitCode: 2,\n });\n }\n return n;\n}\n\n/** Build the audit-progress task list. Single-site → each audit is a sibling\n * task. Fleet → each site is a task whose `output` shows X/N audits done as\n * they complete (audits-per-site still run in parallel). Results are pushed\n * into the shared `results` array; tasks throw on `fail` status so listr2\n * paints them red, but `exitOnError: false` keeps other tasks running. */\nfunction buildAuditTasks(\n sites: Site[],\n which: AuditName[],\n results: AuditResult[],\n renderer: Renderer,\n concurrency: boolean | number,\n) {\n const singleSite = sites.length === 1;\n\n if (singleSite) {\n const site = sites[0]!;\n return new Listr(\n which.map((name) => ({\n title: name,\n task: async (_ctx, task) => {\n const start = Date.now();\n const result = await runOneAudit(site, name);\n results.push(result);\n const elapsed = formatDuration(Date.now() - start);\n task.title = `${name}: ${result.summary} (${elapsed})`;\n if (result.status === \"fail\") throw new Error(result.summary);\n },\n })),\n { concurrent: true, exitOnError: false, renderer },\n );\n }\n\n return new Listr(\n sites.map((site) => {\n const label = site.name || site.path; // `||`: empty slug must fall back to path, not blank\n return {\n title: label,\n task: async (_ctx, task) => {\n const start = Date.now();\n let done = 0;\n task.output = `0/${which.length} audits`;\n const settled = await Promise.all(\n which.map(async (name) => {\n const r = await runOneAudit(site, name);\n results.push(r);\n done += 1;\n task.output = `${done}/${which.length} audits`;\n return r;\n }),\n );\n const elapsed = formatDuration(Date.now() - start);\n const failed = settled.filter((r) => r.status === \"fail\").length;\n const warned = settled.filter((r) => r.status === \"warn\").length;\n const note =\n failed > 0\n ? `${failed} failed`\n : warned > 0\n ? `${warned} warning${warned === 1 ? \"\" : \"s\"}`\n : \"all green\";\n task.title = `${label}: ${note} (${elapsed})`;\n if (failed > 0) throw new Error(`${label}: ${failed} audit(s) failed`);\n },\n };\n }),\n { concurrent: concurrency, exitOnError: false, renderer },\n );\n}\n\ntype WriteSummary = Awaited<\n ReturnType<typeof import(\"../../audits/write-audits-to-airtable.js\").writeAuditsToAirtable>\n>;\n\nfunction formatWriteSummary(summary: WriteSummary): string {\n const lines = summary.writes.map((w) => {\n if (w.audit === \"lighthouse\") {\n const s = w.counts as {\n performance: number;\n accessibility: number;\n bestPractices: number;\n seo: number;\n };\n return ` lighthouse: P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo}`;\n }\n if (w.audit === \"a11y\") {\n return ` a11y: ${(w.counts as { violations: number }).violations} violations`;\n }\n if (w.audit === \"deps\") {\n const c = w.counts as { drifted: number; majorBehind: number };\n return ` deps: ${c.drifted} drifted (${c.majorBehind} major)`;\n }\n const c = w.counts as { critical: number; high: number; moderate: number; low: number };\n return ` security: ${c.critical}C/${c.high}H/${c.moderate}M/${c.low}L`;\n });\n return `→ wrote to Websites[${summary.siteName}]:\\n${lines.join(\"\\n\")}`;\n}\n\n/** Listr renderer choice. `--json` → silent so stdout stays clean for piping.\n * Otherwise listr's `default` renderer auto-falls back to `simple` in\n * non-TTY contexts (CI, log capture, our own integration tests). */\nfunction rendererFor(json: boolean | undefined): Renderer {\n return json ? \"silent\" : \"default\";\n}\n\n/** When `--url` is set but audits other than lighthouse also ran (those use the\n * local checkout, not the deployed URL), return a one-line operator notice;\n * null when there's nothing to warn about. Keeps the mixed-provenance result\n * table from silently confusing the operator. */\nexport function deployedUrlNotice(\n which: AuditName[],\n url: string | undefined,\n cwd: string,\n): string | null {\n if (url === undefined) return null;\n const others = which.filter((n) => n !== \"lighthouse\");\n if (others.length === 0) return null;\n return `note: --url only affects lighthouse; ${others.join(\", \")} ran against the local checkout at ${cwd}`;\n}\n\n/** Audits that need no repo checkout — they probe an external resource. lighthouse hits the\n * live URL; domain probes DNS/TLS of the URL's host; browser drives the deployed URL;\n * netlify-deploy queries the Netlify API by site id; function-health fetches the deployed\n * `/health` endpoint. */\nconst CHECKOUT_FREE_AUDITS: ReadonlySet<AuditName> = new Set<AuditName>([\n \"lighthouse\",\n \"domain\",\n \"browser\",\n \"netlify-deploy\",\n \"function-health\",\n \"form-e2e\",\n]);\n\n/** Audits whose checkout-free run is keyed off `site.netlifyId` (the Netlify API) rather than\n * the deployed URL. Everything else in CHECKOUT_FREE_AUDITS reads `site.deployedUrl`. */\nconst NETLIFY_ID_AUDITS: ReadonlySet<AuditName> = new Set<AuditName>([\"netlify-deploy\"]);\n\n/** A fleet site needs a local checkout unless every requested audit is checkout-free AND the site\n * has the external resource each one runs against (`deployedUrl` for lighthouse/domain/browser,\n * `netlifyId` for netlify-deploy). */\nexport function auditNeedsCheckout(site: Site, which: AuditName[]): boolean {\n const deployedCapable = which.every((n) => {\n if (!CHECKOUT_FREE_AUDITS.has(n)) return false;\n return NETLIFY_ID_AUDITS.has(n) ? site.netlifyId !== undefined : site.deployedUrl !== undefined;\n });\n return !deployedCapable;\n}\n\n/** Apply a single-site `--url` to the resolved sites. Returns the input\n * untouched when no url is given; otherwise requires exactly one site and\n * stamps `deployedUrl` on it so the lighthouse audit takes its deployed path.\n * The `--url`+`--fleet` combination is rejected earlier in `runAuditCommand`;\n * this length guard also covers any future multi-site single-run resolver. */\nexport function applyDeployedUrl(sites: Site[], url: string | undefined): Site[] {\n if (url === undefined) return sites;\n if (sites.length !== 1) {\n throw Object.assign(\n new Error(`--url expects exactly one site, but ${sites.length} resolved.`),\n { exitCode: 2 },\n );\n }\n // Scheme-allowlist: the URL is handed straight to Chrome/lhci, so only\n // http(s) is safe (a file:///gopher:// value would be a local-file read /\n // SSRF). Same predicate the inventory paths use.\n if (!isHttpUrl(url)) {\n throw Object.assign(new Error(`--url must be an http(s) URL (got: ${JSON.stringify(url)})`), {\n exitCode: 2,\n });\n }\n return [{ ...sites[0]!, deployedUrl: url }];\n}\n\nexport async function runAuditCommand(\n site: string | undefined,\n opts: AuditCommandOptions,\n): Promise<{ output: string; code: number }> {\n const which = parseOnly(opts.only) ?? ALL_AUDIT_NAMES;\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n\n // A literal --write-airtable=<slug> is single-site (the slug names one row).\n // Boolean --write-airtable + --fleet is fine: each site's slug comes from the\n // inventory, so there's no cwd-derived-slug ambiguity.\n if (typeof opts.writeAirtable === \"string\" && opts.fleet !== undefined) {\n throw Object.assign(\n new Error(\n \"--write-airtable=<slug> is single-site; with --fleet each site's slug comes from the inventory. Use --write-airtable (no slug) + --fleet.\",\n ),\n { exitCode: 2 },\n );\n }\n\n if (opts.url !== undefined && opts.fleet !== undefined) {\n throw Object.assign(\n new Error(\n \"--url is single-site only and cannot be combined with --fleet. Audit a single site instead.\",\n ),\n { exitCode: 2 },\n );\n }\n\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n cwd,\n });\n\n sites = applyDeployedUrl(sites, opts.url);\n\n let skippedPrep: SkippedSite[] = [];\n if (opts.fleet) {\n const workdir = opts.workdir ?? fleetWorkdir();\n const prep = await prepareFleetSites(sites, {\n workdir,\n needsCheckout: (s) => auditNeedsCheckout(s, which),\n });\n sites = prep.prepared;\n skippedPrep = prep.skipped;\n }\n\n const results: AuditResult[] = [];\n const renderer = rendererFor(opts.json);\n await buildAuditTasks(sites, which, results, renderer, parseConcurrency(opts.concurrency)).run();\n\n let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);\n\n // Surface any site that couldn't be prepared (no auditable target, clone\n // failure) — visibly, but without reding the run. One misconfigured inventory\n // row is an operator fix, not an outage; the other sites still audited and\n // wrote back. \"No silent caps\": a dropped site is never invisible.\n const skipNotice = formatSkippedNotice(skippedPrep);\n if (skipNotice && !opts.json) output += `\\n\\n${skipNotice}`;\n\n // Did any site fail to write back to Airtable? The fleet writer collects\n // per-site failures instead of throwing, so without this the command would\n // exit 0 while rows silently failed to persist — automation keying on `$?`\n // would see a clean run. (The single-site writer throws on failure, so it's\n // already non-zero via the propagated error.)\n let writeBackFailed = false;\n if (opts.writeAirtable !== undefined) {\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const { listWebsites } = await import(\"../../reports/airtable/websites.js\");\n\n if (opts.fleet !== undefined) {\n const { writeFleetAuditsToAirtable, formatFleetWriteSummary } =\n await import(\"../../audits/write-audits-to-airtable.js\");\n const base = openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const fleetWrite = await writeFleetAuditsToAirtable({ base, websites, results });\n if (fleetWrite.failed.length > 0) writeBackFailed = true;\n // Record fleet-activity events (vuln_cleared / cert_renewed rode along on each\n // WriteSummary) plus a per-sweep rollup. Best-effort: a missing Turso cred no-ops.\n const sweep = which.includes(\"security\") ? \"security\" : \"lighthouse\";\n const now = new Date();\n const auditEvents = fleetWrite.written.flatMap((w) => w.events ?? []);\n await recordFleetEventsBestEffort(\n [...auditEvents, fleetSweptEvent(sweep, fleetWrite.written.length, now.toISOString())],\n now,\n );\n // Gate on !json: the write-summary is human text; appending it after the\n // results array would corrupt `--json` output (the other notices already\n // guard this way). The write itself still happens regardless of --json.\n if (!opts.json) output += `\\n\\n${formatFleetWriteSummary(fleetWrite)}`;\n } else {\n const { resolveSlugFromCwd } = await import(\"../../audits/lighthouse-airtable.js\");\n const { writeAuditsToAirtable } = await import(\"../../audits/write-audits-to-airtable.js\");\n const slug =\n typeof opts.writeAirtable === \"string\" && opts.writeAirtable.length > 0\n ? opts.writeAirtable\n : await resolveSlugFromCwd(cwd);\n let writeSummary: WriteSummary | null = null;\n await new Listr(\n [\n {\n title: `Write to Airtable[${slug}]`,\n task: async (_ctx, task) => {\n const base = openBase(readAirtableConfig());\n task.output = \"loading Websites…\";\n const websites = await listWebsites(base);\n task.output = \"writing scores…\";\n writeSummary = await writeAuditsToAirtable({ base, websites, slug, results });\n task.title = `Wrote to Websites[${writeSummary.siteName}] (${writeSummary.writes.length} audit type${writeSummary.writes.length === 1 ? \"\" : \"s\"})`;\n },\n },\n ],\n { renderer },\n ).run();\n if (writeSummary && !opts.json) output += `\\n\\n${formatWriteSummary(writeSummary)}`;\n }\n }\n\n const notice = deployedUrlNotice(which, opts.url, cwd);\n if (notice && !opts.json) output += `\\n\\n${notice}`;\n\n const code = Math.max(\n auditExitCode(results, opts.failOnViolations === true),\n writeBackFailed ? 1 : 0,\n );\n return { output, code };\n}\n"],"mappings":"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","names":["c"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/cli/commands/audit.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { Listr } from \"listr2\";\nimport { runOneAudit, ALL_AUDIT_NAMES } from \"../../audits/index.js\";\nimport type { AuditName, AuditResult, Site } from \"../../types.js\";\nimport { resolveSites } from \"../fleet/resolve-sites.js\";\nimport {\n prepareFleetSites,\n formatSkippedNotice,\n type SkippedSite,\n} from \"../fleet/prepare-sites.js\";\nimport { isHttpUrl } from \"../../util/url.js\";\nimport { fleetWorkdir } from \"../../util/fleet-workdir.js\";\nimport { recordFleetEventsBestEffort } from \"../../audits/fleet-events-writer.js\";\nimport { fleetSweptEvent } from \"../../audits/fleet-event-detectors.js\";\n\nexport type AuditCommandOptions = {\n only?: string;\n json?: boolean;\n fleet?: string;\n workdir?: string;\n cwd?: string;\n /**\n * After running, push the lighthouse scores to the matching Websites row\n * in Airtable. `true` (no value) = derive slug from cwd/package.json#name;\n * string = explicit slug (e.g. \"med-solutions-of-texas\").\n */\n writeAirtable?: string | boolean;\n /** Exit non-zero if any a11y violations are found (overrides warn). For CI gates. */\n failOnViolations?: boolean;\n /** Audit this deployed URL directly (lighthouse only; single-site). */\n url?: string;\n /** Max sites to audit in parallel (fleet mode). Unset = all at once;\n * `1` = sequential (used by the nightly CI workflow). */\n concurrency?: string;\n};\n\nfunction parseOnly(value: string | undefined): AuditName[] | undefined {\n if (!value) return undefined;\n const names = value.split(\",\").map((s) => s.trim());\n for (const n of names) {\n if (!ALL_AUDIT_NAMES.includes(n as AuditName)) {\n throw Object.assign(new Error(`unknown audit in --only: ${n}`), { exitCode: 2 });\n }\n }\n return names as AuditName[];\n}\n\nfunction formatTable(results: AuditResult[]): string {\n return results\n .map((r) => `${r.audit.padEnd(12)} ${r.status.padEnd(5)} ${r.site}\\n ${r.summary}`)\n .join(\"\\n\");\n}\n\nexport function auditExitCode(results: AuditResult[], failOnViolations: boolean): number {\n if (results.some((r) => r.status === \"fail\")) return 1;\n if (failOnViolations) {\n const a11yViolations = results\n .filter((r) => r.audit === \"a11y\")\n .reduce(\n (n, r) =>\n n + ((r.details as { totalViolations?: number } | undefined)?.totalViolations ?? 0),\n 0,\n );\n if (a11yViolations > 0) return 1;\n }\n return 0;\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1_000) return `${ms}ms`;\n const totalSeconds = Math.round(ms / 1_000);\n if (totalSeconds < 60) return `${totalSeconds}s`;\n const m = Math.floor(totalSeconds / 60);\n const s = totalSeconds % 60;\n return `${m}m${s.toString().padStart(2, \"0\")}s`;\n}\n\ntype Renderer = \"default\" | \"silent\";\n\n/** Parse the `--concurrency <n>` flag into a Listr `concurrent` value. Unset →\n * `true` (all sites in parallel, the interactive default). A positive integer\n * bounds how many sites audit at once — `--concurrency 1` runs sequentially,\n * which is what the nightly CI workflow uses so ~10 deployed-Lighthouse runs\n * don't saturate a 2-core runner (one flaked locally at full parallelism). */\nexport function parseConcurrency(value: string | undefined): boolean | number {\n if (value === undefined) return true;\n const n = Number(value);\n if (!Number.isInteger(n) || n < 1) {\n throw Object.assign(new Error(`--concurrency must be a positive integer, got \"${value}\"`), {\n exitCode: 2,\n });\n }\n return n;\n}\n\n/** Build the audit-progress task list. Single-site → each audit is a sibling\n * task. Fleet → each site is a task whose `output` shows X/N audits done as\n * they complete (audits-per-site still run in parallel). Results are pushed\n * into the shared `results` array; tasks throw on `fail` status so listr2\n * paints them red, but `exitOnError: false` keeps other tasks running. */\nfunction buildAuditTasks(\n sites: Site[],\n which: AuditName[],\n results: AuditResult[],\n renderer: Renderer,\n concurrency: boolean | number,\n) {\n const singleSite = sites.length === 1;\n\n if (singleSite) {\n const site = sites[0]!;\n return new Listr(\n which.map((name) => ({\n title: name,\n task: async (_ctx, task) => {\n const start = Date.now();\n const result = await runOneAudit(site, name);\n results.push(result);\n const elapsed = formatDuration(Date.now() - start);\n task.title = `${name}: ${result.summary} (${elapsed})`;\n if (result.status === \"fail\") throw new Error(result.summary);\n },\n })),\n { concurrent: true, exitOnError: false, renderer },\n );\n }\n\n return new Listr(\n sites.map((site) => {\n const label = site.name || site.path; // `||`: empty slug must fall back to path, not blank\n return {\n title: label,\n task: async (_ctx, task) => {\n const start = Date.now();\n let done = 0;\n task.output = `0/${which.length} audits`;\n const settled = await Promise.all(\n which.map(async (name) => {\n const r = await runOneAudit(site, name);\n results.push(r);\n done += 1;\n task.output = `${done}/${which.length} audits`;\n return r;\n }),\n );\n const elapsed = formatDuration(Date.now() - start);\n const failed = settled.filter((r) => r.status === \"fail\").length;\n const warned = settled.filter((r) => r.status === \"warn\").length;\n const note =\n failed > 0\n ? `${failed} failed`\n : warned > 0\n ? `${warned} warning${warned === 1 ? \"\" : \"s\"}`\n : \"all green\";\n task.title = `${label}: ${note} (${elapsed})`;\n if (failed > 0) throw new Error(`${label}: ${failed} audit(s) failed`);\n },\n };\n }),\n { concurrent: concurrency, exitOnError: false, renderer },\n );\n}\n\ntype WriteSummary = Awaited<\n ReturnType<typeof import(\"../../audits/write-audits-to-airtable.js\").writeAuditsToAirtable>\n>;\n\nfunction formatWriteSummary(summary: WriteSummary): string {\n const lines = summary.writes.map((w) => {\n if (w.audit === \"lighthouse\") {\n const s = w.counts as {\n performance: number;\n accessibility: number;\n bestPractices: number;\n seo: number;\n };\n return ` lighthouse: P=${s.performance} A=${s.accessibility} BP=${s.bestPractices} SEO=${s.seo}`;\n }\n if (w.audit === \"a11y\") {\n return ` a11y: ${(w.counts as { violations: number }).violations} violations`;\n }\n if (w.audit === \"deps\") {\n const c = w.counts as { drifted: number; majorBehind: number };\n return ` deps: ${c.drifted} drifted (${c.majorBehind} major)`;\n }\n const c = w.counts as { critical: number; high: number; moderate: number; low: number };\n return ` security: ${c.critical}C/${c.high}H/${c.moderate}M/${c.low}L`;\n });\n return `→ wrote to Websites[${summary.siteName}]:\\n${lines.join(\"\\n\")}`;\n}\n\n/** Listr renderer choice. `--json` → silent so stdout stays clean for piping.\n * Otherwise listr's `default` renderer auto-falls back to `simple` in\n * non-TTY contexts (CI, log capture, our own integration tests). */\nfunction rendererFor(json: boolean | undefined): Renderer {\n return json ? \"silent\" : \"default\";\n}\n\n/** When `--url` is set but audits other than lighthouse also ran (those use the\n * local checkout, not the deployed URL), return a one-line operator notice;\n * null when there's nothing to warn about. Keeps the mixed-provenance result\n * table from silently confusing the operator. */\nexport function deployedUrlNotice(\n which: AuditName[],\n url: string | undefined,\n cwd: string,\n): string | null {\n if (url === undefined) return null;\n const others = which.filter((n) => n !== \"lighthouse\");\n if (others.length === 0) return null;\n return `note: --url only affects lighthouse; ${others.join(\", \")} ran against the local checkout at ${cwd}`;\n}\n\n/** Audits that need no repo checkout — they probe an external resource. lighthouse hits the\n * live URL; domain probes DNS/TLS of the URL's host; browser drives the deployed URL;\n * netlify-deploy queries the Netlify API by site id; function-health fetches the deployed\n * `/health` endpoint. */\nconst CHECKOUT_FREE_AUDITS: ReadonlySet<AuditName> = new Set<AuditName>([\n \"lighthouse\",\n \"domain\",\n \"browser\",\n \"netlify-deploy\",\n \"function-health\",\n \"form-e2e\",\n]);\n\n/** Audits whose checkout-free run is keyed off `site.netlifyId` (the Netlify API) rather than\n * the deployed URL. Everything else in CHECKOUT_FREE_AUDITS reads `site.deployedUrl`. */\nconst NETLIFY_ID_AUDITS: ReadonlySet<AuditName> = new Set<AuditName>([\"netlify-deploy\"]);\n\n/** A fleet site needs a local checkout unless every requested audit is checkout-free AND the site\n * has the external resource each one runs against (`deployedUrl` for lighthouse/domain/browser,\n * `netlifyId` for netlify-deploy). */\nexport function auditNeedsCheckout(site: Site, which: AuditName[]): boolean {\n const deployedCapable = which.every((n) => {\n if (!CHECKOUT_FREE_AUDITS.has(n)) return false;\n return NETLIFY_ID_AUDITS.has(n) ? site.netlifyId !== undefined : site.deployedUrl !== undefined;\n });\n return !deployedCapable;\n}\n\n/** Apply a single-site `--url` to the resolved sites. Returns the input\n * untouched when no url is given; otherwise requires exactly one site and\n * stamps `deployedUrl` on it so the lighthouse audit takes its deployed path.\n * The `--url`+`--fleet` combination is rejected earlier in `runAuditCommand`;\n * this length guard also covers any future multi-site single-run resolver. */\nexport function applyDeployedUrl(sites: Site[], url: string | undefined): Site[] {\n if (url === undefined) return sites;\n if (sites.length !== 1) {\n throw Object.assign(\n new Error(`--url expects exactly one site, but ${sites.length} resolved.`),\n { exitCode: 2 },\n );\n }\n // Scheme-allowlist: the URL is handed straight to Chrome/lhci, so only\n // http(s) is safe (a file:///gopher:// value would be a local-file read /\n // SSRF). Same predicate the inventory paths use.\n if (!isHttpUrl(url)) {\n throw Object.assign(new Error(`--url must be an http(s) URL (got: ${JSON.stringify(url)})`), {\n exitCode: 2,\n });\n }\n return [{ ...sites[0]!, deployedUrl: url }];\n}\n\nexport async function runAuditCommand(\n site: string | undefined,\n opts: AuditCommandOptions,\n): Promise<{ output: string; code: number }> {\n const which = parseOnly(opts.only) ?? ALL_AUDIT_NAMES;\n const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd();\n\n // A literal --write-airtable=<slug> is single-site (the slug names one row).\n // Boolean --write-airtable + --fleet is fine: each site's slug comes from the\n // inventory, so there's no cwd-derived-slug ambiguity.\n if (typeof opts.writeAirtable === \"string\" && opts.fleet !== undefined) {\n throw Object.assign(\n new Error(\n \"--write-airtable=<slug> is single-site; with --fleet each site's slug comes from the inventory. Use --write-airtable (no slug) + --fleet.\",\n ),\n { exitCode: 2 },\n );\n }\n\n if (opts.url !== undefined && opts.fleet !== undefined) {\n throw Object.assign(\n new Error(\n \"--url is single-site only and cannot be combined with --fleet. Audit a single site instead.\",\n ),\n { exitCode: 2 },\n );\n }\n\n let sites = await resolveSites({\n ...(site !== undefined ? { site } : {}),\n ...(opts.fleet !== undefined ? { fleet: opts.fleet } : {}),\n ...(opts.workdir !== undefined ? { workdir: opts.workdir } : {}),\n cwd,\n });\n\n sites = applyDeployedUrl(sites, opts.url);\n\n let skippedPrep: SkippedSite[] = [];\n if (opts.fleet) {\n const workdir = opts.workdir ?? fleetWorkdir();\n const prep = await prepareFleetSites(sites, {\n workdir,\n needsCheckout: (s) => auditNeedsCheckout(s, which),\n });\n sites = prep.prepared;\n skippedPrep = prep.skipped;\n }\n\n const results: AuditResult[] = [];\n const renderer = rendererFor(opts.json);\n await buildAuditTasks(sites, which, results, renderer, parseConcurrency(opts.concurrency)).run();\n\n let output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);\n\n // Surface any site that couldn't be prepared (no auditable target, clone\n // failure) — visibly, but without reding the run. One misconfigured inventory\n // row is an operator fix, not an outage; the other sites still audited and\n // wrote back. \"No silent caps\": a dropped site is never invisible.\n const skipNotice = formatSkippedNotice(skippedPrep);\n if (skipNotice && !opts.json) output += `\\n\\n${skipNotice}`;\n\n // Did any site fail to write back to Airtable? The fleet writer collects\n // per-site failures instead of throwing, so without this the command would\n // exit 0 while rows silently failed to persist — automation keying on `$?`\n // would see a clean run. (The single-site writer throws on failure, so it's\n // already non-zero via the propagated error.)\n let writeBackFailed = false;\n if (opts.writeAirtable !== undefined) {\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const { listWebsites } = await import(\"../../reports/airtable/websites.js\");\n\n if (opts.fleet !== undefined) {\n const { writeFleetAuditsToAirtable, formatFleetWriteSummary } =\n await import(\"../../audits/write-audits-to-airtable.js\");\n const base = openBase(readAirtableConfig());\n const websites = await listWebsites(base);\n const fleetWrite = await writeFleetAuditsToAirtable({ base, websites, results });\n if (fleetWrite.failed.length > 0) writeBackFailed = true;\n // Record fleet-activity events (vuln_cleared / cert_renewed rode along on each\n // WriteSummary) plus a per-sweep rollup. Best-effort: a missing Turso cred no-ops.\n const sweep = which.includes(\"security\") ? \"security\" : \"lighthouse\";\n const now = new Date();\n const auditEvents = fleetWrite.written.flatMap((w) => w.events ?? []);\n await recordFleetEventsBestEffort(\n [...auditEvents, fleetSweptEvent(sweep, fleetWrite.written.length, now.toISOString())],\n now,\n );\n // Gate on !json: the write-summary is human text; appending it after the\n // results array would corrupt `--json` output (the other notices already\n // guard this way). The write itself still happens regardless of --json.\n if (!opts.json) output += `\\n\\n${formatFleetWriteSummary(fleetWrite)}`;\n } else {\n const { resolveSlugFromCwd } = await import(\"../../audits/lighthouse-airtable.js\");\n const { writeAuditsToAirtable } = await import(\"../../audits/write-audits-to-airtable.js\");\n const slug =\n typeof opts.writeAirtable === \"string\" && opts.writeAirtable.length > 0\n ? opts.writeAirtable\n : await resolveSlugFromCwd(cwd);\n let writeSummary: WriteSummary | null = null;\n await new Listr(\n [\n {\n title: `Write to Airtable[${slug}]`,\n task: async (_ctx, task) => {\n const base = openBase(readAirtableConfig());\n task.output = \"loading Websites…\";\n const websites = await listWebsites(base);\n task.output = \"writing scores…\";\n writeSummary = await writeAuditsToAirtable({ base, websites, slug, results });\n task.title = `Wrote to Websites[${writeSummary.siteName}] (${writeSummary.writes.length} audit type${writeSummary.writes.length === 1 ? \"\" : \"s\"})`;\n },\n },\n ],\n { renderer },\n ).run();\n if (writeSummary && !opts.json) output += `\\n\\n${formatWriteSummary(writeSummary)}`;\n }\n }\n\n const notice = deployedUrlNotice(which, opts.url, cwd);\n if (notice && !opts.json) output += `\\n\\n${notice}`;\n\n const code = Math.max(\n auditExitCode(results, opts.failOnViolations === true),\n writeBackFailed ? 1 : 0,\n );\n return { output, code };\n}\n"],"mappings":"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","names":["c"]}
|
|
@@ -2,7 +2,7 @@ declare const lighthouseConfig: {
|
|
|
2
2
|
readonly ci: {
|
|
3
3
|
readonly collect: {
|
|
4
4
|
readonly url: readonly ["http://localhost:5173/dev/a11y-fixtures"];
|
|
5
|
-
readonly startServerCommand: "npm run vite:dev";
|
|
5
|
+
readonly startServerCommand: "npm run vite:dev -- --port 5173 --strictPort";
|
|
6
6
|
readonly startServerReadyPattern: "ready in";
|
|
7
7
|
readonly startServerReadyTimeout: 120000;
|
|
8
8
|
readonly numberOfRuns: 1;
|
|
@@ -12,19 +12,21 @@ import {
|
|
|
12
12
|
diffAttention,
|
|
13
13
|
readDigestState,
|
|
14
14
|
writeDigestState
|
|
15
|
-
} from "./chunk-
|
|
16
|
-
import "./chunk-
|
|
15
|
+
} from "./chunk-KKIKJWM2.js";
|
|
16
|
+
import "./chunk-R5KAXGSP.js";
|
|
17
17
|
import "./chunk-D4FVAIEJ.js";
|
|
18
18
|
import {
|
|
19
19
|
isIdempotencyConflict
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-42A4H6SF.js";
|
|
21
21
|
import "./chunk-GECESMVS.js";
|
|
22
22
|
import {
|
|
23
|
-
escapeHtml,
|
|
24
23
|
isPendingApproval,
|
|
25
24
|
listAllReports
|
|
26
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-6QRLRT3P.js";
|
|
27
26
|
import "./chunk-MPBXOJ3N.js";
|
|
27
|
+
import {
|
|
28
|
+
escapeHtml
|
|
29
|
+
} from "./chunk-SEEA6RXF.js";
|
|
28
30
|
import {
|
|
29
31
|
defaultResendClient
|
|
30
32
|
} from "./chunk-WEKL2HYJ.js";
|
|
@@ -330,4 +332,4 @@ export {
|
|
|
330
332
|
renderDigestHtml,
|
|
331
333
|
runDigest
|
|
332
334
|
};
|
|
333
|
-
//# sourceMappingURL=digest-
|
|
335
|
+
//# sourceMappingURL=digest-N232AYVG.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/reports/digest.ts"],"sourcesContent":["// src/reports/digest.ts\nimport { openBase, readAirtableConfig, type AirtableBase } from \"./airtable/client.js\";\nimport { listAllReports, isPendingApproval } from \"./airtable/reports.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { listWebsites, siteSlug, type WebsiteRow } from \"./airtable/websites.js\";\nimport { defaultResendClient, type ResendClient } from \"./send/resend.js\";\nimport { isIdempotencyConflict } from \"./send/idempotency.js\";\nimport {\n collectVulnAlerts,\n collectDeliveryFailures,\n collectPreflightBlocked,\n collectLighthouseAlerts,\n collectRenovateAlerts,\n collectCiAlerts,\n collectAnalyticsFailures,\n collectTurnstileGuardrailAlerts,\n collectNotifyBounceAlerts,\n NOTIFY_BOUNCE_WINDOW_DAYS,\n} from \"../alerts/digest-collectors.js\";\nimport { diffAttention, readDigestState, writeDigestState } from \"../alerts/digest-state.js\";\nimport { escapeHtml as esc } from \"../util/html.js\";\nimport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n SubmissionsDigestSection,\n} from \"../alerts/attention.js\";\n// Type-only: erased at compile time, so the kysely/libsql devDependency never loads\n// in a consuming fleet site (the runtime read is the dynamic import in\n// fetchSubmissionsDigestCounts, same rule as fetchNotifyBounceCounts).\nimport type { SiteSubmissionCounts } from \"../db/submissions.js\";\n\n// The attention/digest contract lives in `../alerts/attention.ts` (a dependency-free\n// types module) so the `alerts/*` collectors can depend on it without importing back\n// from this renderer/IO module — see attention.ts for the cycle it breaks. Re-exported\n// here so existing `from \"./digest.js\"` type importers keep resolving.\nexport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n SubmissionsDigestSection,\n} from \"../alerts/attention.js\";\n\nconst GREY = \"#757575\";\nconst RED = \"#C00\";\n\n/** Shared anchor style — Gmail does not inherit font-family into <a> tags. */\nconst ANCHOR_STYLE = `color:${RED};font-family:helvetica,sans-serif`;\n\nfunction readySection(items: ReadyItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Ready for your yes</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">Nothing waiting on you.</p>`;\n }\n const rows = items\n .map((it) => {\n const safeUrl = it.dashboardUrl.startsWith(\"https://\") ? it.dashboardUrl : undefined;\n const link = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">review & approve</a>`\n : `review & approve`;\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(it.siteName)}</strong> — ${esc(it.reportType)} (${esc(it.period)})\n — ${link}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${rows}</table>`;\n}\n\nconst SEVERITY_ORDER: Record<AttentionSeverity, number> = { critical: 0, warning: 1 };\n\n/** Render the per-item status badge (\"NEW\"/\"WORSE\"); standing items get nothing. */\nfunction attentionBadge(status?: AttentionStatus): string {\n if (status === \"new\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">NEW</strong> `;\n if (status === \"worse\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">WORSE</strong> `;\n return \"\";\n}\n\nfunction attentionSection(items: AttentionItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Needs attention</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">All clear — nothing needs attention.</p>`;\n }\n\n // Group by siteName, preserving first-seen site order; sort within a site by\n // severity (critical first).\n const bySite = new Map<string, AttentionItem[]>();\n for (const it of items) {\n const bucket = bySite.get(it.siteName);\n if (bucket) bucket.push(it);\n else bySite.set(it.siteName, [it]);\n }\n\n const groups = [...bySite.entries()]\n .map(([siteName, siteItems]) => {\n const sorted = [...siteItems].sort(\n (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity],\n );\n const rows = sorted\n .map((it) => {\n const safeUrl = it.url?.startsWith(\"https://\") ? it.url : undefined;\n const titleHtml = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">${esc(it.title)}</a>`\n : esc(it.title);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">${attentionBadge(it.status)}${titleHtml}</td>\n </tr>`;\n })\n .join(\"\");\n return `\n <tr>\n <td style=\"color:#222;font-family:helvetica,sans-serif;font-size:16px;font-weight:700;padding:8px 0 4px\">${esc(siteName)}</td>\n </tr>\n ${rows}`;\n })\n .join(\"\");\n\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${groups}</table>`;\n}\n\n/** The \"Submissions (24h)\" telemetry block: fleet totals + a per-site glance line.\n * Context, not an alarm — absent/null section or all-zero totals render nothing\n * (no-noise), and the section never triggers a send on its own (see runDigest). */\nfunction submissionsSection(s: SubmissionsDigestSection | null | undefined): string {\n if (!s) return \"\";\n if (s.leads === 0 && s.signups === 0 && s.spamAuto === 0) return \"\";\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Submissions (24h)</h2>`;\n // All three terms always render together — omitting a zero term next to a nonzero\n // one reads as \"not measured\" rather than \"none\"; simplest honest form.\n const totals = [\n `${s.leads} new lead${s.leads === 1 ? \"\" : \"s\"}`,\n `${s.signups} newsletter/RSVP signup${s.signups === 1 ? \"\" : \"s\"}`,\n `${s.spamAuto} auto-filtered spam`,\n ].join(\" · \");\n const totalLine = `<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">${totals}</p>`;\n if (s.bySite.length === 0) return `${heading}${totalLine}`;\n const rows = s.bySite\n .map((site) => {\n const parts: string[] = [];\n if (site.leads > 0) parts.push(`${site.leads} lead${site.leads === 1 ? \"\" : \"s\"}`);\n if (site.signups > 0) parts.push(`${site.signups} signup${site.signups === 1 ? \"\" : \"s\"}`);\n if (site.spamAuto > 0) parts.push(`${site.spamAuto} auto-filtered`);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(site.siteName)}</strong> — ${parts.join(\" · \")}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}${totalLine}<table role=\"presentation\" style=\"border-collapse:collapse;margin:8px 0 0\">${rows}</table>`;\n}\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\n/** Single-operator fleet — fallback when OPERATOR_EMAIL is unset. */\nconst DIGEST_OPERATOR_FALLBACK = \"info@reddoorla.com\";\n\n/** UTC \"YYYY-MM-DD\" — the Resend idempotency key suffix, so a same-day cron re-fire dedupes. */\nfunction digestDateKey(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * The gate for \"Ready for your yes\": Draft ready ∧ ¬Approved to send ∧ Sent at BLANK.\n *\n * Implemented as `listAllReports(base).filter(...)` (the authorized deviation from the plan's\n * draft, which pre-dated Slice 1's merged fix): `listAllReports` already calls `mapRow`, which\n * handles `Period` correctly, so no local `rawToReportRow` duplicate is needed. The JS filter\n * does real work — the test fake does NOT evaluate `filterByFormula` — so correctness is\n * test-provable here.\n *\n * Exported: the fleet homepage (Task 3.5b) reuses it for the pending-approval count.\n */\nexport async function listPendingApproval(base: AirtableBase): Promise<ReportRow[]> {\n return (await listAllReports(base)).filter(isPendingApproval);\n}\n\n// ── collectAttention (IO wrapper, sibling to runDigest) ──────────────────────\n\nexport type CollectAttentionDeps = {\n base: AirtableBase;\n /** Same baseUrl value runDigest threads; used for the /s/<slug> links. */\n baseUrl: string;\n /** Pre-fetched Websites rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listWebsites`. */\n websites?: WebsiteRow[];\n /** Pre-fetched Reports rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listAllReports`. */\n reports?: ReportRow[];\n /** Clock for the GitHub-signals staleness gate (collectCiAlerts /\n * collectRenovateAlerts skip a >3-day-stale sweep). Defaults to wall-clock;\n * runDigest threads its run-start `today` so a quiet repo's frozen CI/Renovate\n * signal stops badging once its sweep goes stale. */\n now?: Date;\n /** Pre-fetched per-site bounced-lead-notification counts (tests, or a caller with\n * an open db). When omitted, collectAttention reads them from libSQL itself —\n * defensively, so a missing TURSO env or a Turso blip drops just this signal. */\n notifyBounces?: ReadonlyMap<string, number>;\n};\n\n/** Per-site bounced-notification counts for the notify-bounce collector, read from\n * libSQL over the collector's own window. The db modules are imported DYNAMICALLY:\n * src/db/submissions.ts pulls kysely at top level, and kysely/libsql live in\n * devDependencies consuming fleet sites don't install — a static import here would\n * crash their CLI at require time (same rule as openDb's own lazy loads). Any\n * failure (no TURSO env, Turso down) logs and yields an empty map: the digest\n * must never blank over a missing optional signal. */\nasync function fetchNotifyBounceCounts(now: Date): Promise<ReadonlyMap<string, number>> {\n try {\n const [{ openDb, readDbConfig }, { countNotifyBouncedBySite }, { screenOutsSince }] =\n await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/submissions.js\"),\n import(\"../db/screenouts.js\"),\n ]);\n const db = await openDb(readDbConfig());\n return await countNotifyBouncedBySite(db, screenOutsSince(now, NOTIFY_BOUNCE_WINDOW_DAYS));\n } catch (e) {\n console.warn(`⚠ notify-bounce counts unavailable (libSQL): ${(e as Error).message}`);\n return new Map();\n }\n}\n\n/** The \"Submissions (24h)\" telemetry window — a precise 24h ISO-timestamp compare\n * (unlike screenOutsSince's date-only strings). */\nconst SUBMISSIONS_DIGEST_WINDOW_MS = 24 * 60 * 60 * 1000;\n\n/** Per-site submission counts for the digest telemetry section, read from libSQL.\n * Dynamic imports + fail-soft for the same reason as fetchNotifyBounceCounts:\n * kysely/libsql are devDependencies consuming fleet sites don't install, and a\n * Turso blip must drop just this section, never the digest. Returns null (not an\n * empty map) on failure so the renderer can omit the section rather than claim a\n * quiet fleet. */\nasync function fetchSubmissionsDigestCounts(\n now: Date,\n): Promise<ReadonlyMap<string, SiteSubmissionCounts> | null> {\n try {\n const [{ openDb, readDbConfig }, { countSubmissionsSinceBySite }] = await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/submissions.js\"),\n ]);\n const db = await openDb(readDbConfig());\n return await countSubmissionsSinceBySite(\n db,\n new Date(now.getTime() - SUBMISSIONS_DIGEST_WINDOW_MS).toISOString(),\n );\n } catch (e) {\n console.warn(`⚠ submissions telemetry unavailable (libSQL): ${(e as Error).message}`);\n return null;\n }\n}\n\n/** Assemble the \"Submissions (24h)\" section from the per-site counts. PURE (exported\n * for direct tests). Totals sum ALL map entries — an orphan site id still counts in\n * the fleet totals. bySite lists only ids that resolve in `sitesById` AND carry any\n * nonzero count, sorted by total volume desc then siteName A-Z. null counts (libSQL\n * unavailable) → null (section omitted). */\nexport function buildSubmissionsDigestSection(\n counts: ReadonlyMap<string, SiteSubmissionCounts> | null,\n sitesById: ReadonlyMap<string, WebsiteRow>,\n): SubmissionsDigestSection | null {\n if (counts === null) return null;\n let leads = 0;\n let signups = 0;\n let spamAuto = 0;\n const bySite: SubmissionsDigestSection[\"bySite\"] = [];\n for (const [siteId, c] of counts) {\n leads += c.leads;\n signups += c.signups;\n spamAuto += c.spamAuto;\n const site = sitesById.get(siteId);\n if (site && c.leads + c.signups + c.spamAuto > 0) {\n bySite.push({ siteName: site.name, ...c });\n }\n }\n bySite.sort(\n (a, b) =>\n b.leads + b.signups + b.spamAuto - (a.leads + a.signups + a.spamAuto) ||\n a.siteName.localeCompare(b.siteName),\n );\n return { leads, signups, spamAuto, bySite };\n}\n\n/** Run a single collector under a try/catch: a thrown collector logs and yields []\n * so one broken signal never blanks the whole \"Needs attention\" section. */\nfunction runCollector(label: string, fn: () => AttentionItem[]): AttentionItem[] {\n try {\n return fn();\n } catch (e) {\n console.warn(`⚠ attention collector \"${label}\" failed: ${(e as Error).message}`);\n return [];\n }\n}\n\n/**\n * Fetch the free signals once (listAllReports + listWebsites) — or reuse the\n * `reports`/`websites` arrays runDigest already read, so a single run reads each\n * table once — build the sitesById map the delivery collector needs, and run each\n * pure collector isolated. Returns the union of items; diffing/badging happens in\n * runDigest.\n *\n * The Renovate + CI signals come from the SAME persisted collectors the operator\n * cockpit (`buildCockpitModel`) runs — `collectRenovateAlerts` (key\n * `renovate:<siteId>`) and `collectCiAlerts` (key `ci:<siteId>`), reading the\n * nightly-persisted `renovateFailingCis`/`defaultBranchCi` fields. This is the\n * key-space unification: because the digest writes the shared Digest State\n * snapshot with these same keys, the cockpit's NEW/WORSE diff finds them and the\n * two surfaces agree (the prior live per-PR `renovate:<repo>#<n>` sweep never\n * matched the cockpit's keys, so its cards badged NEW forever).\n */\nexport async function collectAttention(deps: CollectAttentionDeps): Promise<AttentionItem[]> {\n const reports = deps.reports ?? (await listAllReports(deps.base));\n const websites = deps.websites ?? (await listWebsites(deps.base));\n const now = deps.now ?? new Date();\n const notifyBounces = deps.notifyBounces ?? (await fetchNotifyBounceCounts(now));\n const sitesById = new Map<string, WebsiteRow>(websites.map((w) => [w.id, w]));\n return [\n ...runCollector(\"vuln\", () => collectVulnAlerts(websites, deps.baseUrl)),\n ...runCollector(\"delivery\", () => collectDeliveryFailures(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"preflight\", () => collectPreflightBlocked(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"lighthouse\", () => collectLighthouseAlerts(websites, deps.baseUrl)),\n ...runCollector(\"renovate\", () => collectRenovateAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"ci\", () => collectCiAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"analytics\", () => collectAnalyticsFailures(websites, deps.baseUrl, now)),\n ...runCollector(\"turnstile\", () =>\n collectTurnstileGuardrailAlerts(websites, deps.baseUrl, now),\n ),\n ...runCollector(\"notify-bounce\", () =>\n collectNotifyBounceAlerts(websites, notifyBounces, deps.baseUrl),\n ),\n ];\n}\n\nexport type DigestRunOptions = {\n resend?: ResendClient;\n /** Dashboard origin for the /s/<slug> links, e.g. \"https://reddoor-maintenance.netlify.app\". */\n baseUrl: string;\n /**\n * Inject a pre-opened Airtable base (tests, server handlers).\n * When omitted, `openBase(readAirtableConfig())` is called from the environment.\n */\n base?: AirtableBase;\n /** Per-site submission counts for the \"Submissions (24h)\" section. undefined =\n * fetch from libSQL; null = simulate unavailable (section omitted); a Map =\n * injected (tests). */\n submissionCounts?: ReadonlyMap<string, SiteSubmissionCounts> | null;\n};\n\nexport async function runDigest(\n options: DigestRunOptions,\n): Promise<{ output: string; code: number }> {\n // Capture clock BEFORE any await so the idempotency key can't roll past midnight mid-run.\n const today = new Date();\n try {\n const base = options.base ?? openBase(readAirtableConfig());\n // Read each table ONCE for the whole run, then thread the arrays into\n // collectAttention so it doesn't re-fetch (was: listWebsites ×2, listAllReports\n // ×2). Pending is derived in-line with listPendingApproval's exact predicate.\n const reports = await listAllReports(base);\n const websites = await listWebsites(base);\n const sites = new Map(websites.map((w) => [w.id, w]));\n\n const pending = reports.filter(isPendingApproval);\n\n const readyForYourYes: ReadyItem[] = [];\n const baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n for (const r of pending) {\n const site = sites.get(r.siteId);\n if (!site) continue; // orphan report → skip rather than render a broken link\n // An empty Name slugs to \"\" → `/s/` is a dead link (getWebsiteBySlug can't\n // match it). Fall back to the fleet homepage so the operator still lands\n // somewhere usable instead of a 404.\n const slug = siteSlug(site.name);\n readyForYourYes.push({\n siteName: site.name,\n reportType: r.reportType,\n period: r.period ?? \"—\",\n dashboardUrl: slug ? `${baseUrl}/s/${slug}` : baseUrl,\n });\n }\n\n // M5: collect the free signals (isolated), diff against yesterday's snapshot.\n // Renovate + CI come from the persisted collectors (the same ones the cockpit\n // runs), so the snapshot this digest writes carries the `renovate:<siteId>` /\n // `ci:<siteId>` keys the cockpit diffs against — no live GitHub sweep here.\n const collected = await collectAttention({\n base,\n baseUrl: options.baseUrl,\n websites,\n reports,\n now: today,\n });\n const prior = await readDigestState(base);\n const { tagged, next } = diffAttention(collected, prior, digestDateKey(today));\n // The operator only HEARS about a vuln once Renovate's auto-fix is exhausted\n // (tried and failed a couple of nightly cycles) — before that the fleet is\n // still self-patching and the cockpit's amber Watch band is the only surface.\n // Filter the EMAIL list only: `next` (written below) must keep every vuln key\n // so the cockpit's diff against this same snapshot stays consistent, and so\n // the exhausted-flip diffs as WORSE instead of arriving pre-badged-away.\n const needsAttention = tagged.filter(\n (it) => it.kind !== \"vuln\" || it.autoFixExhausted === true,\n );\n\n // No-noise default: skip entirely when there's nothing to report.\n if (readyForYourYes.length === 0 && needsAttention.length === 0) {\n // Persist `next` even on a skip: a key that resolved on a quiet day clears so a\n // later recurrence diffs as NEW (spec §10), and — since the skip can now fire\n // with muted pre-exhaustion vulns collected — their keys keep snapshotting so\n // the eventual exhausted-flip diffs as WORSE. Wrapped: a write failure can't\n // fail the skip.\n try {\n await writeDigestState(base, next);\n } catch (e) {\n console.warn(`⚠ digest state write failed: ${(e as Error).message}`);\n }\n return { output: \"Digest skipped (nothing ready, nothing needs attention).\", code: 0 };\n }\n\n // Submissions telemetry rides only on a digest that is ALREADY sending — the\n // skip predicate above is deliberately unchanged (leads already fire their own\n // ingest-time notification; this is context, not an alarm), and fetching AFTER\n // the skip check keeps quiet days from touching libSQL at all.\n const counts =\n options.submissionCounts !== undefined\n ? options.submissionCounts\n : await fetchSubmissionsDigestCounts(today);\n const submissions = buildSubmissionsDigestSection(counts, sites);\n\n const html = renderDigestHtml({ readyForYourYes, needsAttention, submissions });\n const client = options.resend ?? defaultResendClient();\n const to = [process.env.OPERATOR_EMAIL?.trim() || DIGEST_OPERATOR_FALLBACK];\n const n = readyForYourYes.length;\n const reportWord = n === 1 ? \"report\" : \"reports\";\n let result: Awaited<ReturnType<typeof client.send>>;\n try {\n result = await client.send({\n from: FROM_ADDRESS,\n to,\n subject: `Your fleet — ${digestDateKey(today)}: ${n} ${reportWord} ready for your yes`,\n html,\n idempotencyKey: `digest-${digestDateKey(today)}`,\n });\n } catch (err) {\n // A same-UTC-day re-run whose content changed re-sends with the same\n // `digest-<date>` idempotency key but a DIFFERENT body. Resend rejects that\n // with a 409 (`invalid_idempotent_request`) — \"This idempotency key has been\n // used ... but the request body was modified ...\". The operator already got\n // today's digest on the first send, so re-sending a changed version same-day\n // would just be a duplicate: treat it as an \"already sent today\" no-op.\n //\n // ResendClient (send/resend.ts) wraps the API error as a plain Error and only\n // preserves the *message* string (no name/statusCode), so the message\n // substring is the only reliable discriminator — match defensively on it.\n // Any OTHER send error re-throws to the outer catch → {code:1}, so a genuine\n // Resend/network failure still fails loudly.\n if (isIdempotencyConflict(err)) {\n // Do NOT write the snapshot: the first send already persisted it; writing\n // this run's `next` would diff against the first run's snapshot and mis-badge.\n return {\n output:\n \"Digest already sent today (content changed since the first send) — skipped to avoid a duplicate.\",\n code: 0,\n };\n }\n throw err;\n }\n // Persist the next snapshot AFTER a successful send. A write failure is caught +\n // logged: the digest already went out, tomorrow re-news at worst. (The send-FAILURE\n // path never reaches here — the outer catch returns code 1 with no write, preserving\n // the NEW badge for the retry.)\n try {\n await writeDigestState(base, next);\n } catch (e) {\n console.warn(`⚠ digest state write failed: ${(e as Error).message}`);\n }\n return { output: `Digest sent to ${to.join(\", \")} (${result.messageId})`, code: 0 };\n } catch (err) {\n // Re-throw config errors (exitCode=2: missing env vars, bad config) so runOrExit\n // surfaces them with the correct process exit code rather than collapsing to 1.\n if (typeof (err as { exitCode?: unknown }).exitCode === \"number\") {\n throw err;\n }\n const message = err instanceof Error ? err.message : String(err);\n return { output: `digest failed: ${message}`, code: 1 };\n }\n}\n\n/** Pure render of the unified daily operator digest. No IO — the caller (runDigest)\n * collects the rows and decides whether to send. */\nexport function renderDigestHtml(sections: DigestSections): string {\n return `<!doctype html>\n<html>\n <head><meta charset=\"utf-8\"></head>\n <body style=\"margin:0;padding:0;background:#ffffff\">\n <table width=\"100%\" style=\"border-collapse:collapse\">\n <tr>\n <td align=\"center\" style=\"padding:24px\">\n <table width=\"600\" style=\"border-collapse:collapse\">\n <tr>\n <td>\n <h1 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:24px;font-weight:700;margin:0 0 8px\">Your fleet today</h1>\n ${readySection(sections.readyForYourYes)}\n ${attentionSection(sections.needsAttention)}\n ${submissionsSection(sections.submissions)}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/reports/digest.ts"],"sourcesContent":["// src/reports/digest.ts\nimport { openBase, readAirtableConfig, type AirtableBase } from \"./airtable/client.js\";\nimport { listAllReports, isPendingApproval } from \"./airtable/reports.js\";\nimport type { ReportRow } from \"./airtable/reports.js\";\nimport { listWebsites, siteSlug, type WebsiteRow } from \"./airtable/websites.js\";\nimport { defaultResendClient, type ResendClient } from \"./send/resend.js\";\nimport { isIdempotencyConflict } from \"./send/idempotency.js\";\nimport {\n collectVulnAlerts,\n collectDeliveryFailures,\n collectPreflightBlocked,\n collectLighthouseAlerts,\n collectRenovateAlerts,\n collectCiAlerts,\n collectAnalyticsFailures,\n collectTurnstileGuardrailAlerts,\n collectNotifyBounceAlerts,\n NOTIFY_BOUNCE_WINDOW_DAYS,\n} from \"../alerts/digest-collectors.js\";\nimport { diffAttention, readDigestState, writeDigestState } from \"../alerts/digest-state.js\";\nimport { escapeHtml as esc } from \"../util/html.js\";\nimport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n SubmissionsDigestSection,\n} from \"../alerts/attention.js\";\n// Type-only: erased at compile time, so the kysely/libsql devDependency never loads\n// in a consuming fleet site (the runtime read is the dynamic import in\n// fetchSubmissionsDigestCounts, same rule as fetchNotifyBounceCounts).\nimport type { SiteSubmissionCounts } from \"../db/submissions.js\";\n\n// The attention/digest contract lives in `../alerts/attention.ts` (a dependency-free\n// types module) so the `alerts/*` collectors can depend on it without importing back\n// from this renderer/IO module — see attention.ts for the cycle it breaks. Re-exported\n// here so existing `from \"./digest.js\"` type importers keep resolving.\nexport type {\n AttentionItem,\n AttentionSeverity,\n AttentionStatus,\n ReadyItem,\n DigestSections,\n SubmissionsDigestSection,\n} from \"../alerts/attention.js\";\n\nconst GREY = \"#757575\";\nconst RED = \"#C00\";\n\n/** Shared anchor style — Gmail does not inherit font-family into <a> tags. */\nconst ANCHOR_STYLE = `color:${RED};font-family:helvetica,sans-serif`;\n\nfunction readySection(items: ReadyItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Ready for your yes</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">Nothing waiting on you.</p>`;\n }\n const rows = items\n .map((it) => {\n const safeUrl = it.dashboardUrl.startsWith(\"https://\") ? it.dashboardUrl : undefined;\n const link = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">review & approve</a>`\n : `review & approve`;\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(it.siteName)}</strong> — ${esc(it.reportType)} (${esc(it.period)})\n — ${link}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${rows}</table>`;\n}\n\nconst SEVERITY_ORDER: Record<AttentionSeverity, number> = { critical: 0, warning: 1 };\n\n/** Render the per-item status badge (\"NEW\"/\"WORSE\"); standing items get nothing. */\nfunction attentionBadge(status?: AttentionStatus): string {\n if (status === \"new\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">NEW</strong> `;\n if (status === \"worse\")\n return `<strong style=\"color:${RED};font-family:helvetica,sans-serif\">WORSE</strong> `;\n return \"\";\n}\n\nfunction attentionSection(items: AttentionItem[]): string {\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Needs attention</h2>`;\n if (items.length === 0) {\n return `${heading}<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">All clear — nothing needs attention.</p>`;\n }\n\n // Group by siteName, preserving first-seen site order; sort within a site by\n // severity (critical first).\n const bySite = new Map<string, AttentionItem[]>();\n for (const it of items) {\n const bucket = bySite.get(it.siteName);\n if (bucket) bucket.push(it);\n else bySite.set(it.siteName, [it]);\n }\n\n const groups = [...bySite.entries()]\n .map(([siteName, siteItems]) => {\n const sorted = [...siteItems].sort(\n (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity],\n );\n const rows = sorted\n .map((it) => {\n const safeUrl = it.url?.startsWith(\"https://\") ? it.url : undefined;\n const titleHtml = safeUrl\n ? `<a href=\"${esc(safeUrl)}\" style=\"${ANCHOR_STYLE}\">${esc(it.title)}</a>`\n : esc(it.title);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">${attentionBadge(it.status)}${titleHtml}</td>\n </tr>`;\n })\n .join(\"\");\n return `\n <tr>\n <td style=\"color:#222;font-family:helvetica,sans-serif;font-size:16px;font-weight:700;padding:8px 0 4px\">${esc(siteName)}</td>\n </tr>\n ${rows}`;\n })\n .join(\"\");\n\n return `${heading}<table role=\"presentation\" style=\"border-collapse:collapse;margin:0\">${groups}</table>`;\n}\n\n/** The \"Submissions (24h)\" telemetry block: fleet totals + a per-site glance line.\n * Context, not an alarm — absent/null section or all-zero totals render nothing\n * (no-noise), and the section never triggers a send on its own (see runDigest). */\nfunction submissionsSection(s: SubmissionsDigestSection | null | undefined): string {\n if (!s) return \"\";\n if (s.leads === 0 && s.signups === 0 && s.spamAuto === 0) return \"\";\n const heading = `<h2 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:20px;font-weight:700;margin:32px 0 8px\">Submissions (24h)</h2>`;\n // All three terms always render together — omitting a zero term next to a nonzero\n // one reads as \"not measured\" rather than \"none\"; simplest honest form.\n const totals = [\n `${s.leads} new lead${s.leads === 1 ? \"\" : \"s\"}`,\n `${s.signups} newsletter/RSVP signup${s.signups === 1 ? \"\" : \"s\"}`,\n `${s.spamAuto} auto-filtered spam`,\n ].join(\" · \");\n const totalLine = `<p style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;margin:0\">${totals}</p>`;\n if (s.bySite.length === 0) return `${heading}${totalLine}`;\n const rows = s.bySite\n .map((site) => {\n const parts: string[] = [];\n if (site.leads > 0) parts.push(`${site.leads} lead${site.leads === 1 ? \"\" : \"s\"}`);\n if (site.signups > 0) parts.push(`${site.signups} signup${site.signups === 1 ? \"\" : \"s\"}`);\n if (site.spamAuto > 0) parts.push(`${site.spamAuto} auto-filtered`);\n return `\n <tr>\n <td style=\"color:${GREY};font-family:helvetica,sans-serif;font-size:16px;line-height:24px;padding-bottom:8px\">\n <strong style=\"color:#222\">${esc(site.siteName)}</strong> — ${parts.join(\" · \")}\n </td>\n </tr>`;\n })\n .join(\"\");\n return `${heading}${totalLine}<table role=\"presentation\" style=\"border-collapse:collapse;margin:8px 0 0\">${rows}</table>`;\n}\n\nconst FROM_ADDRESS = \"Reddoor Reports <reports@reddoorla.com>\";\n/** Single-operator fleet — fallback when OPERATOR_EMAIL is unset. */\nconst DIGEST_OPERATOR_FALLBACK = \"info@reddoorla.com\";\n\n/** UTC \"YYYY-MM-DD\" — the Resend idempotency key suffix, so a same-day cron re-fire dedupes. */\nfunction digestDateKey(d: Date): string {\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * The gate for \"Ready for your yes\": Draft ready ∧ ¬Approved to send ∧ Sent at BLANK.\n *\n * Implemented as `listAllReports(base).filter(...)` (the authorized deviation from the plan's\n * draft, which pre-dated Slice 1's merged fix): `listAllReports` already calls `mapRow`, which\n * handles `Period` correctly, so no local `rawToReportRow` duplicate is needed. The JS filter\n * does real work — the test fake does NOT evaluate `filterByFormula` — so correctness is\n * test-provable here.\n *\n * Exported: the fleet homepage (Task 3.5b) reuses it for the pending-approval count.\n */\nexport async function listPendingApproval(base: AirtableBase): Promise<ReportRow[]> {\n return (await listAllReports(base)).filter(isPendingApproval);\n}\n\n// ── collectAttention (IO wrapper, sibling to runDigest) ──────────────────────\n\nexport type CollectAttentionDeps = {\n base: AirtableBase;\n /** Same baseUrl value runDigest threads; used for the /s/<slug> links. */\n baseUrl: string;\n /** Pre-fetched Websites rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listWebsites`. */\n websites?: WebsiteRow[];\n /** Pre-fetched Reports rows. When supplied (runDigest already read them),\n * collectAttention reuses them instead of issuing a second `listAllReports`. */\n reports?: ReportRow[];\n /** Clock for the GitHub-signals staleness gate (collectCiAlerts /\n * collectRenovateAlerts skip a >3-day-stale sweep). Defaults to wall-clock;\n * runDigest threads its run-start `today` so a quiet repo's frozen CI/Renovate\n * signal stops badging once its sweep goes stale. */\n now?: Date;\n /** Pre-fetched per-site bounced-lead-notification counts (tests, or a caller with\n * an open db). When omitted, collectAttention reads them from libSQL itself —\n * defensively, so a missing TURSO env or a Turso blip drops just this signal. */\n notifyBounces?: ReadonlyMap<string, number>;\n};\n\n/** Per-site bounced-notification counts for the notify-bounce collector, read from\n * libSQL over the collector's own window. The db modules are imported DYNAMICALLY:\n * src/db/submissions.ts pulls kysely at top level, and kysely/libsql live in\n * devDependencies consuming fleet sites don't install — a static import here would\n * crash their CLI at require time (same rule as openDb's own lazy loads). Any\n * failure (no TURSO env, Turso down) logs and yields an empty map: the digest\n * must never blank over a missing optional signal. */\nasync function fetchNotifyBounceCounts(now: Date): Promise<ReadonlyMap<string, number>> {\n try {\n const [{ openDb, readDbConfig }, { countNotifyBouncedBySite }, { screenOutsSince }] =\n await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/submissions.js\"),\n import(\"../db/screenouts.js\"),\n ]);\n const db = await openDb(readDbConfig());\n return await countNotifyBouncedBySite(db, screenOutsSince(now, NOTIFY_BOUNCE_WINDOW_DAYS));\n } catch (e) {\n console.warn(`⚠ notify-bounce counts unavailable (libSQL): ${(e as Error).message}`);\n return new Map();\n }\n}\n\n/** The \"Submissions (24h)\" telemetry window — a precise 24h ISO-timestamp compare\n * (unlike screenOutsSince's date-only strings). */\nconst SUBMISSIONS_DIGEST_WINDOW_MS = 24 * 60 * 60 * 1000;\n\n/** Per-site submission counts for the digest telemetry section, read from libSQL.\n * Dynamic imports + fail-soft for the same reason as fetchNotifyBounceCounts:\n * kysely/libsql are devDependencies consuming fleet sites don't install, and a\n * Turso blip must drop just this section, never the digest. Returns null (not an\n * empty map) on failure so the renderer can omit the section rather than claim a\n * quiet fleet. */\nasync function fetchSubmissionsDigestCounts(\n now: Date,\n): Promise<ReadonlyMap<string, SiteSubmissionCounts> | null> {\n try {\n const [{ openDb, readDbConfig }, { countSubmissionsSinceBySite }] = await Promise.all([\n import(\"../db/client.js\"),\n import(\"../db/submissions.js\"),\n ]);\n const db = await openDb(readDbConfig());\n return await countSubmissionsSinceBySite(\n db,\n new Date(now.getTime() - SUBMISSIONS_DIGEST_WINDOW_MS).toISOString(),\n );\n } catch (e) {\n console.warn(`⚠ submissions telemetry unavailable (libSQL): ${(e as Error).message}`);\n return null;\n }\n}\n\n/** Assemble the \"Submissions (24h)\" section from the per-site counts. PURE (exported\n * for direct tests). Totals sum ALL map entries — an orphan site id still counts in\n * the fleet totals. bySite lists only ids that resolve in `sitesById` AND carry any\n * nonzero count, sorted by total volume desc then siteName A-Z. null counts (libSQL\n * unavailable) → null (section omitted). */\nexport function buildSubmissionsDigestSection(\n counts: ReadonlyMap<string, SiteSubmissionCounts> | null,\n sitesById: ReadonlyMap<string, WebsiteRow>,\n): SubmissionsDigestSection | null {\n if (counts === null) return null;\n let leads = 0;\n let signups = 0;\n let spamAuto = 0;\n const bySite: SubmissionsDigestSection[\"bySite\"] = [];\n for (const [siteId, c] of counts) {\n leads += c.leads;\n signups += c.signups;\n spamAuto += c.spamAuto;\n const site = sitesById.get(siteId);\n if (site && c.leads + c.signups + c.spamAuto > 0) {\n bySite.push({ siteName: site.name, ...c });\n }\n }\n bySite.sort(\n (a, b) =>\n b.leads + b.signups + b.spamAuto - (a.leads + a.signups + a.spamAuto) ||\n a.siteName.localeCompare(b.siteName),\n );\n return { leads, signups, spamAuto, bySite };\n}\n\n/** Run a single collector under a try/catch: a thrown collector logs and yields []\n * so one broken signal never blanks the whole \"Needs attention\" section. */\nfunction runCollector(label: string, fn: () => AttentionItem[]): AttentionItem[] {\n try {\n return fn();\n } catch (e) {\n console.warn(`⚠ attention collector \"${label}\" failed: ${(e as Error).message}`);\n return [];\n }\n}\n\n/**\n * Fetch the free signals once (listAllReports + listWebsites) — or reuse the\n * `reports`/`websites` arrays runDigest already read, so a single run reads each\n * table once — build the sitesById map the delivery collector needs, and run each\n * pure collector isolated. Returns the union of items; diffing/badging happens in\n * runDigest.\n *\n * The Renovate + CI signals come from the SAME persisted collectors the operator\n * cockpit (`buildCockpitModel`) runs — `collectRenovateAlerts` (key\n * `renovate:<siteId>`) and `collectCiAlerts` (key `ci:<siteId>`), reading the\n * nightly-persisted `renovateFailingCis`/`defaultBranchCi` fields. This is the\n * key-space unification: because the digest writes the shared Digest State\n * snapshot with these same keys, the cockpit's NEW/WORSE diff finds them and the\n * two surfaces agree (the prior live per-PR `renovate:<repo>#<n>` sweep never\n * matched the cockpit's keys, so its cards badged NEW forever).\n */\nexport async function collectAttention(deps: CollectAttentionDeps): Promise<AttentionItem[]> {\n const reports = deps.reports ?? (await listAllReports(deps.base));\n const websites = deps.websites ?? (await listWebsites(deps.base));\n const now = deps.now ?? new Date();\n const notifyBounces = deps.notifyBounces ?? (await fetchNotifyBounceCounts(now));\n const sitesById = new Map<string, WebsiteRow>(websites.map((w) => [w.id, w]));\n return [\n ...runCollector(\"vuln\", () => collectVulnAlerts(websites, deps.baseUrl)),\n ...runCollector(\"delivery\", () => collectDeliveryFailures(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"preflight\", () => collectPreflightBlocked(reports, sitesById, deps.baseUrl)),\n ...runCollector(\"lighthouse\", () => collectLighthouseAlerts(websites, deps.baseUrl)),\n ...runCollector(\"renovate\", () => collectRenovateAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"ci\", () => collectCiAlerts(websites, deps.baseUrl, now)),\n ...runCollector(\"analytics\", () => collectAnalyticsFailures(websites, deps.baseUrl, now)),\n ...runCollector(\"turnstile\", () =>\n collectTurnstileGuardrailAlerts(websites, deps.baseUrl, now),\n ),\n ...runCollector(\"notify-bounce\", () =>\n collectNotifyBounceAlerts(websites, notifyBounces, deps.baseUrl),\n ),\n ];\n}\n\nexport type DigestRunOptions = {\n resend?: ResendClient;\n /** Dashboard origin for the /s/<slug> links, e.g. \"https://reddoor-maintenance.netlify.app\". */\n baseUrl: string;\n /**\n * Inject a pre-opened Airtable base (tests, server handlers).\n * When omitted, `openBase(readAirtableConfig())` is called from the environment.\n */\n base?: AirtableBase;\n /** Per-site submission counts for the \"Submissions (24h)\" section. undefined =\n * fetch from libSQL; null = simulate unavailable (section omitted); a Map =\n * injected (tests). */\n submissionCounts?: ReadonlyMap<string, SiteSubmissionCounts> | null;\n};\n\nexport async function runDigest(\n options: DigestRunOptions,\n): Promise<{ output: string; code: number }> {\n // Capture clock BEFORE any await so the idempotency key can't roll past midnight mid-run.\n const today = new Date();\n try {\n const base = options.base ?? openBase(readAirtableConfig());\n // Read each table ONCE for the whole run, then thread the arrays into\n // collectAttention so it doesn't re-fetch (was: listWebsites ×2, listAllReports\n // ×2). Pending is derived in-line with listPendingApproval's exact predicate.\n const reports = await listAllReports(base);\n const websites = await listWebsites(base);\n const sites = new Map(websites.map((w) => [w.id, w]));\n\n const pending = reports.filter(isPendingApproval);\n\n const readyForYourYes: ReadyItem[] = [];\n const baseUrl = options.baseUrl.replace(/\\/$/, \"\");\n for (const r of pending) {\n const site = sites.get(r.siteId);\n if (!site) continue; // orphan report → skip rather than render a broken link\n // An empty Name slugs to \"\" → `/s/` is a dead link (getWebsiteBySlug can't\n // match it). Fall back to the fleet homepage so the operator still lands\n // somewhere usable instead of a 404.\n const slug = siteSlug(site.name);\n readyForYourYes.push({\n siteName: site.name,\n reportType: r.reportType,\n period: r.period ?? \"—\",\n dashboardUrl: slug ? `${baseUrl}/s/${slug}` : baseUrl,\n });\n }\n\n // M5: collect the free signals (isolated), diff against yesterday's snapshot.\n // Renovate + CI come from the persisted collectors (the same ones the cockpit\n // runs), so the snapshot this digest writes carries the `renovate:<siteId>` /\n // `ci:<siteId>` keys the cockpit diffs against — no live GitHub sweep here.\n const collected = await collectAttention({\n base,\n baseUrl: options.baseUrl,\n websites,\n reports,\n now: today,\n });\n const prior = await readDigestState(base);\n const { tagged, next } = diffAttention(collected, prior, digestDateKey(today));\n // The operator only HEARS about a vuln once Renovate's auto-fix is exhausted\n // (tried and failed a couple of nightly cycles) — before that the fleet is\n // still self-patching and the cockpit's amber Watch band is the only surface.\n // Filter the EMAIL list only: `next` (written below) must keep every vuln key\n // so the cockpit's diff against this same snapshot stays consistent, and so\n // the exhausted-flip diffs as WORSE instead of arriving pre-badged-away.\n const needsAttention = tagged.filter(\n (it) => it.kind !== \"vuln\" || it.autoFixExhausted === true,\n );\n\n // No-noise default: skip entirely when there's nothing to report.\n if (readyForYourYes.length === 0 && needsAttention.length === 0) {\n // Persist `next` even on a skip: a key that resolved on a quiet day clears so a\n // later recurrence diffs as NEW (spec §10), and — since the skip can now fire\n // with muted pre-exhaustion vulns collected — their keys keep snapshotting so\n // the eventual exhausted-flip diffs as WORSE. Wrapped: a write failure can't\n // fail the skip.\n try {\n await writeDigestState(base, next);\n } catch (e) {\n console.warn(`⚠ digest state write failed: ${(e as Error).message}`);\n }\n return { output: \"Digest skipped (nothing ready, nothing needs attention).\", code: 0 };\n }\n\n // Submissions telemetry rides only on a digest that is ALREADY sending — the\n // skip predicate above is deliberately unchanged (leads already fire their own\n // ingest-time notification; this is context, not an alarm), and fetching AFTER\n // the skip check keeps quiet days from touching libSQL at all.\n const counts =\n options.submissionCounts !== undefined\n ? options.submissionCounts\n : await fetchSubmissionsDigestCounts(today);\n const submissions = buildSubmissionsDigestSection(counts, sites);\n\n const html = renderDigestHtml({ readyForYourYes, needsAttention, submissions });\n const client = options.resend ?? defaultResendClient();\n const to = [process.env.OPERATOR_EMAIL?.trim() || DIGEST_OPERATOR_FALLBACK];\n const n = readyForYourYes.length;\n const reportWord = n === 1 ? \"report\" : \"reports\";\n let result: Awaited<ReturnType<typeof client.send>>;\n try {\n result = await client.send({\n from: FROM_ADDRESS,\n to,\n subject: `Your fleet — ${digestDateKey(today)}: ${n} ${reportWord} ready for your yes`,\n html,\n idempotencyKey: `digest-${digestDateKey(today)}`,\n });\n } catch (err) {\n // A same-UTC-day re-run whose content changed re-sends with the same\n // `digest-<date>` idempotency key but a DIFFERENT body. Resend rejects that\n // with a 409 (`invalid_idempotent_request`) — \"This idempotency key has been\n // used ... but the request body was modified ...\". The operator already got\n // today's digest on the first send, so re-sending a changed version same-day\n // would just be a duplicate: treat it as an \"already sent today\" no-op.\n //\n // ResendClient (send/resend.ts) wraps the API error as a plain Error and only\n // preserves the *message* string (no name/statusCode), so the message\n // substring is the only reliable discriminator — match defensively on it.\n // Any OTHER send error re-throws to the outer catch → {code:1}, so a genuine\n // Resend/network failure still fails loudly.\n if (isIdempotencyConflict(err)) {\n // Do NOT write the snapshot: the first send already persisted it; writing\n // this run's `next` would diff against the first run's snapshot and mis-badge.\n return {\n output:\n \"Digest already sent today (content changed since the first send) — skipped to avoid a duplicate.\",\n code: 0,\n };\n }\n throw err;\n }\n // Persist the next snapshot AFTER a successful send. A write failure is caught +\n // logged: the digest already went out, tomorrow re-news at worst. (The send-FAILURE\n // path never reaches here — the outer catch returns code 1 with no write, preserving\n // the NEW badge for the retry.)\n try {\n await writeDigestState(base, next);\n } catch (e) {\n console.warn(`⚠ digest state write failed: ${(e as Error).message}`);\n }\n return { output: `Digest sent to ${to.join(\", \")} (${result.messageId})`, code: 0 };\n } catch (err) {\n // Re-throw config errors (exitCode=2: missing env vars, bad config) so runOrExit\n // surfaces them with the correct process exit code rather than collapsing to 1.\n if (typeof (err as { exitCode?: unknown }).exitCode === \"number\") {\n throw err;\n }\n const message = err instanceof Error ? err.message : String(err);\n return { output: `digest failed: ${message}`, code: 1 };\n }\n}\n\n/** Pure render of the unified daily operator digest. No IO — the caller (runDigest)\n * collects the rows and decides whether to send. */\nexport function renderDigestHtml(sections: DigestSections): string {\n return `<!doctype html>\n<html>\n <head><meta charset=\"utf-8\"></head>\n <body style=\"margin:0;padding:0;background:#ffffff\">\n <table width=\"100%\" style=\"border-collapse:collapse\">\n <tr>\n <td align=\"center\" style=\"padding:24px\">\n <table width=\"600\" style=\"border-collapse:collapse\">\n <tr>\n <td>\n <h1 style=\"color:${RED};font-family:helvetica,sans-serif;font-size:24px;font-weight:700;margin:0 0 8px\">Your fleet today</h1>\n ${readySection(sections.readyForYourYes)}\n ${attentionSection(sections.needsAttention)}\n ${submissionsSection(sections.submissions)}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>`;\n}\n"],"mappings":"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","names":[]}
|