@reddoorla/maintenance 0.1.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.
@@ -0,0 +1,1408 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/bin.ts
4
+ import { readFileSync } from "fs";
5
+ import { dirname, join as join14 } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { cac } from "cac";
8
+
9
+ // src/cli/commands/audit.ts
10
+ import { resolve as resolve2 } from "path";
11
+
12
+ // src/audits/util/spawn.ts
13
+ import { spawn } from "child_process";
14
+ var defaultSpawn = (cmd, args, opts = {}) => new Promise((resolve6, reject) => {
15
+ const child = spawn(cmd, [...args], {
16
+ cwd: opts.cwd,
17
+ env: opts.env ?? process.env,
18
+ stdio: ["ignore", "pipe", "pipe"]
19
+ });
20
+ let stdout = "";
21
+ let stderr = "";
22
+ child.stdout.on("data", (chunk) => stdout += String(chunk));
23
+ child.stderr.on("data", (chunk) => stderr += String(chunk));
24
+ const timer = opts.timeoutMs ? setTimeout(() => {
25
+ child.kill("SIGTERM");
26
+ reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));
27
+ }, opts.timeoutMs) : void 0;
28
+ child.on("error", (err) => {
29
+ if (timer) clearTimeout(timer);
30
+ reject(err);
31
+ });
32
+ child.on("close", (code) => {
33
+ if (timer) clearTimeout(timer);
34
+ resolve6({ code: code ?? -1, stdout, stderr });
35
+ });
36
+ });
37
+
38
+ // src/audits/deps.ts
39
+ import { readFile } from "fs/promises";
40
+ import { join } from "path";
41
+
42
+ // src/configs/baseline-versions.ts
43
+ var baselineVersions = {
44
+ // SvelteKit core
45
+ svelte: "^5.55.5",
46
+ "@sveltejs/kit": "^2.59.0",
47
+ "@sveltejs/adapter-netlify": "^6.0.4",
48
+ "@sveltejs/adapter-auto": "^7.0.0",
49
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
50
+ "svelte-check": "^4.4.7",
51
+ // Build tooling
52
+ vite: "^8.0.10",
53
+ vitest: "^4.1.1",
54
+ typescript: "^6.0.3",
55
+ // Tailwind 4
56
+ tailwindcss: "^4.0.14",
57
+ "@tailwindcss/vite": "^4.3.0",
58
+ // Prismic
59
+ "@prismicio/client": "^7.3.1",
60
+ "@prismicio/svelte": "^2.0.0",
61
+ "@slicemachine/adapter-sveltekit": "^0.3.36",
62
+ "slice-machine-ui": "^2.11.1",
63
+ // Test tooling
64
+ "@playwright/test": "^1.59.1",
65
+ "@axe-core/playwright": "^4.11.3",
66
+ "@lhci/cli": "^0.15.1",
67
+ // Lint
68
+ eslint: "^10.3.0",
69
+ "eslint-plugin-svelte": "^3.1.0",
70
+ "eslint-config-prettier": "^10.1.1",
71
+ prettier: "^3.1.1",
72
+ "prettier-plugin-svelte": "^3.2.6",
73
+ "typescript-eslint": "^8.59.1",
74
+ "@eslint/js": "^10.0.1",
75
+ globals: "^17.6.0",
76
+ // Misc
77
+ "@lucide/svelte": "^1.14.0",
78
+ "@zerodevx/svelte-img": "^2.1.2"
79
+ };
80
+
81
+ // src/audits/deps.ts
82
+ function siteLabel(site) {
83
+ return site.name ?? site.path;
84
+ }
85
+ function stripCaret(range) {
86
+ return range.replace(/^[\^~]/, "");
87
+ }
88
+ function parseSemver(v) {
89
+ const cleaned = stripCaret(v).split("-")[0] ?? "0.0.0";
90
+ const parts = cleaned.split(".").map((n) => Number.parseInt(n, 10));
91
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
92
+ }
93
+ function compareSemver(actual, baseline) {
94
+ const [aMajor, aMinor, aPatch] = parseSemver(actual);
95
+ const [bMajor, bMinor, bPatch] = parseSemver(baseline);
96
+ if (aMajor > bMajor) return "newer";
97
+ if (aMajor < bMajor) return "major";
98
+ if (aMinor > bMinor) return "newer";
99
+ if (aMinor < bMinor) return "minor";
100
+ if (aPatch > bPatch) return "newer";
101
+ if (aPatch < bPatch) return "patch";
102
+ return "same";
103
+ }
104
+ async function depsAudit(ctx) {
105
+ const pkgPath = join(ctx.site.path, "package.json");
106
+ let pkgRaw;
107
+ try {
108
+ pkgRaw = await readFile(pkgPath, "utf-8");
109
+ } catch (err) {
110
+ return {
111
+ audit: "deps",
112
+ site: siteLabel(ctx.site),
113
+ status: "skip",
114
+ summary: `no package.json at ${pkgPath}`,
115
+ details: { error: String(err) }
116
+ };
117
+ }
118
+ const pkg2 = JSON.parse(pkgRaw);
119
+ const installed = {
120
+ ...pkg2.dependencies ?? {},
121
+ ...pkg2.devDependencies ?? {}
122
+ };
123
+ const details = [];
124
+ for (const [name, baseline] of Object.entries(baselineVersions)) {
125
+ const actual = installed[name];
126
+ if (!actual) continue;
127
+ details.push({
128
+ pkg: name,
129
+ baseline,
130
+ actual,
131
+ drift: compareSemver(actual, baseline)
132
+ });
133
+ }
134
+ const anyMajor = details.some((d) => d.drift === "major");
135
+ const anyMinor = details.some((d) => d.drift === "minor");
136
+ const anyNewer = details.some((d) => d.drift === "newer");
137
+ const status = anyMajor ? "fail" : anyMinor || anyNewer ? "warn" : "pass";
138
+ const summary = status === "pass" ? `all ${details.length} tracked deps in line with baseline` : status === "warn" ? `${details.filter((d) => d.drift !== "same").length} of ${details.length} tracked deps drifted` : `${details.filter((d) => d.drift === "major").length} deps lagging by a major version`;
139
+ return {
140
+ audit: "deps",
141
+ site: siteLabel(ctx.site),
142
+ status,
143
+ summary,
144
+ details
145
+ };
146
+ }
147
+
148
+ // src/audits/lint.ts
149
+ import { existsSync } from "fs";
150
+ import { readFile as readFile2 } from "fs/promises";
151
+ import { join as join2, relative } from "path";
152
+ import { ESLint } from "eslint";
153
+ import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "prettier";
154
+ import { glob } from "tinyglobby";
155
+ var TARGET_GLOBS = ["**/*.{ts,js,svelte}"];
156
+ var IGNORE = ["node_modules/**", "dist/**", ".svelte-kit/**", "build/**", ".netlify/**"];
157
+ function siteLabel2(site) {
158
+ return site.name ?? site.path;
159
+ }
160
+ async function listFiles(cwd) {
161
+ return glob(TARGET_GLOBS, { cwd, ignore: IGNORE, absolute: false });
162
+ }
163
+ async function lintAudit(ctx) {
164
+ const { site } = ctx;
165
+ const configPath = join2(site.path, "eslint.config.js");
166
+ if (!existsSync(configPath)) {
167
+ return {
168
+ audit: "lint",
169
+ site: siteLabel2(site),
170
+ status: "skip",
171
+ summary: "no eslint config at site root"
172
+ };
173
+ }
174
+ const eslint2 = new ESLint({
175
+ cwd: site.path,
176
+ overrideConfigFile: configPath,
177
+ errorOnUnmatchedPattern: false
178
+ });
179
+ const relFiles = await listFiles(site.path);
180
+ const filesToLint = relFiles.map((f) => join2(site.path, f));
181
+ const eslintResults = await eslint2.lintFiles(filesToLint);
182
+ const eslintErrors = eslintResults.reduce((n, r) => n + r.errorCount, 0);
183
+ const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);
184
+ const prettierUnformatted = [];
185
+ for (const file of filesToLint) {
186
+ const source = await readFile2(file, "utf-8");
187
+ const options = await prettierResolveConfig(file) ?? {};
188
+ const ok = await prettierCheck(source, { ...options, filepath: file });
189
+ if (!ok) prettierUnformatted.push(relative(site.path, file));
190
+ }
191
+ const status = eslintErrors > 0 || prettierUnformatted.length > 0 ? "fail" : eslintWarnings > 0 ? "warn" : "pass";
192
+ const summary = status === "pass" ? `lint clean across ${filesToLint.length} files` : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;
193
+ return {
194
+ audit: "lint",
195
+ site: siteLabel2(site),
196
+ status,
197
+ summary,
198
+ details: {
199
+ eslintErrors,
200
+ eslintWarnings,
201
+ prettierUnformatted,
202
+ files: filesToLint.length
203
+ }
204
+ };
205
+ }
206
+
207
+ // src/audits/security.ts
208
+ function siteLabel3(site) {
209
+ return site.name ?? site.path;
210
+ }
211
+ function classify(v) {
212
+ if (v.critical > 0 || v.high > 0) return "fail";
213
+ if (v.moderate > 0 || v.low > 0) return "warn";
214
+ return "pass";
215
+ }
216
+ async function tryRun(spawn2, cmd, args, cwd) {
217
+ try {
218
+ return await spawn2(cmd, args, { cwd });
219
+ } catch (err) {
220
+ const e = err;
221
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) return { missing: true };
222
+ throw err;
223
+ }
224
+ }
225
+ async function securityAudit(ctx) {
226
+ const spawn2 = ctx.spawn ?? defaultSpawn;
227
+ const site = ctx.site;
228
+ const label = siteLabel3(site);
229
+ let used = "pnpm audit";
230
+ let raw = await tryRun(
231
+ spawn2,
232
+ "pnpm",
233
+ ["audit", "--json", "--prod"],
234
+ site.path
235
+ );
236
+ if ("missing" in raw) {
237
+ used = "npm audit";
238
+ raw = await tryRun(spawn2, "npm", ["audit", "--json", "--omit=dev"], site.path);
239
+ }
240
+ if ("missing" in raw) {
241
+ return {
242
+ audit: "security",
243
+ site: label,
244
+ status: "skip",
245
+ summary: "neither pnpm nor npm is available on PATH"
246
+ };
247
+ }
248
+ if (raw.code !== 0 && raw.code !== 1) {
249
+ return {
250
+ audit: "security",
251
+ site: label,
252
+ status: "skip",
253
+ summary: `${used} exited with code ${raw.code}`,
254
+ details: { stderr: raw.stderr }
255
+ };
256
+ }
257
+ let parsed;
258
+ try {
259
+ parsed = JSON.parse(raw.stdout);
260
+ } catch (err) {
261
+ return {
262
+ audit: "security",
263
+ site: label,
264
+ status: "skip",
265
+ summary: `${used} produced unparseable JSON`,
266
+ details: { error: String(err), stdout: raw.stdout.slice(0, 500) }
267
+ };
268
+ }
269
+ const vuln = {
270
+ low: parsed.metadata?.vulnerabilities?.low ?? 0,
271
+ moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,
272
+ high: parsed.metadata?.vulnerabilities?.high ?? 0,
273
+ critical: parsed.metadata?.vulnerabilities?.critical ?? 0
274
+ };
275
+ const status = classify(vuln);
276
+ const total = vuln.low + vuln.moderate + vuln.high + vuln.critical;
277
+ const summary = status === "pass" ? `${used}: 0 vulnerabilities` : `${used}: ${total} vulnerabilities (${vuln.critical}C/${vuln.high}H/${vuln.moderate}M/${vuln.low}L)`;
278
+ return {
279
+ audit: "security",
280
+ site: label,
281
+ status,
282
+ summary,
283
+ details: vuln
284
+ };
285
+ }
286
+
287
+ // src/audits/lighthouse.ts
288
+ import { writeFile, mkdtemp, rm } from "fs/promises";
289
+ import { tmpdir } from "os";
290
+ import { join as join3 } from "path";
291
+
292
+ // src/configs/lighthouse.ts
293
+ var lighthouseConfig = {
294
+ ci: {
295
+ collect: {
296
+ url: ["http://localhost:5173/dev/a11y-fixtures"],
297
+ startServerCommand: "pnpm vite:dev",
298
+ startServerReadyPattern: "ready in",
299
+ startServerReadyTimeout: 12e4,
300
+ numberOfRuns: 1,
301
+ settings: {
302
+ preset: "desktop",
303
+ skipAudits: ["uses-http2"]
304
+ }
305
+ },
306
+ assert: {
307
+ assertions: {
308
+ "categories:accessibility": ["error", { minScore: 0.95 }],
309
+ "categories:best-practices": ["error", { minScore: 0.9 }],
310
+ "categories:seo": ["error", { minScore: 0.9 }],
311
+ "categories:performance": ["warn", { minScore: 0.7 }]
312
+ }
313
+ },
314
+ upload: {
315
+ target: "temporary-public-storage"
316
+ }
317
+ }
318
+ };
319
+
320
+ // src/audits/lighthouse.ts
321
+ function siteLabel4(site) {
322
+ return site.name ?? site.path;
323
+ }
324
+ function isFakeShape(stdout) {
325
+ try {
326
+ const parsed = JSON.parse(stdout);
327
+ if (typeof parsed.assertionsFailed === "number" && parsed.summary) return parsed;
328
+ } catch {
329
+ return null;
330
+ }
331
+ return null;
332
+ }
333
+ async function lighthouseAudit(ctx) {
334
+ const spawn2 = ctx.spawn ?? defaultSpawn;
335
+ const site = ctx.site;
336
+ const label = siteLabel4(site);
337
+ const dir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
338
+ const configPath = join3(dir, "lighthouserc.json");
339
+ await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
340
+ let raw;
341
+ try {
342
+ raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
343
+ cwd: site.path
344
+ });
345
+ } catch (err) {
346
+ await rm(dir, { recursive: true, force: true });
347
+ const e = err;
348
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
349
+ return {
350
+ audit: "lighthouse",
351
+ site: label,
352
+ status: "skip",
353
+ summary: "npx/@lhci/cli not available"
354
+ };
355
+ }
356
+ throw err;
357
+ }
358
+ await rm(dir, { recursive: true, force: true });
359
+ const fake = isFakeShape(raw.stdout);
360
+ const normalized = fake ?? {
361
+ summary: {},
362
+ assertionsFailed: raw.code === 0 ? 0 : 1,
363
+ assertions: raw.code === 0 ? [] : [{ category: "unknown", level: "error", message: raw.stderr.slice(0, 200) }]
364
+ };
365
+ const anyError = (normalized.assertions ?? []).some((a) => a.level === "error");
366
+ const anyWarn = (normalized.assertions ?? []).some((a) => a.level === "warn");
367
+ const status = anyError ? "fail" : anyWarn ? "warn" : "pass";
368
+ const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${normalized.assertionsFailed} assertion(s) failed`;
369
+ return {
370
+ audit: "lighthouse",
371
+ site: label,
372
+ status,
373
+ summary,
374
+ details: normalized
375
+ };
376
+ }
377
+
378
+ // src/audits/a11y.ts
379
+ import { writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
380
+ import { tmpdir as tmpdir2 } from "os";
381
+ import { join as join4 } from "path";
382
+
383
+ // src/configs/playwright-a11y.ts
384
+ import { defineConfig, devices } from "@playwright/test";
385
+ var a11yRoutes = [
386
+ { path: "/dev/a11y-fixtures", name: "a11y fixtures" },
387
+ { path: "/dev/animate-in", name: "animate-in demo" }
388
+ ];
389
+ var playwrightA11yConfig = defineConfig({
390
+ testDir: "tests",
391
+ testMatch: /.*\.spec\.ts$/,
392
+ fullyParallel: true,
393
+ forbidOnly: !!process.env.CI,
394
+ retries: process.env.CI ? 2 : 0,
395
+ reporter: process.env.CI ? "github" : "list",
396
+ use: {
397
+ baseURL: "http://localhost:5173",
398
+ trace: "on-first-retry"
399
+ },
400
+ projects: [
401
+ {
402
+ name: "chromium",
403
+ use: { ...devices["Desktop Chrome"] }
404
+ }
405
+ ],
406
+ webServer: {
407
+ command: "pnpm vite:dev",
408
+ url: "http://localhost:5173/dev/a11y-fixtures",
409
+ reuseExistingServer: !process.env.CI,
410
+ timeout: 12e4
411
+ }
412
+ });
413
+
414
+ // src/audits/a11y.ts
415
+ function siteLabel5(site) {
416
+ return site.name ?? site.path;
417
+ }
418
+ function isFakeShape2(stdout) {
419
+ try {
420
+ const parsed = JSON.parse(stdout);
421
+ if (typeof parsed.totalViolations === "number" && parsed.byImpact) return parsed;
422
+ } catch {
423
+ return null;
424
+ }
425
+ return null;
426
+ }
427
+ function buildSpec() {
428
+ return `
429
+ import { test, expect } from "@playwright/test";
430
+ import AxeBuilder from "@axe-core/playwright";
431
+ const pages = ${JSON.stringify(a11yRoutes)};
432
+ for (const { path, name } of pages) {
433
+ test(\`\${name} has no axe violations\`, async ({ page }) => {
434
+ await page.goto(path);
435
+ const results = await new AxeBuilder({ page })
436
+ .withTags(["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22aa"])
437
+ .analyze();
438
+ expect(results.violations).toEqual([]);
439
+ });
440
+ }
441
+ `;
442
+ }
443
+ async function a11yAudit(ctx) {
444
+ const spawn2 = ctx.spawn ?? defaultSpawn;
445
+ const site = ctx.site;
446
+ const label = siteLabel5(site);
447
+ const dir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-"));
448
+ const specPath = join4(dir, "a11y.spec.ts");
449
+ await writeFile2(specPath, buildSpec(), "utf-8");
450
+ let raw;
451
+ try {
452
+ raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=json", specPath], {
453
+ cwd: site.path
454
+ });
455
+ } catch (err) {
456
+ await rm2(dir, { recursive: true, force: true });
457
+ const e = err;
458
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
459
+ return {
460
+ audit: "a11y",
461
+ site: label,
462
+ status: "skip",
463
+ summary: "npx/playwright not available"
464
+ };
465
+ }
466
+ throw err;
467
+ }
468
+ await rm2(dir, { recursive: true, force: true });
469
+ const fake = isFakeShape2(raw.stdout);
470
+ const normalized = fake ?? {
471
+ totalViolations: raw.code === 0 ? 0 : 1,
472
+ byImpact: raw.code === 0 ? {} : { moderate: 1 }
473
+ };
474
+ const hasSerious = (normalized.byImpact.serious ?? 0) > 0 || (normalized.byImpact.critical ?? 0) > 0;
475
+ const hasAny = normalized.totalViolations > 0;
476
+ const status = hasSerious ? "fail" : hasAny ? "warn" : "pass";
477
+ const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes` : `a11y: ${normalized.totalViolations} violations`;
478
+ return {
479
+ audit: "a11y",
480
+ site: label,
481
+ status,
482
+ summary,
483
+ details: normalized
484
+ };
485
+ }
486
+
487
+ // src/audits/index.ts
488
+ var REGISTRY = {
489
+ deps: depsAudit,
490
+ lint: lintAudit,
491
+ security: securityAudit,
492
+ lighthouse: lighthouseAudit,
493
+ a11y: a11yAudit
494
+ };
495
+ var ALL_AUDIT_NAMES = Object.keys(REGISTRY);
496
+ var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
497
+ function timedSpawn(timeoutMs) {
498
+ return (cmd, args, opts = {}) => defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });
499
+ }
500
+ async function runAudits(site, which) {
501
+ const names = which ?? ALL_AUDIT_NAMES;
502
+ for (const n of names) {
503
+ if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);
504
+ }
505
+ const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
506
+ const label = site.name ?? site.path;
507
+ return Promise.all(
508
+ names.map(
509
+ (n) => REGISTRY[n]({ site, spawn: spawn2 }).catch(
510
+ (err) => ({
511
+ audit: n,
512
+ site: label,
513
+ status: "fail",
514
+ summary: `${n}: unexpected error \u2014 ${String(err)}`
515
+ })
516
+ )
517
+ )
518
+ );
519
+ }
520
+
521
+ // src/cli/fleet/resolve-sites.ts
522
+ import { pathToFileURL } from "url";
523
+ import { resolve, extname } from "path";
524
+
525
+ // src/inventory/local.ts
526
+ import { basename } from "path";
527
+ function localPath(path, opts = {}) {
528
+ const site = { path, name: opts.name ?? basename(path) };
529
+ return async () => [site];
530
+ }
531
+
532
+ // src/inventory/json.ts
533
+ import { readFile as readFile3 } from "fs/promises";
534
+ function validate(raw) {
535
+ if (!Array.isArray(raw)) {
536
+ throw new Error("inventory JSON must be an array of sites");
537
+ }
538
+ return raw.map((entry, i) => {
539
+ if (!entry || typeof entry !== "object") {
540
+ throw new Error(`inventory entry ${i} is not an object`);
541
+ }
542
+ const e = entry;
543
+ if (typeof e.path !== "string" || e.path.length === 0) {
544
+ throw new Error(`inventory entry ${i} is missing required field: path`);
545
+ }
546
+ const site = { path: e.path };
547
+ if (typeof e.name === "string") site.name = e.name;
548
+ if (typeof e.repoUrl === "string") site.repoUrl = e.repoUrl;
549
+ if (typeof e.meta === "object" && e.meta !== null) {
550
+ site.meta = e.meta;
551
+ }
552
+ return site;
553
+ });
554
+ }
555
+ function fromJsonFile(path) {
556
+ return async () => {
557
+ const raw = JSON.parse(await readFile3(path, "utf-8"));
558
+ return validate(raw);
559
+ };
560
+ }
561
+
562
+ // src/cli/fleet/resolve-sites.ts
563
+ async function resolveSites(input) {
564
+ if (input.site && input.fleet) {
565
+ throw Object.assign(new Error("cannot combine a positional [site] with --fleet"), {
566
+ exitCode: 2
567
+ });
568
+ }
569
+ if (input.fleet) {
570
+ const fleetPath = resolve(input.cwd, input.fleet);
571
+ const ext = extname(fleetPath).toLowerCase();
572
+ let provider;
573
+ if (ext === ".json") {
574
+ provider = fromJsonFile(fleetPath);
575
+ } else if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
576
+ const mod = await import(pathToFileURL(fleetPath).href);
577
+ if (!mod.default || typeof mod.default !== "function") {
578
+ throw Object.assign(new Error(`--fleet ${input.fleet}: default export is not a function`), {
579
+ exitCode: 2
580
+ });
581
+ }
582
+ provider = mod.default;
583
+ } else {
584
+ throw Object.assign(
585
+ new Error(`--fleet ${input.fleet}: unsupported extension ${ext || "(none)"}`),
586
+ { exitCode: 2 }
587
+ );
588
+ }
589
+ return provider();
590
+ }
591
+ return localPath(resolve(input.cwd, input.site ?? input.cwd))();
592
+ }
593
+
594
+ // src/cli/fleet/clone-if-needed.ts
595
+ import { stat, readdir, mkdir } from "fs/promises";
596
+ import { join as join5 } from "path";
597
+ function deriveNameFromRepoUrl(repoUrl) {
598
+ const slash = repoUrl.split("/").pop() ?? repoUrl;
599
+ return slash.replace(/\.git$/, "");
600
+ }
601
+ async function isNonEmptyDir(path) {
602
+ try {
603
+ const s = await stat(path);
604
+ if (!s.isDirectory()) return false;
605
+ const entries = await readdir(path);
606
+ return entries.length > 0;
607
+ } catch {
608
+ return false;
609
+ }
610
+ }
611
+ async function cloneIfNeeded(site, opts) {
612
+ if (await isNonEmptyDir(site.path)) return site;
613
+ if (!site.repoUrl) {
614
+ throw new Error(`site path does not exist (${site.path}) and no repoUrl is set \u2014 cannot clone`);
615
+ }
616
+ const name = site.name ?? deriveNameFromRepoUrl(site.repoUrl);
617
+ const target = join5(opts.workdir, name);
618
+ await mkdir(opts.workdir, { recursive: true });
619
+ if (await isNonEmptyDir(target)) {
620
+ return { ...site, name, path: target };
621
+ }
622
+ const spawn2 = opts.spawn ?? defaultSpawn;
623
+ const result = await spawn2("git", ["clone", site.repoUrl, target], {
624
+ cwd: opts.workdir,
625
+ timeoutMs: 5 * 6e4
626
+ });
627
+ if (result.code !== 0) {
628
+ throw new Error(`git clone failed (code ${result.code}): ${result.stderr}`);
629
+ }
630
+ return { ...site, name, path: target };
631
+ }
632
+
633
+ // src/cli/commands/audit.ts
634
+ function parseOnly(value) {
635
+ if (!value) return void 0;
636
+ const names = value.split(",").map((s) => s.trim());
637
+ for (const n of names) {
638
+ if (!ALL_AUDIT_NAMES.includes(n)) {
639
+ throw Object.assign(new Error(`unknown audit in --only: ${n}`), { exitCode: 2 });
640
+ }
641
+ }
642
+ return names;
643
+ }
644
+ function formatTable(results) {
645
+ return results.map((r) => `${r.audit.padEnd(12)} ${r.status.padEnd(5)} ${r.site}
646
+ ${r.summary}`).join("\n");
647
+ }
648
+ function exitCode(results) {
649
+ return results.some((r) => r.status === "fail") ? 1 : 0;
650
+ }
651
+ async function runAuditCommand(site, opts) {
652
+ const which = parseOnly(opts.only);
653
+ const cwd = opts.cwd ? resolve2(opts.cwd) : process.cwd();
654
+ let sites = await resolveSites({
655
+ ...site !== void 0 ? { site } : {},
656
+ ...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
657
+ cwd
658
+ });
659
+ if (opts.fleet) {
660
+ const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
661
+ sites = await Promise.all(sites.map((s) => cloneIfNeeded(s, { workdir })));
662
+ }
663
+ const results = [];
664
+ for (const s of sites) {
665
+ const r = await runAudits(s, which);
666
+ results.push(...r);
667
+ }
668
+ const output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
669
+ return { output, code: exitCode(results) };
670
+ }
671
+
672
+ // src/cli/commands/sync-configs.ts
673
+ import { readFile as readFile5 } from "fs/promises";
674
+ import { join as join7, resolve as resolve3 } from "path";
675
+
676
+ // src/recipes/sync-configs.ts
677
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
678
+ import { join as join6 } from "path";
679
+
680
+ // src/recipes/sync-configs/templates.ts
681
+ var eslint = {
682
+ config: "eslint",
683
+ path: "eslint.config.js",
684
+ contents: `import { createEslintConfig } from "@reddoorla/maintenance/configs/eslint";
685
+ import svelteConfig from "./svelte.config.js";
686
+
687
+ export default createEslintConfig({ svelteConfig });
688
+ `
689
+ };
690
+ var prettier = {
691
+ config: "prettier",
692
+ path: ".prettierrc.json",
693
+ contents: `{
694
+ "trailingComma": "all",
695
+ "singleQuote": false,
696
+ "printWidth": 100,
697
+ "plugins": ["prettier-plugin-svelte"]
698
+ }
699
+ `
700
+ };
701
+ var lighthouse = {
702
+ config: "lighthouse",
703
+ path: "lighthouserc.json",
704
+ contents: `${JSON.stringify(
705
+ {
706
+ $note: "Generated by @reddoorla/maintenance sync-configs; edit src/configs/lighthouse.ts in the package instead.",
707
+ extends: "@reddoorla/maintenance/configs/lighthouse"
708
+ },
709
+ null,
710
+ 2
711
+ )}
712
+ `
713
+ };
714
+ var playwrightA11y = {
715
+ config: "playwright-a11y",
716
+ path: "playwright.config.ts",
717
+ contents: `export { default } from "@reddoorla/maintenance/configs/playwright-a11y";
718
+ `
719
+ };
720
+ var ALL_TEMPLATES = [eslint, prettier, lighthouse, playwrightA11y];
721
+ function templatesByName(which) {
722
+ return ALL_TEMPLATES.filter((t) => which.includes(t.config));
723
+ }
724
+
725
+ // src/util/git.ts
726
+ import { execFile } from "child_process";
727
+ import { promisify } from "util";
728
+ var exec = promisify(execFile);
729
+ async function git(cwd, args) {
730
+ return exec("git", args, { cwd, env: process.env });
731
+ }
732
+ function branchName(recipe, when = /* @__PURE__ */ new Date()) {
733
+ const iso = when.toISOString().replace(/[-:.]/g, "").replace(/Z$/, "Z");
734
+ const trimmed = iso.replace(/(\d{8}T\d{6})\d+(Z)$/, "$1$2");
735
+ return `maint/${recipe}-${trimmed}`;
736
+ }
737
+ async function isWorkingTreeClean(cwd) {
738
+ const { stdout } = await git(cwd, ["status", "--porcelain"]);
739
+ return stdout.trim().length === 0;
740
+ }
741
+ async function createBranch(cwd, name) {
742
+ await git(cwd, ["checkout", "-b", name]);
743
+ }
744
+ async function stageAll(cwd) {
745
+ await git(cwd, ["add", "-A"]);
746
+ }
747
+ async function commit(cwd, message) {
748
+ await stageAll(cwd);
749
+ const { stdout: status } = await git(cwd, ["status", "--porcelain"]);
750
+ if (status.trim().length === 0) return null;
751
+ await git(cwd, ["commit", "-m", message]);
752
+ const { stdout: sha } = await git(cwd, ["rev-parse", "HEAD"]);
753
+ return sha.trim();
754
+ }
755
+
756
+ // src/recipes/sync-configs.ts
757
+ function siteLabel6(site) {
758
+ return site.name ?? site.path;
759
+ }
760
+ async function readMaybe(path) {
761
+ try {
762
+ return await readFile4(path, "utf-8");
763
+ } catch {
764
+ return null;
765
+ }
766
+ }
767
+ async function planDiffs(cwd, templates) {
768
+ const diffs = [];
769
+ for (const t of templates) {
770
+ const existing = await readMaybe(join6(cwd, t.path));
771
+ if (existing !== t.contents) diffs.push(t);
772
+ }
773
+ return diffs;
774
+ }
775
+ async function syncConfigs(site, opts = {}) {
776
+ const label = siteLabel6(site);
777
+ const targets = opts.which ? templatesByName(opts.which) : ALL_TEMPLATES;
778
+ const diffs = await planDiffs(site.path, targets);
779
+ if (diffs.length === 0) {
780
+ return {
781
+ recipe: "sync-configs",
782
+ site: label,
783
+ status: "noop",
784
+ commits: [],
785
+ notes: "all targeted configs already match"
786
+ };
787
+ }
788
+ if (!await isWorkingTreeClean(site.path)) {
789
+ throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
790
+ }
791
+ const branch = branchName("sync-configs");
792
+ await createBranch(site.path, branch);
793
+ const shas = [];
794
+ for (const t of diffs) {
795
+ await writeFile3(join6(site.path, t.path), t.contents, "utf-8");
796
+ const sha = await commit(
797
+ site.path,
798
+ `chore: sync ${t.config} config from @reddoorla/maintenance`
799
+ );
800
+ if (sha) shas.push(sha);
801
+ }
802
+ return {
803
+ recipe: "sync-configs",
804
+ site: label,
805
+ status: "applied",
806
+ commits: shas,
807
+ notes: `branch: ${branch}`
808
+ };
809
+ }
810
+
811
+ // src/cli/commands/sync-configs.ts
812
+ function parseOnly2(value) {
813
+ return value ? value.split(",").map((s) => s.trim()) : void 0;
814
+ }
815
+ async function dryPlan(cwd, which) {
816
+ const targets = which ? templatesByName(which) : ALL_TEMPLATES;
817
+ const lines = [];
818
+ for (const t of targets) {
819
+ let existing = "";
820
+ try {
821
+ existing = await readFile5(join7(cwd, t.path), "utf-8");
822
+ } catch {
823
+ }
824
+ if (existing !== t.contents) lines.push(`would update ${t.path} (config: ${t.config})`);
825
+ }
826
+ return lines.length === 0 ? "no changes needed" : lines.join("\n");
827
+ }
828
+ function formatResult(r) {
829
+ if (r.status === "noop") return `[${r.site}] noop: ${r.notes ?? "all configs in sync"}`;
830
+ return `[${r.site}] applied: ${r.commits.length} commit(s)
831
+ ${r.notes ?? ""}`;
832
+ }
833
+ async function runSyncConfigsCommand(site, opts) {
834
+ const which = parseOnly2(opts.only);
835
+ const cwd = opts.cwd ? resolve3(opts.cwd) : process.cwd();
836
+ let sites = await resolveSites({
837
+ ...site !== void 0 ? { site } : {},
838
+ ...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
839
+ cwd
840
+ });
841
+ if (opts.fleet) {
842
+ const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
843
+ sites = await Promise.all(sites.map((s) => cloneIfNeeded(s, { workdir })));
844
+ }
845
+ if (opts.dry) {
846
+ const blocks = [];
847
+ for (const s of sites) {
848
+ blocks.push(`[${s.name ?? s.path}]
849
+ ` + await dryPlan(s.path, which));
850
+ }
851
+ return { output: blocks.join("\n\n"), code: 0 };
852
+ }
853
+ const results = [];
854
+ for (const s of sites) results.push(await syncConfigs(s, which ? { which } : {}));
855
+ const output = results.map(formatResult).join("\n");
856
+ const code = results.some((r) => r.status === "failed") ? 1 : 0;
857
+ return { output, code };
858
+ }
859
+
860
+ // src/cli/commands/bump-deps.ts
861
+ import { resolve as resolve4 } from "path";
862
+
863
+ // src/recipes/bump-deps.ts
864
+ function siteLabel7(site) {
865
+ return site.name ?? site.path;
866
+ }
867
+ function outdatedFlagsForGroup(group) {
868
+ if (group === "major") return ["--latest"];
869
+ if (group === "minor") return [];
870
+ return ["--depth", "0"];
871
+ }
872
+ function upFlagsForGroup(group) {
873
+ if (group === "major") return ["--latest"];
874
+ return [];
875
+ }
876
+ async function bumpDeps(site, opts = {}) {
877
+ const label = siteLabel7(site);
878
+ const group = opts.group ?? "minor";
879
+ const spawn2 = opts.spawn ?? defaultSpawn;
880
+ const outdated = await spawn2("pnpm", ["outdated", "--json", ...outdatedFlagsForGroup(group)], {
881
+ cwd: site.path
882
+ });
883
+ let parsed;
884
+ try {
885
+ parsed = JSON.parse(outdated.stdout || "{}");
886
+ } catch {
887
+ parsed = {};
888
+ }
889
+ const nothingToDo = Object.keys(parsed).length === 0;
890
+ if (nothingToDo) {
891
+ return {
892
+ recipe: "bump-deps",
893
+ site: label,
894
+ status: "noop",
895
+ commits: [],
896
+ notes: `pnpm outdated reported nothing for group=${group}`
897
+ };
898
+ }
899
+ if (!await isWorkingTreeClean(site.path)) {
900
+ throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
901
+ }
902
+ const branch = branchName("bump-deps");
903
+ await createBranch(site.path, branch);
904
+ await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], { cwd: site.path });
905
+ const sha = await commit(site.path, `chore(deps): bump dependencies (${group})`);
906
+ const shas = sha ? [sha] : [];
907
+ return {
908
+ recipe: "bump-deps",
909
+ site: label,
910
+ status: shas.length > 0 ? "applied" : "noop",
911
+ commits: shas,
912
+ notes: `branch: ${branch}`
913
+ };
914
+ }
915
+
916
+ // src/cli/commands/bump-deps.ts
917
+ var GROUPS = ["patch", "minor", "major"];
918
+ function formatResult2(r) {
919
+ if (r.status === "noop") return `[${r.site}] noop: ${r.notes ?? ""}`;
920
+ return `[${r.site}] applied: ${r.commits.length} commit(s)
921
+ ${r.notes ?? ""}`;
922
+ }
923
+ async function runBumpDepsCommand(site, opts) {
924
+ const group = opts.group ?? "minor";
925
+ if (!GROUPS.includes(group)) {
926
+ throw Object.assign(
927
+ new Error(`unknown --group: ${group}. expected one of ${GROUPS.join(", ")}`),
928
+ { exitCode: 2 }
929
+ );
930
+ }
931
+ const cwd = opts.cwd ? resolve4(opts.cwd) : process.cwd();
932
+ let sites = await resolveSites({
933
+ ...site !== void 0 ? { site } : {},
934
+ ...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
935
+ cwd
936
+ });
937
+ if (opts.fleet) {
938
+ const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
939
+ sites = await Promise.all(sites.map((s) => cloneIfNeeded(s, { workdir })));
940
+ }
941
+ const results = [];
942
+ for (const s of sites) results.push(await bumpDeps(s, { group }));
943
+ const output = results.map(formatResult2).join("\n");
944
+ const code = results.some((r) => r.status === "failed") ? 1 : 0;
945
+ return { output, code };
946
+ }
947
+
948
+ // src/cli/commands/upgrade.ts
949
+ import { resolve as resolve5 } from "path";
950
+
951
+ // src/recipes/svelte-5/index.ts
952
+ import { join as join13 } from "path";
953
+
954
+ // src/util/pkg.ts
955
+ import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
956
+ async function readPackageJson(path) {
957
+ const raw = await readFile6(path, "utf-8");
958
+ return JSON.parse(raw);
959
+ }
960
+ async function writePackageJson(path, pkg2) {
961
+ const content = JSON.stringify(pkg2, null, 2) + "\n";
962
+ await writeFile4(path, content, "utf-8");
963
+ }
964
+ function bumpDep(pkg2, name, version) {
965
+ const next = {
966
+ ...pkg2
967
+ };
968
+ if (pkg2.dependencies) {
969
+ next.dependencies = { ...pkg2.dependencies };
970
+ }
971
+ if (pkg2.devDependencies) {
972
+ next.devDependencies = { ...pkg2.devDependencies };
973
+ }
974
+ if (next.dependencies && name in next.dependencies) {
975
+ if (next.dependencies[name] === version) return pkg2;
976
+ next.dependencies[name] = version;
977
+ return next;
978
+ }
979
+ if (next.devDependencies && name in next.devDependencies) {
980
+ if (next.devDependencies[name] === version) return pkg2;
981
+ next.devDependencies[name] = version;
982
+ return next;
983
+ }
984
+ next.devDependencies = { ...next.devDependencies ?? {}, [name]: version };
985
+ return next;
986
+ }
987
+
988
+ // src/recipes/svelte-5/step-bump-versions.ts
989
+ import { join as join8 } from "path";
990
+ var SVELTE_5_VERSIONS = {
991
+ svelte: "^5.55.5",
992
+ "@sveltejs/kit": "^2.59.0",
993
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
994
+ "@sveltejs/adapter-netlify": "^6.0.4",
995
+ "@sveltejs/adapter-auto": "^7.0.0",
996
+ vite: "^8.0.10",
997
+ "svelte-check": "^4.4.7",
998
+ typescript: "^6.0.3",
999
+ "typescript-svelte-plugin": "^0.3.52"
1000
+ };
1001
+ async function bumpToSvelte5Versions(cwd) {
1002
+ const pkgPath = join8(cwd, "package.json");
1003
+ const pkg2 = await readPackageJson(pkgPath);
1004
+ let next = pkg2;
1005
+ for (const [name, version] of Object.entries(SVELTE_5_VERSIONS)) {
1006
+ next = bumpDep(next, name, version);
1007
+ }
1008
+ if (next === pkg2) return false;
1009
+ await writePackageJson(pkgPath, next);
1010
+ return true;
1011
+ }
1012
+
1013
+ // src/recipes/svelte-5/step-svelte-config.ts
1014
+ import { readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
1015
+ import { join as join9 } from "path";
1016
+ async function migrateSvelteConfig(cwd) {
1017
+ const path = join9(cwd, "svelte.config.js");
1018
+ let src;
1019
+ try {
1020
+ src = await readFile7(path, "utf-8");
1021
+ } catch {
1022
+ return false;
1023
+ }
1024
+ let next = src;
1025
+ next = next.replace(
1026
+ /^import\s+\{\s*vitePreprocess\s*\}\s+from\s+["']@sveltejs\/vite-plugin-svelte["'];\n/m,
1027
+ ""
1028
+ );
1029
+ next = next.replace(/^\s*preprocess:\s*vitePreprocess\(\)\s*,?\s*\n/m, "");
1030
+ if (next === src) return false;
1031
+ await writeFile5(path, next, "utf-8");
1032
+ return true;
1033
+ }
1034
+
1035
+ // src/recipes/svelte-5/step-svelte-migrate.ts
1036
+ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
1037
+ try {
1038
+ const { code, stderr } = await spawn2(
1039
+ "npx",
1040
+ ["--yes", "svelte-migrate", "svelte-5", "--no-install"],
1041
+ { cwd, timeoutMs: 5 * 6e4 }
1042
+ );
1043
+ if (code !== 0) {
1044
+ return { ran: false, stderr };
1045
+ }
1046
+ return { ran: true, stderr };
1047
+ } catch (err) {
1048
+ const e = err;
1049
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
1050
+ return { ran: false, stderr: "npx unavailable" };
1051
+ }
1052
+ throw err;
1053
+ }
1054
+ }
1055
+
1056
+ // src/recipes/svelte-5/step-tailwind-upgrade.ts
1057
+ import { join as join10 } from "path";
1058
+ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
1059
+ const pkg2 = await readPackageJson(join10(cwd, "package.json"));
1060
+ const tailwindVersion = pkg2.devDependencies?.tailwindcss ?? pkg2.dependencies?.tailwindcss;
1061
+ if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
1062
+ if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
1063
+ try {
1064
+ const { code, stderr } = await spawn2("npx", ["--yes", "@tailwindcss/upgrade", "--force"], {
1065
+ cwd,
1066
+ timeoutMs: 5 * 6e4
1067
+ });
1068
+ if (code !== 0) return { ran: false, reason: stderr.slice(0, 200) };
1069
+ return { ran: true };
1070
+ } catch (err) {
1071
+ const e = err;
1072
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
1073
+ return { ran: false, reason: "npx unavailable" };
1074
+ }
1075
+ throw err;
1076
+ }
1077
+ }
1078
+
1079
+ // src/recipes/svelte-5/step-gotchas.ts
1080
+ import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
1081
+ import { join as join11 } from "path";
1082
+ import { glob as glob2 } from "tinyglobby";
1083
+
1084
+ // src/recipes/svelte-5/codemods/on-event-to-handler.ts
1085
+ var SCRIPT_BLOCK = /<script\b[^>]*>[\s\S]*?<\/script>/g;
1086
+ var SIMPLE_ON_EVENT = /\bon:([a-z]+)(?=\s*=)/g;
1087
+ function onEventToHandler(source) {
1088
+ const masked = [];
1089
+ const placeholder = (i) => ` SCRIPT_${i} `;
1090
+ const intermediate = source.replace(SCRIPT_BLOCK, (match) => {
1091
+ masked.push(match);
1092
+ return placeholder(masked.length - 1);
1093
+ });
1094
+ const rewritten = intermediate.replace(SIMPLE_ON_EVENT, (_full, name) => `on${name}`);
1095
+ let out = rewritten;
1096
+ masked.forEach((blk, i) => {
1097
+ out = out.replace(placeholder(i), blk);
1098
+ });
1099
+ return out;
1100
+ }
1101
+
1102
+ // src/recipes/svelte-5/codemods/dollar-props.ts
1103
+ var SCRIPT_TS = /<script\b[^>]*lang=["']ts["'][^>]*>([\s\S]*?)<\/script>/;
1104
+ var EXPORT_LET = /^\s*export\s+let\s+(\w+)\s*(?::\s*([^=;\n]+))?\s*(?:=\s*([^;\n]+))?;?\s*$/gm;
1105
+ function transformScript(scriptBody) {
1106
+ const props = [];
1107
+ const cleaned = scriptBody.replace(
1108
+ EXPORT_LET,
1109
+ (_full, name, type, defaultExpr) => {
1110
+ props.push({
1111
+ name,
1112
+ type: type?.trim(),
1113
+ defaultExpr: defaultExpr?.trim()
1114
+ });
1115
+ return "";
1116
+ }
1117
+ );
1118
+ if (props.length === 0) return { body: scriptBody, changed: false };
1119
+ const typeSig = props.map((p) => {
1120
+ const optional = p.defaultExpr ? "?" : "";
1121
+ return `${p.name}${optional}: ${p.type ?? "unknown"}`;
1122
+ }).join("; ");
1123
+ const destructured = props.map((p) => p.defaultExpr ? `${p.name} = ${p.defaultExpr}` : p.name).join(", ");
1124
+ const decl = ` let { ${destructured} }: { ${typeSig} } = $props();`;
1125
+ const next = cleaned.replace(/^(\s*)/, (m) => `${m}${decl}
1126
+ `);
1127
+ return { body: next, changed: true };
1128
+ }
1129
+ function exportLetToProps(source) {
1130
+ const match = source.match(SCRIPT_TS);
1131
+ if (!match) return source;
1132
+ const inner = match[1] ?? "";
1133
+ const { body, changed } = transformScript(inner);
1134
+ if (!changed) return source;
1135
+ return source.replace(SCRIPT_TS, (full) => full.replace(inner, body));
1136
+ }
1137
+
1138
+ // src/recipes/svelte-5/codemods/dollar-restprops.ts
1139
+ function removeDollarRestProps(source) {
1140
+ let next = source;
1141
+ next = next.replace(/\$\$restProps/g, "rest");
1142
+ next = next.replace(/^\s*interface\s+\$\$Props\s*\{[^}]*\}\s*\n/gm, "");
1143
+ return next;
1144
+ }
1145
+
1146
+ // src/recipes/svelte-5/step-gotchas.ts
1147
+ var SVELTE_GLOBS = ["src/**/*.svelte"];
1148
+ var IGNORE2 = ["node_modules/**", ".svelte-kit/**", "build/**"];
1149
+ var CODEMODS = [onEventToHandler, exportLetToProps, removeDollarRestProps];
1150
+ async function applyGotchaCodemods(cwd) {
1151
+ let filesChanged = 0;
1152
+ const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
1153
+ for (const rel of relPaths) {
1154
+ const path = join11(cwd, rel);
1155
+ const before = await readFile8(path, "utf-8");
1156
+ const after = CODEMODS.reduce((s, fn) => fn(s), before);
1157
+ if (after !== before) {
1158
+ await writeFile6(path, after, "utf-8");
1159
+ filesChanged += 1;
1160
+ }
1161
+ }
1162
+ return { filesChanged };
1163
+ }
1164
+
1165
+ // src/recipes/svelte-5/step-verify.ts
1166
+ async function verifyMigration(cwd, spawn2 = defaultSpawn) {
1167
+ let install;
1168
+ try {
1169
+ install = await spawn2("pnpm", ["install"], { cwd, timeoutMs: 10 * 6e4 });
1170
+ } catch {
1171
+ install = { skipped: true };
1172
+ }
1173
+ let check;
1174
+ try {
1175
+ check = await spawn2("pnpm", ["run", "check"], { cwd, timeoutMs: 5 * 6e4 });
1176
+ } catch {
1177
+ check = { skipped: true };
1178
+ }
1179
+ return { install, check };
1180
+ }
1181
+
1182
+ // src/recipes/svelte-5/step-summary.ts
1183
+ import { writeFile as writeFile7 } from "fs/promises";
1184
+ import { join as join12 } from "path";
1185
+ async function writeMigrationSummary(input) {
1186
+ const lines = [
1187
+ `# Svelte 4 \u2192 5 migration summary`,
1188
+ ``,
1189
+ `Generated by @reddoorla/maintenance.`,
1190
+ ``,
1191
+ `- svelte-migrate run: ${input.svelteMigrateRan ? "yes" : "no"}`,
1192
+ `- @tailwindcss/upgrade run: ${input.tailwindUpgraded ? "yes" : "no"}`,
1193
+ `- .svelte files touched by gotcha codemods: ${input.filesChangedByCodemods}`,
1194
+ ``,
1195
+ `Next steps:`,
1196
+ `- Run \`pnpm run check\` and resolve any remaining warnings.`,
1197
+ `- Spot-check rune migrations in components that use \`reactive\` statements.`,
1198
+ `- Verify Playwright a11y tests still pass.`
1199
+ ];
1200
+ const content = lines.join("\n") + "\n";
1201
+ const path = join12(input.cwd, "MIGRATION_SVELTE_5.md");
1202
+ await writeFile7(path, content, "utf-8");
1203
+ return path;
1204
+ }
1205
+
1206
+ // src/recipes/svelte-5/index.ts
1207
+ function siteLabel8(site) {
1208
+ return site.name ?? site.path;
1209
+ }
1210
+ async function alreadyOnSvelte5(cwd) {
1211
+ try {
1212
+ const pkg2 = await readPackageJson(join13(cwd, "package.json"));
1213
+ const v = pkg2.devDependencies?.svelte ?? pkg2.dependencies?.svelte;
1214
+ return !!v && /^\^?5\./.test(v);
1215
+ } catch {
1216
+ return false;
1217
+ }
1218
+ }
1219
+ async function upgradeSvelte4to5(site, opts = {}) {
1220
+ const label = siteLabel8(site);
1221
+ const spawn2 = opts.spawn ?? defaultSpawn;
1222
+ if (await alreadyOnSvelte5(site.path)) {
1223
+ return {
1224
+ recipe: "svelte-4-to-5",
1225
+ site: label,
1226
+ status: "noop",
1227
+ commits: [],
1228
+ notes: "site already declares svelte ^5.x"
1229
+ };
1230
+ }
1231
+ if (!await isWorkingTreeClean(site.path)) {
1232
+ throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
1233
+ }
1234
+ const branch = branchName("svelte-4-to-5");
1235
+ await createBranch(site.path, branch);
1236
+ const shas = [];
1237
+ const bumped = await bumpToSvelte5Versions(site.path);
1238
+ if (bumped) {
1239
+ const sha = await commit(site.path, "chore(svelte5): bump svelte/kit/vite/vite-plugin-svelte");
1240
+ if (sha) shas.push(sha);
1241
+ }
1242
+ const configChanged = await migrateSvelteConfig(site.path);
1243
+ if (configChanged) {
1244
+ const sha = await commit(
1245
+ site.path,
1246
+ "refactor(svelte5): migrate svelte.config.js (drop vitePreprocess)"
1247
+ );
1248
+ if (sha) shas.push(sha);
1249
+ }
1250
+ const migrate = await runSvelteMigrate(site.path, spawn2);
1251
+ if (migrate.ran) {
1252
+ const sha = await commit(site.path, "refactor(svelte5): run official svelte-migrate codemod");
1253
+ if (sha) shas.push(sha);
1254
+ }
1255
+ const tw = await upgradeTailwind(site.path, spawn2);
1256
+ if (tw.ran) {
1257
+ const sha = await commit(site.path, "chore(svelte5): tailwindcss 3 \u2192 4 upgrade");
1258
+ if (sha) shas.push(sha);
1259
+ }
1260
+ const codemods = await applyGotchaCodemods(site.path);
1261
+ if (codemods.filesChanged > 0) {
1262
+ const sha = await commit(
1263
+ site.path,
1264
+ `refactor(svelte5): apply gotcha codemods (${codemods.filesChanged} files)`
1265
+ );
1266
+ if (sha) shas.push(sha);
1267
+ }
1268
+ await verifyMigration(site.path, spawn2);
1269
+ const verifySha = await commit(site.path, "chore(svelte5): pnpm install + check");
1270
+ if (verifySha) shas.push(verifySha);
1271
+ await writeMigrationSummary({
1272
+ cwd: site.path,
1273
+ filesChangedByCodemods: codemods.filesChanged,
1274
+ svelteMigrateRan: migrate.ran,
1275
+ tailwindUpgraded: tw.ran
1276
+ });
1277
+ const summarySha = await commit(site.path, "docs(svelte5): add MIGRATION_SVELTE_5.md summary");
1278
+ if (summarySha) shas.push(summarySha);
1279
+ return {
1280
+ recipe: "svelte-4-to-5",
1281
+ site: label,
1282
+ status: shas.length > 0 ? "applied" : "noop",
1283
+ commits: shas,
1284
+ notes: `branch: ${branch}`
1285
+ };
1286
+ }
1287
+
1288
+ // src/cli/commands/upgrade.ts
1289
+ var KNOWN_UPGRADES = /* @__PURE__ */ new Set(["svelte-4-to-5"]);
1290
+ function formatResult3(r) {
1291
+ if (r.status === "noop") return `[${r.site}] noop: ${r.notes ?? ""}`;
1292
+ return `[${r.site}] applied: ${r.commits.length} commit(s)
1293
+ ${r.notes ?? ""}`;
1294
+ }
1295
+ async function runUpgradeCommand(upgradeName, site, opts = {}) {
1296
+ if (!upgradeName || !KNOWN_UPGRADES.has(upgradeName)) {
1297
+ throw Object.assign(
1298
+ new Error(
1299
+ `unknown upgrade: ${upgradeName ?? "(none)"}. expected one of ${[...KNOWN_UPGRADES].join(", ")}`
1300
+ ),
1301
+ { exitCode: 2 }
1302
+ );
1303
+ }
1304
+ const cwd = opts.cwd ? resolve5(opts.cwd) : process.cwd();
1305
+ let sites = await resolveSites({
1306
+ ...site !== void 0 ? { site } : {},
1307
+ ...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
1308
+ cwd
1309
+ });
1310
+ if (opts.fleet) {
1311
+ const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
1312
+ sites = await Promise.all(sites.map((s) => cloneIfNeeded(s, { workdir })));
1313
+ }
1314
+ const results = [];
1315
+ for (const s of sites) {
1316
+ if (upgradeName === "svelte-4-to-5") {
1317
+ results.push(await upgradeSvelte4to5(s));
1318
+ }
1319
+ }
1320
+ const output = results.map(formatResult3).join("\n");
1321
+ const code = results.some((r) => r.status === "failed") ? 1 : 0;
1322
+ return { output, code };
1323
+ }
1324
+
1325
+ // src/cli/bin.ts
1326
+ var here = dirname(fileURLToPath(import.meta.url));
1327
+ var pkg = JSON.parse(readFileSync(join14(here, "../../package.json"), "utf-8"));
1328
+ var AUDIT_DESCRIPTIONS = {
1329
+ deps: "Diff site package.json against the bundled baseline version map.",
1330
+ lighthouse: "Run @lhci/cli autorun using the canonical lighthouserc.",
1331
+ a11y: "Playwright + axe against the canonical a11y routes.",
1332
+ security: "pnpm audit (falls back to npm audit), prod-deps by default.",
1333
+ lint: "ESLint + Prettier using the canonical configs."
1334
+ };
1335
+ var RECIPE_DESCRIPTIONS = {
1336
+ "sync-configs": "Overwrite a site's canonical configs to match @reddoorla/maintenance.",
1337
+ "bump-deps": "Bump dependencies and commit the lockfile change.",
1338
+ "svelte-4-to-5": "Run the 7-commit Svelte 4 \u2192 5 upgrade recipe."
1339
+ };
1340
+ var cli = cac("reddoor-maint");
1341
+ cli.option("--cwd <path>", "Override working directory (default: process.cwd())");
1342
+ cli.option("--verbose", "Verbose output (full stack on errors)");
1343
+ cli.command("list-audits", "Print the available audits.").action(() => {
1344
+ for (const [name, desc] of Object.entries(AUDIT_DESCRIPTIONS)) {
1345
+ console.log(`${name.padEnd(12)} ${desc}`);
1346
+ }
1347
+ });
1348
+ cli.command("list-recipes", "Print the available recipes.").action(() => {
1349
+ for (const [name, desc] of Object.entries(RECIPE_DESCRIPTIONS)) {
1350
+ console.log(`${name.padEnd(16)} ${desc}`);
1351
+ }
1352
+ });
1353
+ cli.command("audit [site]", "Run audits against a site (default: cwd).").option("--only <names>", "Comma-separated audit names (e.g. deps,lighthouse)").option("--json", "Machine-readable JSON output").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js); aggregates across sites").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
1354
+ async (site, opts) => {
1355
+ try {
1356
+ const { output, code } = await runAuditCommand(site, opts);
1357
+ console.log(output);
1358
+ process.exit(code);
1359
+ } catch (err) {
1360
+ const e = err;
1361
+ console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
1362
+ process.exit(e.exitCode ?? 1);
1363
+ }
1364
+ }
1365
+ );
1366
+ cli.command("sync-configs [site]", "Sync canonical configs into a site.").option("--only <names>", "Comma-separated config names (e.g. eslint,prettier)").option("--dry", "Print diff without writing").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
1367
+ async (site, opts) => {
1368
+ try {
1369
+ const { output, code } = await runSyncConfigsCommand(site, opts);
1370
+ console.log(output);
1371
+ process.exit(code);
1372
+ } catch (err) {
1373
+ const e = err;
1374
+ console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
1375
+ process.exit(e.exitCode ?? 1);
1376
+ }
1377
+ }
1378
+ );
1379
+ cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>", "patch | minor | major", { default: "minor" }).option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
1380
+ async (site, opts) => {
1381
+ try {
1382
+ const { output, code } = await runBumpDepsCommand(site, opts);
1383
+ console.log(output);
1384
+ process.exit(code);
1385
+ } catch (err) {
1386
+ const e = err;
1387
+ console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
1388
+ process.exit(e.exitCode ?? 1);
1389
+ }
1390
+ }
1391
+ );
1392
+ cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to-5).").example("reddoor-maint upgrade svelte-4-to-5 ./my-site").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
1393
+ async (upgrade, site, opts) => {
1394
+ try {
1395
+ const { output, code } = await runUpgradeCommand(upgrade, site, opts);
1396
+ console.log(output);
1397
+ process.exit(code);
1398
+ } catch (err) {
1399
+ const e = err;
1400
+ console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
1401
+ process.exit(e.exitCode ?? 1);
1402
+ }
1403
+ }
1404
+ );
1405
+ cli.help();
1406
+ cli.version(pkg.version);
1407
+ cli.parse();
1408
+ //# sourceMappingURL=bin.js.map