@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,666 @@
1
+ // src/cli/commands/audit.ts
2
+ import { resolve as resolve2 } from "path";
3
+
4
+ // src/audits/util/spawn.ts
5
+ import { spawn } from "child_process";
6
+ var defaultSpawn = (cmd, args, opts = {}) => new Promise((resolve3, reject) => {
7
+ const child = spawn(cmd, [...args], {
8
+ cwd: opts.cwd,
9
+ env: opts.env ?? process.env,
10
+ stdio: ["ignore", "pipe", "pipe"]
11
+ });
12
+ let stdout = "";
13
+ let stderr = "";
14
+ child.stdout.on("data", (chunk) => stdout += String(chunk));
15
+ child.stderr.on("data", (chunk) => stderr += String(chunk));
16
+ const timer = opts.timeoutMs ? setTimeout(() => {
17
+ child.kill("SIGTERM");
18
+ reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));
19
+ }, opts.timeoutMs) : void 0;
20
+ child.on("error", (err) => {
21
+ if (timer) clearTimeout(timer);
22
+ reject(err);
23
+ });
24
+ child.on("close", (code) => {
25
+ if (timer) clearTimeout(timer);
26
+ resolve3({ code: code ?? -1, stdout, stderr });
27
+ });
28
+ });
29
+
30
+ // src/audits/deps.ts
31
+ import { readFile } from "fs/promises";
32
+ import { join } from "path";
33
+
34
+ // src/configs/baseline-versions.ts
35
+ var baselineVersions = {
36
+ // SvelteKit core
37
+ svelte: "^5.55.5",
38
+ "@sveltejs/kit": "^2.59.0",
39
+ "@sveltejs/adapter-netlify": "^6.0.4",
40
+ "@sveltejs/adapter-auto": "^7.0.0",
41
+ "@sveltejs/vite-plugin-svelte": "^7.0.0",
42
+ "svelte-check": "^4.4.7",
43
+ // Build tooling
44
+ vite: "^8.0.10",
45
+ vitest: "^4.1.1",
46
+ typescript: "^6.0.3",
47
+ // Tailwind 4
48
+ tailwindcss: "^4.0.14",
49
+ "@tailwindcss/vite": "^4.3.0",
50
+ // Prismic
51
+ "@prismicio/client": "^7.3.1",
52
+ "@prismicio/svelte": "^2.0.0",
53
+ "@slicemachine/adapter-sveltekit": "^0.3.36",
54
+ "slice-machine-ui": "^2.11.1",
55
+ // Test tooling
56
+ "@playwright/test": "^1.59.1",
57
+ "@axe-core/playwright": "^4.11.3",
58
+ "@lhci/cli": "^0.15.1",
59
+ // Lint
60
+ eslint: "^10.3.0",
61
+ "eslint-plugin-svelte": "^3.1.0",
62
+ "eslint-config-prettier": "^10.1.1",
63
+ prettier: "^3.1.1",
64
+ "prettier-plugin-svelte": "^3.2.6",
65
+ "typescript-eslint": "^8.59.1",
66
+ "@eslint/js": "^10.0.1",
67
+ globals: "^17.6.0",
68
+ // Misc
69
+ "@lucide/svelte": "^1.14.0",
70
+ "@zerodevx/svelte-img": "^2.1.2"
71
+ };
72
+
73
+ // src/audits/deps.ts
74
+ function siteLabel(site) {
75
+ return site.name ?? site.path;
76
+ }
77
+ function stripCaret(range) {
78
+ return range.replace(/^[\^~]/, "");
79
+ }
80
+ function parseSemver(v) {
81
+ const cleaned = stripCaret(v).split("-")[0] ?? "0.0.0";
82
+ const parts = cleaned.split(".").map((n) => Number.parseInt(n, 10));
83
+ return [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
84
+ }
85
+ function compareSemver(actual, baseline) {
86
+ const [aMajor, aMinor, aPatch] = parseSemver(actual);
87
+ const [bMajor, bMinor, bPatch] = parseSemver(baseline);
88
+ if (aMajor > bMajor) return "newer";
89
+ if (aMajor < bMajor) return "major";
90
+ if (aMinor > bMinor) return "newer";
91
+ if (aMinor < bMinor) return "minor";
92
+ if (aPatch > bPatch) return "newer";
93
+ if (aPatch < bPatch) return "patch";
94
+ return "same";
95
+ }
96
+ async function depsAudit(ctx) {
97
+ const pkgPath = join(ctx.site.path, "package.json");
98
+ let pkgRaw;
99
+ try {
100
+ pkgRaw = await readFile(pkgPath, "utf-8");
101
+ } catch (err) {
102
+ return {
103
+ audit: "deps",
104
+ site: siteLabel(ctx.site),
105
+ status: "skip",
106
+ summary: `no package.json at ${pkgPath}`,
107
+ details: { error: String(err) }
108
+ };
109
+ }
110
+ const pkg = JSON.parse(pkgRaw);
111
+ const installed = {
112
+ ...pkg.dependencies ?? {},
113
+ ...pkg.devDependencies ?? {}
114
+ };
115
+ const details = [];
116
+ for (const [name, baseline] of Object.entries(baselineVersions)) {
117
+ const actual = installed[name];
118
+ if (!actual) continue;
119
+ details.push({
120
+ pkg: name,
121
+ baseline,
122
+ actual,
123
+ drift: compareSemver(actual, baseline)
124
+ });
125
+ }
126
+ const anyMajor = details.some((d) => d.drift === "major");
127
+ const anyMinor = details.some((d) => d.drift === "minor");
128
+ const anyNewer = details.some((d) => d.drift === "newer");
129
+ const status = anyMajor ? "fail" : anyMinor || anyNewer ? "warn" : "pass";
130
+ 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`;
131
+ return {
132
+ audit: "deps",
133
+ site: siteLabel(ctx.site),
134
+ status,
135
+ summary,
136
+ details
137
+ };
138
+ }
139
+
140
+ // src/audits/lint.ts
141
+ import { existsSync } from "fs";
142
+ import { readFile as readFile2 } from "fs/promises";
143
+ import { join as join2, relative } from "path";
144
+ import { ESLint } from "eslint";
145
+ import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "prettier";
146
+ import { glob } from "tinyglobby";
147
+ var TARGET_GLOBS = ["**/*.{ts,js,svelte}"];
148
+ var IGNORE = ["node_modules/**", "dist/**", ".svelte-kit/**", "build/**", ".netlify/**"];
149
+ function siteLabel2(site) {
150
+ return site.name ?? site.path;
151
+ }
152
+ async function listFiles(cwd) {
153
+ return glob(TARGET_GLOBS, { cwd, ignore: IGNORE, absolute: false });
154
+ }
155
+ async function lintAudit(ctx) {
156
+ const { site } = ctx;
157
+ const configPath = join2(site.path, "eslint.config.js");
158
+ if (!existsSync(configPath)) {
159
+ return {
160
+ audit: "lint",
161
+ site: siteLabel2(site),
162
+ status: "skip",
163
+ summary: "no eslint config at site root"
164
+ };
165
+ }
166
+ const eslint = new ESLint({
167
+ cwd: site.path,
168
+ overrideConfigFile: configPath,
169
+ errorOnUnmatchedPattern: false
170
+ });
171
+ const relFiles = await listFiles(site.path);
172
+ const filesToLint = relFiles.map((f) => join2(site.path, f));
173
+ const eslintResults = await eslint.lintFiles(filesToLint);
174
+ const eslintErrors = eslintResults.reduce((n, r) => n + r.errorCount, 0);
175
+ const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);
176
+ const prettierUnformatted = [];
177
+ for (const file of filesToLint) {
178
+ const source = await readFile2(file, "utf-8");
179
+ const options = await prettierResolveConfig(file) ?? {};
180
+ const ok = await prettierCheck(source, { ...options, filepath: file });
181
+ if (!ok) prettierUnformatted.push(relative(site.path, file));
182
+ }
183
+ const status = eslintErrors > 0 || prettierUnformatted.length > 0 ? "fail" : eslintWarnings > 0 ? "warn" : "pass";
184
+ const summary = status === "pass" ? `lint clean across ${filesToLint.length} files` : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;
185
+ return {
186
+ audit: "lint",
187
+ site: siteLabel2(site),
188
+ status,
189
+ summary,
190
+ details: {
191
+ eslintErrors,
192
+ eslintWarnings,
193
+ prettierUnformatted,
194
+ files: filesToLint.length
195
+ }
196
+ };
197
+ }
198
+
199
+ // src/audits/security.ts
200
+ function siteLabel3(site) {
201
+ return site.name ?? site.path;
202
+ }
203
+ function classify(v) {
204
+ if (v.critical > 0 || v.high > 0) return "fail";
205
+ if (v.moderate > 0 || v.low > 0) return "warn";
206
+ return "pass";
207
+ }
208
+ async function tryRun(spawn2, cmd, args, cwd) {
209
+ try {
210
+ return await spawn2(cmd, args, { cwd });
211
+ } catch (err) {
212
+ const e = err;
213
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) return { missing: true };
214
+ throw err;
215
+ }
216
+ }
217
+ async function securityAudit(ctx) {
218
+ const spawn2 = ctx.spawn ?? defaultSpawn;
219
+ const site = ctx.site;
220
+ const label = siteLabel3(site);
221
+ let used = "pnpm audit";
222
+ let raw = await tryRun(
223
+ spawn2,
224
+ "pnpm",
225
+ ["audit", "--json", "--prod"],
226
+ site.path
227
+ );
228
+ if ("missing" in raw) {
229
+ used = "npm audit";
230
+ raw = await tryRun(spawn2, "npm", ["audit", "--json", "--omit=dev"], site.path);
231
+ }
232
+ if ("missing" in raw) {
233
+ return {
234
+ audit: "security",
235
+ site: label,
236
+ status: "skip",
237
+ summary: "neither pnpm nor npm is available on PATH"
238
+ };
239
+ }
240
+ if (raw.code !== 0 && raw.code !== 1) {
241
+ return {
242
+ audit: "security",
243
+ site: label,
244
+ status: "skip",
245
+ summary: `${used} exited with code ${raw.code}`,
246
+ details: { stderr: raw.stderr }
247
+ };
248
+ }
249
+ let parsed;
250
+ try {
251
+ parsed = JSON.parse(raw.stdout);
252
+ } catch (err) {
253
+ return {
254
+ audit: "security",
255
+ site: label,
256
+ status: "skip",
257
+ summary: `${used} produced unparseable JSON`,
258
+ details: { error: String(err), stdout: raw.stdout.slice(0, 500) }
259
+ };
260
+ }
261
+ const vuln = {
262
+ low: parsed.metadata?.vulnerabilities?.low ?? 0,
263
+ moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,
264
+ high: parsed.metadata?.vulnerabilities?.high ?? 0,
265
+ critical: parsed.metadata?.vulnerabilities?.critical ?? 0
266
+ };
267
+ const status = classify(vuln);
268
+ const total = vuln.low + vuln.moderate + vuln.high + vuln.critical;
269
+ const summary = status === "pass" ? `${used}: 0 vulnerabilities` : `${used}: ${total} vulnerabilities (${vuln.critical}C/${vuln.high}H/${vuln.moderate}M/${vuln.low}L)`;
270
+ return {
271
+ audit: "security",
272
+ site: label,
273
+ status,
274
+ summary,
275
+ details: vuln
276
+ };
277
+ }
278
+
279
+ // src/audits/lighthouse.ts
280
+ import { writeFile, mkdtemp, rm } from "fs/promises";
281
+ import { tmpdir } from "os";
282
+ import { join as join3 } from "path";
283
+
284
+ // src/configs/lighthouse.ts
285
+ var lighthouseConfig = {
286
+ ci: {
287
+ collect: {
288
+ url: ["http://localhost:5173/dev/a11y-fixtures"],
289
+ startServerCommand: "pnpm vite:dev",
290
+ startServerReadyPattern: "ready in",
291
+ startServerReadyTimeout: 12e4,
292
+ numberOfRuns: 1,
293
+ settings: {
294
+ preset: "desktop",
295
+ skipAudits: ["uses-http2"]
296
+ }
297
+ },
298
+ assert: {
299
+ assertions: {
300
+ "categories:accessibility": ["error", { minScore: 0.95 }],
301
+ "categories:best-practices": ["error", { minScore: 0.9 }],
302
+ "categories:seo": ["error", { minScore: 0.9 }],
303
+ "categories:performance": ["warn", { minScore: 0.7 }]
304
+ }
305
+ },
306
+ upload: {
307
+ target: "temporary-public-storage"
308
+ }
309
+ }
310
+ };
311
+
312
+ // src/audits/lighthouse.ts
313
+ function siteLabel4(site) {
314
+ return site.name ?? site.path;
315
+ }
316
+ function isFakeShape(stdout) {
317
+ try {
318
+ const parsed = JSON.parse(stdout);
319
+ if (typeof parsed.assertionsFailed === "number" && parsed.summary) return parsed;
320
+ } catch {
321
+ return null;
322
+ }
323
+ return null;
324
+ }
325
+ async function lighthouseAudit(ctx) {
326
+ const spawn2 = ctx.spawn ?? defaultSpawn;
327
+ const site = ctx.site;
328
+ const label = siteLabel4(site);
329
+ const dir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
330
+ const configPath = join3(dir, "lighthouserc.json");
331
+ await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
332
+ let raw;
333
+ try {
334
+ raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
335
+ cwd: site.path
336
+ });
337
+ } catch (err) {
338
+ await rm(dir, { recursive: true, force: true });
339
+ const e = err;
340
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
341
+ return {
342
+ audit: "lighthouse",
343
+ site: label,
344
+ status: "skip",
345
+ summary: "npx/@lhci/cli not available"
346
+ };
347
+ }
348
+ throw err;
349
+ }
350
+ await rm(dir, { recursive: true, force: true });
351
+ const fake = isFakeShape(raw.stdout);
352
+ const normalized = fake ?? {
353
+ summary: {},
354
+ assertionsFailed: raw.code === 0 ? 0 : 1,
355
+ assertions: raw.code === 0 ? [] : [{ category: "unknown", level: "error", message: raw.stderr.slice(0, 200) }]
356
+ };
357
+ const anyError = (normalized.assertions ?? []).some((a) => a.level === "error");
358
+ const anyWarn = (normalized.assertions ?? []).some((a) => a.level === "warn");
359
+ const status = anyError ? "fail" : anyWarn ? "warn" : "pass";
360
+ const summary = status === "pass" ? "lighthouse: all categories passing" : `lighthouse: ${normalized.assertionsFailed} assertion(s) failed`;
361
+ return {
362
+ audit: "lighthouse",
363
+ site: label,
364
+ status,
365
+ summary,
366
+ details: normalized
367
+ };
368
+ }
369
+
370
+ // src/audits/a11y.ts
371
+ import { writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
372
+ import { tmpdir as tmpdir2 } from "os";
373
+ import { join as join4 } from "path";
374
+
375
+ // src/configs/playwright-a11y.ts
376
+ import { defineConfig, devices } from "@playwright/test";
377
+ var a11yRoutes = [
378
+ { path: "/dev/a11y-fixtures", name: "a11y fixtures" },
379
+ { path: "/dev/animate-in", name: "animate-in demo" }
380
+ ];
381
+ var playwrightA11yConfig = defineConfig({
382
+ testDir: "tests",
383
+ testMatch: /.*\.spec\.ts$/,
384
+ fullyParallel: true,
385
+ forbidOnly: !!process.env.CI,
386
+ retries: process.env.CI ? 2 : 0,
387
+ reporter: process.env.CI ? "github" : "list",
388
+ use: {
389
+ baseURL: "http://localhost:5173",
390
+ trace: "on-first-retry"
391
+ },
392
+ projects: [
393
+ {
394
+ name: "chromium",
395
+ use: { ...devices["Desktop Chrome"] }
396
+ }
397
+ ],
398
+ webServer: {
399
+ command: "pnpm vite:dev",
400
+ url: "http://localhost:5173/dev/a11y-fixtures",
401
+ reuseExistingServer: !process.env.CI,
402
+ timeout: 12e4
403
+ }
404
+ });
405
+
406
+ // src/audits/a11y.ts
407
+ function siteLabel5(site) {
408
+ return site.name ?? site.path;
409
+ }
410
+ function isFakeShape2(stdout) {
411
+ try {
412
+ const parsed = JSON.parse(stdout);
413
+ if (typeof parsed.totalViolations === "number" && parsed.byImpact) return parsed;
414
+ } catch {
415
+ return null;
416
+ }
417
+ return null;
418
+ }
419
+ function buildSpec() {
420
+ return `
421
+ import { test, expect } from "@playwright/test";
422
+ import AxeBuilder from "@axe-core/playwright";
423
+ const pages = ${JSON.stringify(a11yRoutes)};
424
+ for (const { path, name } of pages) {
425
+ test(\`\${name} has no axe violations\`, async ({ page }) => {
426
+ await page.goto(path);
427
+ const results = await new AxeBuilder({ page })
428
+ .withTags(["wcag2a","wcag2aa","wcag21a","wcag21aa","wcag22aa"])
429
+ .analyze();
430
+ expect(results.violations).toEqual([]);
431
+ });
432
+ }
433
+ `;
434
+ }
435
+ async function a11yAudit(ctx) {
436
+ const spawn2 = ctx.spawn ?? defaultSpawn;
437
+ const site = ctx.site;
438
+ const label = siteLabel5(site);
439
+ const dir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-"));
440
+ const specPath = join4(dir, "a11y.spec.ts");
441
+ await writeFile2(specPath, buildSpec(), "utf-8");
442
+ let raw;
443
+ try {
444
+ raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=json", specPath], {
445
+ cwd: site.path
446
+ });
447
+ } catch (err) {
448
+ await rm2(dir, { recursive: true, force: true });
449
+ const e = err;
450
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) {
451
+ return {
452
+ audit: "a11y",
453
+ site: label,
454
+ status: "skip",
455
+ summary: "npx/playwright not available"
456
+ };
457
+ }
458
+ throw err;
459
+ }
460
+ await rm2(dir, { recursive: true, force: true });
461
+ const fake = isFakeShape2(raw.stdout);
462
+ const normalized = fake ?? {
463
+ totalViolations: raw.code === 0 ? 0 : 1,
464
+ byImpact: raw.code === 0 ? {} : { moderate: 1 }
465
+ };
466
+ const hasSerious = (normalized.byImpact.serious ?? 0) > 0 || (normalized.byImpact.critical ?? 0) > 0;
467
+ const hasAny = normalized.totalViolations > 0;
468
+ const status = hasSerious ? "fail" : hasAny ? "warn" : "pass";
469
+ const summary = status === "pass" ? `a11y: 0 violations across ${a11yRoutes.length} routes` : `a11y: ${normalized.totalViolations} violations`;
470
+ return {
471
+ audit: "a11y",
472
+ site: label,
473
+ status,
474
+ summary,
475
+ details: normalized
476
+ };
477
+ }
478
+
479
+ // src/audits/index.ts
480
+ var REGISTRY = {
481
+ deps: depsAudit,
482
+ lint: lintAudit,
483
+ security: securityAudit,
484
+ lighthouse: lighthouseAudit,
485
+ a11y: a11yAudit
486
+ };
487
+ var ALL_AUDIT_NAMES = Object.keys(REGISTRY);
488
+ var DEFAULT_AUDIT_TIMEOUT_MS = 3e4;
489
+ function timedSpawn(timeoutMs) {
490
+ return (cmd, args, opts = {}) => defaultSpawn(cmd, args, { ...opts, timeoutMs: opts.timeoutMs ?? timeoutMs });
491
+ }
492
+ async function runAudits(site, which) {
493
+ const names = which ?? ALL_AUDIT_NAMES;
494
+ for (const n of names) {
495
+ if (!(n in REGISTRY)) throw new Error(`unknown audit: ${n}`);
496
+ }
497
+ const spawn2 = timedSpawn(DEFAULT_AUDIT_TIMEOUT_MS);
498
+ const label = site.name ?? site.path;
499
+ return Promise.all(
500
+ names.map(
501
+ (n) => REGISTRY[n]({ site, spawn: spawn2 }).catch(
502
+ (err) => ({
503
+ audit: n,
504
+ site: label,
505
+ status: "fail",
506
+ summary: `${n}: unexpected error \u2014 ${String(err)}`
507
+ })
508
+ )
509
+ )
510
+ );
511
+ }
512
+
513
+ // src/cli/fleet/resolve-sites.ts
514
+ import { pathToFileURL } from "url";
515
+ import { resolve, extname } from "path";
516
+
517
+ // src/inventory/local.ts
518
+ import { basename } from "path";
519
+ function localPath(path, opts = {}) {
520
+ const site = { path, name: opts.name ?? basename(path) };
521
+ return async () => [site];
522
+ }
523
+
524
+ // src/inventory/json.ts
525
+ import { readFile as readFile3 } from "fs/promises";
526
+ function validate(raw) {
527
+ if (!Array.isArray(raw)) {
528
+ throw new Error("inventory JSON must be an array of sites");
529
+ }
530
+ return raw.map((entry, i) => {
531
+ if (!entry || typeof entry !== "object") {
532
+ throw new Error(`inventory entry ${i} is not an object`);
533
+ }
534
+ const e = entry;
535
+ if (typeof e.path !== "string" || e.path.length === 0) {
536
+ throw new Error(`inventory entry ${i} is missing required field: path`);
537
+ }
538
+ const site = { path: e.path };
539
+ if (typeof e.name === "string") site.name = e.name;
540
+ if (typeof e.repoUrl === "string") site.repoUrl = e.repoUrl;
541
+ if (typeof e.meta === "object" && e.meta !== null) {
542
+ site.meta = e.meta;
543
+ }
544
+ return site;
545
+ });
546
+ }
547
+ function fromJsonFile(path) {
548
+ return async () => {
549
+ const raw = JSON.parse(await readFile3(path, "utf-8"));
550
+ return validate(raw);
551
+ };
552
+ }
553
+
554
+ // src/cli/fleet/resolve-sites.ts
555
+ async function resolveSites(input) {
556
+ if (input.site && input.fleet) {
557
+ throw Object.assign(new Error("cannot combine a positional [site] with --fleet"), {
558
+ exitCode: 2
559
+ });
560
+ }
561
+ if (input.fleet) {
562
+ const fleetPath = resolve(input.cwd, input.fleet);
563
+ const ext = extname(fleetPath).toLowerCase();
564
+ let provider;
565
+ if (ext === ".json") {
566
+ provider = fromJsonFile(fleetPath);
567
+ } else if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
568
+ const mod = await import(pathToFileURL(fleetPath).href);
569
+ if (!mod.default || typeof mod.default !== "function") {
570
+ throw Object.assign(new Error(`--fleet ${input.fleet}: default export is not a function`), {
571
+ exitCode: 2
572
+ });
573
+ }
574
+ provider = mod.default;
575
+ } else {
576
+ throw Object.assign(
577
+ new Error(`--fleet ${input.fleet}: unsupported extension ${ext || "(none)"}`),
578
+ { exitCode: 2 }
579
+ );
580
+ }
581
+ return provider();
582
+ }
583
+ return localPath(resolve(input.cwd, input.site ?? input.cwd))();
584
+ }
585
+
586
+ // src/cli/fleet/clone-if-needed.ts
587
+ import { stat, readdir, mkdir } from "fs/promises";
588
+ import { join as join5 } from "path";
589
+ function deriveNameFromRepoUrl(repoUrl) {
590
+ const slash = repoUrl.split("/").pop() ?? repoUrl;
591
+ return slash.replace(/\.git$/, "");
592
+ }
593
+ async function isNonEmptyDir(path) {
594
+ try {
595
+ const s = await stat(path);
596
+ if (!s.isDirectory()) return false;
597
+ const entries = await readdir(path);
598
+ return entries.length > 0;
599
+ } catch {
600
+ return false;
601
+ }
602
+ }
603
+ async function cloneIfNeeded(site, opts) {
604
+ if (await isNonEmptyDir(site.path)) return site;
605
+ if (!site.repoUrl) {
606
+ throw new Error(`site path does not exist (${site.path}) and no repoUrl is set \u2014 cannot clone`);
607
+ }
608
+ const name = site.name ?? deriveNameFromRepoUrl(site.repoUrl);
609
+ const target = join5(opts.workdir, name);
610
+ await mkdir(opts.workdir, { recursive: true });
611
+ if (await isNonEmptyDir(target)) {
612
+ return { ...site, name, path: target };
613
+ }
614
+ const spawn2 = opts.spawn ?? defaultSpawn;
615
+ const result = await spawn2("git", ["clone", site.repoUrl, target], {
616
+ cwd: opts.workdir,
617
+ timeoutMs: 5 * 6e4
618
+ });
619
+ if (result.code !== 0) {
620
+ throw new Error(`git clone failed (code ${result.code}): ${result.stderr}`);
621
+ }
622
+ return { ...site, name, path: target };
623
+ }
624
+
625
+ // src/cli/commands/audit.ts
626
+ function parseOnly(value) {
627
+ if (!value) return void 0;
628
+ const names = value.split(",").map((s) => s.trim());
629
+ for (const n of names) {
630
+ if (!ALL_AUDIT_NAMES.includes(n)) {
631
+ throw Object.assign(new Error(`unknown audit in --only: ${n}`), { exitCode: 2 });
632
+ }
633
+ }
634
+ return names;
635
+ }
636
+ function formatTable(results) {
637
+ return results.map((r) => `${r.audit.padEnd(12)} ${r.status.padEnd(5)} ${r.site}
638
+ ${r.summary}`).join("\n");
639
+ }
640
+ function exitCode(results) {
641
+ return results.some((r) => r.status === "fail") ? 1 : 0;
642
+ }
643
+ async function runAuditCommand(site, opts) {
644
+ const which = parseOnly(opts.only);
645
+ const cwd = opts.cwd ? resolve2(opts.cwd) : process.cwd();
646
+ let sites = await resolveSites({
647
+ ...site !== void 0 ? { site } : {},
648
+ ...opts.fleet !== void 0 ? { fleet: opts.fleet } : {},
649
+ cwd
650
+ });
651
+ if (opts.fleet) {
652
+ const workdir = opts.workdir ?? `${process.env.HOME ?? ""}/.reddoor-maint/sites`;
653
+ sites = await Promise.all(sites.map((s) => cloneIfNeeded(s, { workdir })));
654
+ }
655
+ const results = [];
656
+ for (const s of sites) {
657
+ const r = await runAudits(s, which);
658
+ results.push(...r);
659
+ }
660
+ const output = opts.json ? JSON.stringify(results, null, 2) : formatTable(results);
661
+ return { output, code: exitCode(results) };
662
+ }
663
+ export {
664
+ runAuditCommand
665
+ };
666
+ //# sourceMappingURL=audit.js.map