@reddoorla/maintenance 0.6.8 → 0.8.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/README.md CHANGED
@@ -1,19 +1,317 @@
1
- # @reddoorla/maintenance
1
+ # `@reddoorla/maintenance`
2
2
 
3
- Canonical maintenance configs, audits, and recipes for sites built on the reddoor starter.
3
+ Canonical maintenance configs, audits, and recipes for the reddoor SvelteKit + Prismic fleet.
4
4
 
5
- See `docs/specs/2026-05-20-package-design.md` for the design and `docs/superpowers/plans/` for active implementation plans.
6
-
7
- ## Install
5
+ A single CLI (`reddoor-maint`) that runs **audits** to inspect a site, **recipes** to mutate one (branch-isolated, idempotent, never on a dirty tree), and ships the **canonical configs** every reddoor site shares (eslint, prettier, lighthouse, playwright-a11y, svelte). Designed to run against either a single local site or a fleet declared in an inventory file.
8
6
 
9
7
  ```bash
10
8
  pnpm add -D @reddoorla/maintenance
9
+ pnpm reddoor-maint --help
10
+ ```
11
+
12
+ ---
13
+
14
+ ## The onboarding flow
15
+
16
+ A reddoor site goes through this sequence the first time you adopt the package. Each recipe is idempotent — running it again on an already-onboarded site is a `noop`.
17
+
18
+ ```text
19
+ convert-to-pnpm → onboard → sync-configs → svelte-codemods → audit
11
20
  ```
12
21
 
22
+ | Step | Recipe | What it does |
23
+ | ---- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | 1 | `convert-to-pnpm` | Removes `package-lock.json` / `yarn.lock`, pins `packageManager: pnpm@…`, rewrites `npm` references in scripts, runs `pnpm install` to materialise `pnpm-lock.yaml`. |
25
+ | 2 | `onboard` | Installs `@reddoorla/maintenance` + the audit deps (`@lhci/cli`, `@playwright/test`, `@axe-core/playwright`) on the site. Pins the maintenance dep to a caret range against this package's own version at runtime. |
26
+ | 3 | `sync-configs` | Writes the canonical config templates into the site (eslint, prettier, lighthouserc, playwright config, svelte config) and merges canonical entries into `.gitignore`. |
27
+ | 4 | `svelte-codemods` | Optional cleanup pass applying the Svelte 5 gotcha codemods (`export let` → `$props()`, `on:event` → `onevent`, `$:` → `$derived`/`$effect`, etc.) for sites that surface new strictness warnings after the original upgrade. |
28
+ | 5 | `audit` | Runs `deps`, `lighthouse`, `a11y`, `security`, `lint` — see [Audits](#audits). |
29
+
30
+ Each recipe refuses to run on a dirty working tree, creates a fresh `maint/<recipe>-<UTC-ms-timestamp>` branch, and emits one or more atomic commits.
31
+
32
+ ---
33
+
13
34
  ## CLI
14
35
 
36
+ ```text
37
+ reddoor-maint list-audits # audit descriptions
38
+ reddoor-maint list-recipes # recipe descriptions
39
+
40
+ reddoor-maint audit [site] # run audits
41
+ reddoor-maint sync-configs [site]
42
+ reddoor-maint bump-deps [site]
43
+ reddoor-maint convert-to-pnpm [site]
44
+ reddoor-maint onboard [site]
45
+ reddoor-maint svelte-codemods [site]
46
+ reddoor-maint upgrade svelte-4-to-5 [site]
47
+ ```
48
+
49
+ `[site]` defaults to `process.cwd()`. Add `--fleet path/to/inventory.json` (or `.mjs` / `.js`) to run across every site in an inventory instead. `--cwd <path>` overrides the working directory for any command.
50
+
51
+ ### Common flags
52
+
53
+ - `--only <names>` — comma-separated subset. Validates against the known set; typos exit with code 2.
54
+ - `audit`: `deps`, `lighthouse`, `a11y`, `security`, `lint`
55
+ - `sync-configs`: `eslint`, `prettier`, `lighthouse`, `playwright-a11y`, `svelte`, `gitignore`
56
+ - `--dry` (sync-configs) — print the planned diff without writing.
57
+ - `--group patch | minor | major` (bump-deps) — semver bucket. Default `minor`.
58
+ - `--audits lighthouse,a11y` (onboard) — which audit deps to ensure.
59
+ - `--verbose` — print full stack on errors instead of just the message.
60
+
61
+ ### Exit codes
62
+
63
+ - `0` — success (including `noop`)
64
+ - `1` — at least one audit failed, or a recipe returned `failed`
65
+ - `2` — invalid argument (e.g. unknown `--only` name, unknown `--group`)
66
+
67
+ ---
68
+
69
+ ## Recipes
70
+
71
+ Each recipe is `(site, opts?) => Promise<RecipeResult>` and is exported from the package entry as a library function too:
72
+
73
+ ```ts
74
+ import {
75
+ syncConfigs,
76
+ bumpDeps,
77
+ onboard,
78
+ convertToPnpm,
79
+ svelteCodemods,
80
+ upgradeSvelte4to5,
81
+ } from "@reddoorla/maintenance";
82
+ ```
83
+
84
+ Shared contract — every recipe:
85
+
86
+ - **Refuses to run on a dirty working tree.** Either throws (most recipes) or returns `{ status: "failed", notes: "…" }` (e.g. `onboard` when `pnpm-lock.yaml` is missing, `bump-deps` when a competing lockfile is present).
87
+ - **Creates a fresh branch** `maint/<recipe>-<UTC-millisecond-timestamp>` before mutating anything.
88
+ - **Emits atomic commits** — each logical change gets its own commit.
89
+ - **Is idempotent** — re-running on the already-applied state returns `{ status: "noop", commits: [] }` without creating a branch.
90
+ - **Returns a `RecipeResult`** — `{ recipe, site, status: "applied" | "noop" | "failed", commits: string[], notes?: string }`.
91
+
92
+ ### `sync-configs`
93
+
94
+ Writes the canonical config templates into the site (eslint, prettier, lighthouserc, playwright config, svelte config) and merges the canonical entries into `.gitignore`. Each config is its own commit. `--dry` reports the planned diff (including gitignore drift) without writing. `--only <name>[,<name>]` restricts to a subset.
95
+
96
+ ### `bump-deps`
97
+
98
+ Pre-flights that the site is on pnpm (refuses with a clear remediation if `package-lock.json` or `yarn.lock` is present without `pnpm-lock.yaml`), runs `pnpm install` to ensure the lockfile is current, then `pnpm outdated --json` to decide whether anything needs upgrading. If yes: creates a branch and runs `pnpm up` scoped to the requested `--group` (patch / minor / major), then commits the result. `noop` if nothing's out of date.
99
+
100
+ ### `convert-to-pnpm`
101
+
102
+ Removes the npm or yarn lockfile, pins `packageManager: pnpm@X.Y.Z` in `package.json`, rewrites `npm`/`npx`/`yarn` references in scripts to their pnpm equivalents, removes any leftover flat `node_modules` (to avoid phantom-dep contamination), then `pnpm install` to materialise `pnpm-lock.yaml`. Three to four commits depending on what's present. `noop` if `pnpm-lock.yaml` already exists.
103
+
104
+ ### `onboard`
105
+
106
+ Adds `@reddoorla/maintenance` + the audit deps (`@lhci/cli`, `@playwright/test`, `@axe-core/playwright`) to the site's `devDependencies` if they're missing. Audit dep versions come from `src/configs/baseline-versions.ts` so they can't drift from the rest of the package. The maintenance dep is pinned to a caret range against this package's own version at runtime — no manual syncing required at each minor bump. Refuses with `{ status: "failed", notes: "run convert-to-pnpm first" }` if the site has no `pnpm-lock.yaml`.
107
+
108
+ ### `svelte-codemods`
109
+
110
+ Standalone codemod pass for sites already on Svelte 5. Applies the same gotcha codemods the full `svelte-4-to-5` recipe runs (`export let` → `$props()`, `on:event` → `onevent`, `$:` → `$derived`/`$effect`, `$$props.class` rewrite, `$$restProps` → destructured `...rest`, `$state` + `$effect` → `$derived`). Useful when post-upgrade Svelte 5 surfaces new strictness warnings and the fleet needs a clean re-application.
111
+
112
+ ### `upgrade svelte-4-to-5`
113
+
114
+ The full 7-step Svelte 4 → 5 migration: bump framework versions, migrate `svelte.config.js`, run the official `svelte-migrate` codemod, run `@tailwindcss/upgrade`, apply gotcha codemods over `src/**/*.svelte`, verify with `pnpm install` + `pnpm run check`, and write a `MIGRATION_SVELTE_5.md` summary. Each step is its own commit; the file leaves a record of what ran and what may need manual review.
115
+
116
+ ---
117
+
118
+ ## Audits
119
+
120
+ Each audit is `(ctx) => Promise<AuditResult>` and is exported from the package entry. All audits return a closed-union status: `"pass" | "warn" | "fail" | "skip"`.
121
+
122
+ | Name | What it checks |
123
+ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
124
+ | `deps` | Diffs site's `package.json` against `src/configs/baseline-versions.ts`. Surfaces deps that drift from the canonical version map. |
125
+ | `lighthouse` | Runs `@lhci/cli autorun` using the canonical `lighthouserc.json`. |
126
+ | `a11y` | Spawns Playwright + `@axe-core/playwright` against a canonical set of a11y routes. |
127
+ | `security` | `pnpm audit --json --prod` with automatic fall-through to `npm audit` when pnpm can't run (missing lockfile, error envelope, etc.). Normalises advisory shapes from both tools into a single `AdvisoryEntry[]`. |
128
+ | `lint` | ESLint + Prettier using the canonical configs (re-exported via `@reddoorla/maintenance/configs/eslint` and `@reddoorla/maintenance/configs/prettier`). |
129
+
15
130
  ```bash
16
- pnpm reddoor-maint --help
131
+ reddoor-maint audit # all five against cwd
132
+ reddoor-maint audit --only security,a11y # a subset
133
+ reddoor-maint audit --json # machine-readable output
134
+ reddoor-maint audit --fleet inventory.json # batch across an inventory
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Fleet mode
140
+
141
+ Pass `--fleet <path>` to run a command against multiple sites declared in an inventory file. Inventory files can be:
142
+
143
+ - **`.json`** — an array of site objects (most common).
144
+ - **`.mjs`** / **`.js`** — an ES module whose default export is `() => Promise<Site[]>` (useful when site list comes from an API).
145
+
146
+ ### Inventory file format (`.json`)
147
+
148
+ ```json
149
+ [
150
+ {
151
+ "path": "/Users/me/Documents/GitHub/caltex-landing",
152
+ "name": "caltex-landing",
153
+ "repoUrl": "https://github.com/redacted/caltex-landing.git"
154
+ },
155
+ {
156
+ "path": "/Users/me/Documents/GitHub/espada",
157
+ "name": "espada"
158
+ }
159
+ ]
160
+ ```
161
+
162
+ | Field | Required | Notes |
163
+ | --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
164
+ | `path` | yes | Must be **absolute**. Relative paths are rejected so the invocation's cwd can't accidentally change which site is targeted. |
165
+ | `name` | no | Friendly label for log output. Falls back to `path` when omitted. |
166
+ | `repoUrl` | no | When set, recipes that run with `--fleet --workdir <dir>` will `git clone` the site into `<workdir>/<name>` if it isn't already there. URL scheme is validated against an allowlist (`https://`, `http://`, `ssh://`, `git://`, `file://`, scp-style `user@host:path`) and `git clone` is invoked with `--` to neutralise argv-injection. |
167
+ | `meta` | no | Free-form object preserved on the `Site` for downstream consumers. |
168
+
169
+ `--workdir <path>` selects the clone target (default `~/.reddoor-maint/sites`). Without `--workdir`, fleet sites are expected to already exist at their declared `path`.
170
+
171
+ ---
172
+
173
+ ## Library usage
174
+
175
+ The package's main entry exports every recipe and audit so you can wire them into custom tooling (CI jobs, scheduled scripts, alternative CLIs):
176
+
177
+ ```ts
178
+ import {
179
+ // recipes
180
+ syncConfigs,
181
+ bumpDeps,
182
+ convertToPnpm,
183
+ onboard,
184
+ svelteCodemods,
185
+ upgradeSvelte4to5,
186
+
187
+ // audits
188
+ runAudits,
189
+ ALL_AUDIT_NAMES,
190
+
191
+ // recipe registry
192
+ ALL_RECIPE_NAMES,
193
+ isRecipeName,
194
+ } from "@reddoorla/maintenance";
195
+
196
+ const result = await syncConfigs({ path: "/abs/path/to/site" });
197
+ if (result.status === "applied") {
198
+ console.log(`applied ${result.commits.length} commits on branch ${result.notes}`);
199
+ }
200
+ ```
201
+
202
+ The canonical configs are also importable as their own subpath exports — sites use these in their own root configs:
203
+
204
+ ```js
205
+ // eslint.config.js
206
+ import { createEslintConfig } from "@reddoorla/maintenance/configs/eslint";
207
+ import svelteConfig from "./svelte.config.js";
208
+ export default createEslintConfig({ svelteConfig });
209
+ ```
210
+
211
+ ```js
212
+ // svelte.config.js
213
+ import { createSvelteConfig } from "@reddoorla/maintenance/configs/svelte";
214
+ export default createSvelteConfig();
215
+ ```
216
+
217
+ ```js
218
+ // playwright.config.ts
219
+ export { default } from "@reddoorla/maintenance/configs/playwright-a11y";
220
+ ```
221
+
222
+ ---
223
+
224
+ ## Troubleshooting
225
+
226
+ **`--only <name>` and nothing happened?**
227
+ Make sure you spelled the name correctly. The CLI validates against the known set and exits with code 2 on a typo — but if you're invoking the recipe programmatically and passed an invalid name, you'll just see a `noop`. `reddoor-maint list-audits` and `reddoor-maint list-recipes` print the canonical names.
228
+
229
+ **Recipe refused to run with `"working tree is not clean"`?**
230
+ Recipes refuse to mutate on top of uncommitted work. Either commit/stash your changes, or run on a clean checkout.
231
+
232
+ **`bump-deps` returned `failed: site has package-lock.json but no pnpm-lock.yaml`?**
233
+ Run `reddoor-maint convert-to-pnpm` first. `bump-deps` is pnpm-only.
234
+
235
+ **`onboard` returned `failed: no pnpm-lock.yaml`?**
236
+ Same — run `reddoor-maint convert-to-pnpm` first. Onboarding is intentionally split from package-manager conversion so the two transitions are reviewable separately.
237
+
238
+ **`security` audit returned `skip`?**
239
+ Neither `pnpm audit` nor `npm audit` produced a parseable result. Check that at least one of pnpm or npm is on `PATH` and that the site has a corresponding lockfile.
240
+
241
+ **Where do the canonical baseline versions come from?**
242
+ [`src/configs/baseline-versions.ts`](src/configs/baseline-versions.ts). The `deps` audit compares against this map, and `onboard`'s audit deps source their pins from here too — so any version bump in baseline-versions automatically flows through to fresh onboards.
243
+
244
+ ---
245
+
246
+ ## Reports (per-site maintenance/testing emails)
247
+
248
+ Generates the monthly/quarterly/yearly client-facing email reports — Lighthouse scores + GA users + the standard checklist + per-client header image — from Airtable as the source of truth, delivered via [Resend](https://resend.com/).
249
+
250
+ ### Required env
251
+
252
+ ```bash
253
+ AIRTABLE_PAT=patXXXX # Airtable PAT: schema.bases:read, data.records:read+write
254
+ AIRTABLE_BASE_ID=appHG8nLOzULzXOER
255
+ RESEND_API_KEY=re_XXXX
256
+ RESEND_WEBHOOK_SECRET=whsec_XXXX # only for the deployed webhook
17
257
  ```
18
258
 
19
- (Status: 0.x — under construction.)
259
+ ### Operator flow
260
+
261
+ 0. **Prereq: refresh Lighthouse scores on each Websites row.** From each site's checkout:
262
+
263
+ ```bash
264
+ reddoor-maint audit lighthouse --write-airtable
265
+ ```
266
+
267
+ This runs Lighthouse and writes the 4 scores directly to the matching Websites row (slug auto-derived from `package.json#name`; pass `--write-airtable=<slug>` to override), along with a `Last lighthouse audit at` timestamp. The report orchestrator copies these into the new Reports row — drafting a report for a site missing scores fails with a clear error.
268
+
269
+ 1. **Draft overdue reports**
270
+
271
+ ```bash
272
+ reddoor-maint report --due
273
+ ```
274
+
275
+ Scans Websites where `maintenence freq` ≠ `None`, finds (site, type) pairs whose next-due date has passed, creates Reports rows with snapshotted scores + attaches the rendered HTML preview.
276
+
277
+ 2. **Preview a single site without touching Airtable**
278
+
279
+ ```bash
280
+ reddoor-maint report <slug> --preview
281
+ ```
282
+
283
+ Writes `reports/<slug>/draft.html` locally — open in a browser to verify before any side effects. (The header image renders broken in the browser because the CID can only resolve inside an email client; that's expected for a preview.)
284
+
285
+ 3. **Review on Airtable mobile**
286
+ - Tap the `Rendered HTML` attachment on the Reports row to preview in Safari.
287
+ - Fill in `GA users (period)` and `GA users (prev period)`.
288
+ - Optionally add a `Commentary` line; optionally override the subject.
289
+ - Flip `Approved to send`.
290
+
291
+ 4. **Send approved reports**
292
+
293
+ ```bash
294
+ reddoor-maint report --send-ready
295
+ ```
296
+
297
+ Renders + sends every Reports row with `Draft ready=true && Approved to send=true && Sent at IS NULL`. Stamps `Sent at` + `Delivery status=pending` on each.
298
+
299
+ 5. **Delivery status updates automatically** via the Resend webhook (Netlify Function at `netlify/functions/resend-webhook.mts`) — `Delivery status` flips to `delivered` / `bounced` / `complained` as events arrive.
300
+
301
+ ### Frequency math
302
+
303
+ Per (site, type): `dueDate = max(last Sent at for this type, Websites.maintenance day fallback) + frequency months`. A site with no Reports row AND no fallback day is due immediately.
304
+
305
+ ### Header images
306
+
307
+ Each Website row's `Header image` attachment is fetched at send time and embedded inline via CID (Content-ID) in the email — no CDN, no link rot, ~100 KB per send.
308
+
309
+ ---
310
+
311
+ ## Versioning
312
+
313
+ Patch / minor / major bumps follow [Changesets](https://github.com/changesets/changesets). The release workflow opens a version-bump PR on every merge to `main` with pending changesets; merging that PR triggers the publish step. Releases are signed via npm OIDC trusted publishing — no long-lived `NPM_TOKEN` lives in CI.
314
+
315
+ ## License
316
+
317
+ MIT © Tucker Lemos