@reddoorla/maintenance 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -260,59 +260,64 @@ function extractAdvisoriesFromNpm(parsed) {
260
260
  }
261
261
  return [...roots.values()];
262
262
  }
263
- async function tryRun(spawn2, cmd, args, cwd) {
263
+ async function runAuditTool(spawn2, cmd, args, cwd) {
264
+ let raw;
264
265
  try {
265
- return await spawn2(cmd, args, { cwd });
266
+ raw = await spawn2(cmd, args, { cwd });
266
267
  } catch (err) {
267
268
  const e = err;
268
- if (e.code === "ENOENT" || /ENOENT/.test(String(err))) return { missing: true };
269
- throw err;
270
- }
271
- }
272
- async function securityAudit(ctx) {
273
- const spawn2 = ctx.spawn ?? defaultSpawn;
274
- const site = ctx.site;
275
- const label = siteLabel3(site);
276
- let used = "pnpm audit";
277
- let raw = await tryRun(
278
- spawn2,
279
- "pnpm",
280
- ["audit", "--json", "--prod"],
281
- site.path
282
- );
283
- if ("missing" in raw) {
284
- used = "npm audit";
285
- raw = await tryRun(spawn2, "npm", ["audit", "--json", "--omit=dev"], site.path);
286
- }
287
- if ("missing" in raw) {
288
- return {
289
- audit: "security",
290
- site: label,
291
- status: "skip",
292
- summary: "neither pnpm nor npm is available on PATH"
293
- };
269
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) return { kind: "missing" };
270
+ return { kind: "error", reason: `spawn failed: ${String(err).slice(0, 200)}` };
294
271
  }
295
272
  if (raw.code !== 0 && raw.code !== 1) {
296
273
  return {
297
- audit: "security",
298
- site: label,
299
- status: "skip",
300
- summary: `${used} exited with code ${raw.code}`,
301
- details: { stderr: raw.stderr }
274
+ kind: "error",
275
+ reason: `exit ${raw.code}${raw.stderr ? `: ${raw.stderr.slice(0, 150)}` : ""}`
302
276
  };
303
277
  }
304
278
  let parsed;
305
279
  try {
306
- parsed = JSON.parse(raw.stdout);
280
+ parsed = JSON.parse(raw.stdout || "{}");
307
281
  } catch (err) {
308
- return {
309
- audit: "security",
310
- site: label,
311
- status: "skip",
312
- summary: `${used} produced unparseable JSON`,
313
- details: { error: String(err), stdout: raw.stdout.slice(0, 500) }
314
- };
282
+ return { kind: "error", reason: `unparseable JSON: ${String(err).slice(0, 100)}` };
283
+ }
284
+ const errEnvelope = parsed.error;
285
+ if (errEnvelope && typeof errEnvelope === "object") {
286
+ return { kind: "error", reason: errEnvelope.code ?? "error envelope returned" };
315
287
  }
288
+ if (!parsed.metadata?.vulnerabilities) {
289
+ return { kind: "error", reason: "no metadata.vulnerabilities in output" };
290
+ }
291
+ return { kind: "ok", parsed };
292
+ }
293
+ async function securityAudit(ctx) {
294
+ const spawn2 = ctx.spawn ?? defaultSpawn;
295
+ const site = ctx.site;
296
+ const label = siteLabel3(site);
297
+ let used = "pnpm audit";
298
+ let result = await runAuditTool(spawn2, "pnpm", ["audit", "--json", "--prod"], site.path);
299
+ if (result.kind !== "ok") {
300
+ const pnpmReason = result.kind === "missing" ? "not installed" : result.reason;
301
+ const npmResult = await runAuditTool(
302
+ spawn2,
303
+ "npm",
304
+ ["audit", "--json", "--omit=dev"],
305
+ site.path
306
+ );
307
+ if (npmResult.kind === "ok") {
308
+ result = npmResult;
309
+ used = "npm audit";
310
+ } else {
311
+ const npmReason = npmResult.kind === "missing" ? "not installed" : npmResult.reason;
312
+ return {
313
+ audit: "security",
314
+ site: label,
315
+ status: "skip",
316
+ summary: `cannot run audit \u2014 pnpm: ${pnpmReason}; npm: ${npmReason}`
317
+ };
318
+ }
319
+ }
320
+ const parsed = result.parsed;
316
321
  const counts = {
317
322
  low: parsed.metadata?.vulnerabilities?.low ?? 0,
318
323
  moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,
@@ -342,7 +347,10 @@ var lighthouseConfig = {
342
347
  ci: {
343
348
  collect: {
344
349
  url: ["http://localhost:5173/dev/a11y-fixtures"],
345
- startServerCommand: "pnpm vite:dev",
350
+ // `npm run vite:dev` works on both pnpm and npm sites — pnpm respects
351
+ // the `run` form too. Keeps this config portable across the fleet
352
+ // while sites transition to pnpm.
353
+ startServerCommand: "npm run vite:dev",
346
354
  startServerReadyPattern: "ready in",
347
355
  startServerReadyTimeout: 12e4,
348
356
  numberOfRuns: 1,
@@ -494,7 +502,8 @@ var playwrightA11yConfig = defineConfig({
494
502
  }
495
503
  ],
496
504
  webServer: {
497
- command: "pnpm vite:dev",
505
+ // Portable across pnpm and npm sites — pnpm respects `npm run` too.
506
+ command: "npm run vite:dev",
498
507
  url: "http://localhost:5173/dev/a11y-fixtures",
499
508
  reuseExistingServer: !process.env.CI,
500
509
  timeout: 12e4
@@ -1238,8 +1247,124 @@ async function upgradeSvelte4to5(site, opts = {}) {
1238
1247
  };
1239
1248
  }
1240
1249
 
1250
+ // src/recipes/convert-to-pnpm.ts
1251
+ import { rm as rm3, stat } from "fs/promises";
1252
+ import { join as join12 } from "path";
1253
+
1254
+ // src/recipes/convert-to-pnpm/script-rewrites.ts
1255
+ function rewriteScriptForPnpm(script) {
1256
+ let out = script;
1257
+ out = out.replace(/\bnpm run(?=\s)/g, "pnpm run");
1258
+ out = out.replace(/\bnpx(?=\s)/g, "pnpm dlx");
1259
+ return out;
1260
+ }
1261
+ function rewriteScriptsForPnpm(scripts) {
1262
+ const next = {};
1263
+ let changedCount = 0;
1264
+ for (const [name, value] of Object.entries(scripts)) {
1265
+ const rewritten = rewriteScriptForPnpm(value);
1266
+ next[name] = rewritten;
1267
+ if (rewritten !== value) changedCount++;
1268
+ }
1269
+ return { scripts: next, changedCount };
1270
+ }
1271
+
1272
+ // src/recipes/convert-to-pnpm.ts
1273
+ var DEFAULT_PNPM_VERSION = "10.33.1";
1274
+ async function exists(path) {
1275
+ try {
1276
+ await stat(path);
1277
+ return true;
1278
+ } catch {
1279
+ return false;
1280
+ }
1281
+ }
1282
+ function siteLabel9(site) {
1283
+ return site.name ?? site.path;
1284
+ }
1285
+ async function convertToPnpm(site, opts = {}) {
1286
+ const label = siteLabel9(site);
1287
+ const spawn2 = opts.spawn ?? defaultSpawn;
1288
+ const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
1289
+ const pnpmLockPath = join12(site.path, "pnpm-lock.yaml");
1290
+ const npmLockPath = join12(site.path, "package-lock.json");
1291
+ const yarnLockPath = join12(site.path, "yarn.lock");
1292
+ if (await exists(pnpmLockPath)) {
1293
+ return {
1294
+ recipe: "convert-to-pnpm",
1295
+ site: label,
1296
+ status: "noop",
1297
+ commits: [],
1298
+ notes: "site already has pnpm-lock.yaml"
1299
+ };
1300
+ }
1301
+ const hasNpmLock = await exists(npmLockPath);
1302
+ const hasYarnLock = await exists(yarnLockPath);
1303
+ if (!hasNpmLock && !hasYarnLock) {
1304
+ return {
1305
+ recipe: "convert-to-pnpm",
1306
+ site: label,
1307
+ status: "noop",
1308
+ commits: [],
1309
+ notes: "no convertible lockfile (package-lock.json or yarn.lock) at site root"
1310
+ };
1311
+ }
1312
+ if (!await isWorkingTreeClean(site.path)) {
1313
+ throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
1314
+ }
1315
+ const branch = branchName("convert-to-pnpm");
1316
+ await createBranch(site.path, branch);
1317
+ const shas = [];
1318
+ if (hasNpmLock) await rm3(npmLockPath, { force: true });
1319
+ if (hasYarnLock) await rm3(yarnLockPath, { force: true });
1320
+ const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
1321
+ const lockSha = await commit(site.path, `chore(pnpm): remove ${sourceLock}`);
1322
+ if (lockSha) shas.push(lockSha);
1323
+ const pkgPath = join12(site.path, "package.json");
1324
+ const pkg = await readPackageJson(pkgPath);
1325
+ const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
1326
+ if (pkg.scripts && typeof pkg.scripts === "object") {
1327
+ const { scripts: rewritten, changedCount } = rewriteScriptsForPnpm(
1328
+ pkg.scripts
1329
+ );
1330
+ if (changedCount > 0) {
1331
+ next.scripts = rewritten;
1332
+ }
1333
+ }
1334
+ await writePackageJson(pkgPath, next);
1335
+ const pkgSha = await commit(site.path, "chore(pnpm): pin packageManager + rewrite npm scripts");
1336
+ if (pkgSha) shas.push(pkgSha);
1337
+ const installResult = await spawn2("pnpm", ["install"], {
1338
+ cwd: site.path,
1339
+ streaming: true
1340
+ });
1341
+ if (installResult.code !== 0) {
1342
+ return {
1343
+ recipe: "convert-to-pnpm",
1344
+ site: label,
1345
+ status: "failed",
1346
+ commits: shas,
1347
+ notes: `pnpm install failed (exit ${installResult.code}). branch ${branch} left for inspection.`
1348
+ };
1349
+ }
1350
+ const installSha = await commit(site.path, "chore(pnpm): add pnpm-lock.yaml");
1351
+ if (installSha) shas.push(installSha);
1352
+ return {
1353
+ recipe: "convert-to-pnpm",
1354
+ site: label,
1355
+ status: "applied",
1356
+ commits: shas,
1357
+ notes: `branch: ${branch}`
1358
+ };
1359
+ }
1360
+
1241
1361
  // src/recipes/index.ts
1242
- var ALL_RECIPE_NAMES = ["sync-configs", "bump-deps", "svelte-4-to-5"];
1362
+ var ALL_RECIPE_NAMES = [
1363
+ "sync-configs",
1364
+ "bump-deps",
1365
+ "svelte-4-to-5",
1366
+ "convert-to-pnpm"
1367
+ ];
1243
1368
  function isRecipeName(value) {
1244
1369
  return ALL_RECIPE_NAMES.includes(value);
1245
1370
  }
@@ -1291,6 +1416,7 @@ export {
1291
1416
  ALL_RECIPE_NAMES,
1292
1417
  a11yAudit,
1293
1418
  bumpDeps,
1419
+ convertToPnpm,
1294
1420
  depsAudit,
1295
1421
  fromJsonFile,
1296
1422
  isRecipeName,