@lzear/repo-lint 4.2.2 → 4.4.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
@@ -1,149 +1,565 @@
1
1
  // src/index.ts
2
2
  import { execSync } from "child_process";
3
- import { existsSync as existsSync2 } from "fs";
3
+ import { existsSync as existsSync3 } from "fs";
4
4
  import { tmpdir } from "os";
5
- import path2 from "path";
5
+ import path3 from "path";
6
6
 
7
7
  // src/checks.ts
8
+ import { spawnSync as spawnSync2 } from "child_process";
9
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
10
+ import path2 from "path";
11
+ import { fileURLToPath } from "url";
12
+ import { run as ncuRun2 } from "npm-check-updates";
13
+ import { publint } from "publint";
14
+ import { maxSatisfying, satisfies } from "semver";
15
+
16
+ // src/update.ts
8
17
  import { spawnSync } from "child_process";
9
- import { existsSync, readdirSync, readFileSync } from "fs";
18
+ import { existsSync, readFileSync, writeFileSync } from "fs";
10
19
  import path from "path";
11
- import { fileURLToPath } from "url";
20
+ import { pathToFileURL } from "url";
12
21
  import { run as ncuRun } from "npm-check-updates";
13
- import { publint } from "publint";
14
- var _dirname = path.dirname(fileURLToPath(import.meta.url));
15
- var readPkg = (dir) => {
16
- const f = path.join(dir, "package.json");
17
- if (!existsSync(f)) return null;
22
+ var LOCKFILES = [
23
+ ["bun.lock", "bun"],
24
+ ["bun.lockb", "bun"],
25
+ ["pnpm-lock.yaml", "pnpm"],
26
+ ["yarn.lock", "yarn"],
27
+ ["package-lock.json", "npm"]
28
+ ];
29
+ var PM_FIELD_RE = /^(npm|yarn|pnpm|bun)@([^+\s]+)/;
30
+ var readJsonFile = (file) => {
31
+ const raw = readFileSync(file, "utf8");
32
+ return {
33
+ data: JSON.parse(raw),
34
+ indent: /^[ \t]+/m.exec(raw)?.[0] ?? " ",
35
+ trailingNewline: raw.endsWith("\n")
36
+ };
37
+ };
38
+ var writeJsonFile = (file, json) => {
39
+ writeFileSync(
40
+ file,
41
+ JSON.stringify(json.data, null, json.indent) + (json.trailingNewline ? "\n" : ""),
42
+ "utf8"
43
+ );
44
+ };
45
+ var detectPackageManager = (dir) => {
46
+ const packageFile = path.join(dir, "package.json");
47
+ if (existsSync(packageFile))
48
+ try {
49
+ const package_ = JSON.parse(readFileSync(packageFile, "utf8"));
50
+ if (typeof package_.packageManager === "string") {
51
+ const match = PM_FIELD_RE.exec(package_.packageManager);
52
+ if (match?.[1] && match[2])
53
+ return {
54
+ name: match[1],
55
+ version: match[2],
56
+ source: "packageManager"
57
+ };
58
+ }
59
+ } catch {
60
+ }
61
+ for (const [file, name] of LOCKFILES)
62
+ if (existsSync(path.join(dir, file))) return { name, source: "lockfile" };
63
+ return { name: "npm", source: "default" };
64
+ };
65
+ var fetchJson = async (url) => {
66
+ const response = await fetch(url);
67
+ if (!response.ok) throw new Error(`GET ${url} \u2014 HTTP ${response.status}`);
68
+ return await response.json();
69
+ };
70
+ var pmRegistryName = (name, currentMajor) => name === "yarn" && currentMajor >= 2 ? "@yarnpkg%2Fcli-dist" : name;
71
+ var latestPackageVersion = async (name) => {
72
+ const data = await fetchJson(`https://registry.npmjs.org/${name}/latest`);
73
+ if (typeof data.version !== "string")
74
+ throw new Error(`no version in registry response for ${name}`);
75
+ return data.version;
76
+ };
77
+ var hasWorkspaces = (dir) => {
78
+ try {
79
+ const package_ = JSON.parse(
80
+ readFileSync(path.join(dir, "package.json"), "utf8")
81
+ );
82
+ const ws = package_.workspaces;
83
+ if (Array.isArray(ws)) return ws.length > 0;
84
+ const packages = ws?.packages;
85
+ return Array.isArray(packages) && packages.length > 0;
86
+ } catch {
87
+ return false;
88
+ }
89
+ };
90
+ var NCURC_FILES = [
91
+ ".ncurc.json",
92
+ ".ncurc.js",
93
+ ".ncurc.cjs",
94
+ ".ncurc.mjs",
95
+ ".ncurc"
96
+ ];
97
+ var loadNcuRc = async (dir) => {
98
+ for (const name of NCURC_FILES) {
99
+ const file = path.join(dir, name);
100
+ if (!existsSync(file)) continue;
101
+ if (name === ".ncurc" || name.endsWith(".json"))
102
+ return {
103
+ file: name,
104
+ config: JSON.parse(readFileSync(file, "utf8"))
105
+ };
106
+ const module_ = await import(pathToFileURL(file).href);
107
+ return { file: name, config: module_.default ?? module_ };
108
+ }
109
+ return { config: {} };
110
+ };
111
+ var stepDeps = async (dir, isDry) => {
112
+ const base = { id: "deps", desc: "dependency ranges (ncu)" };
113
+ try {
114
+ const isWorkspaces = hasWorkspaces(dir);
115
+ const rc = await loadNcuRc(dir);
116
+ const result = await ncuRun({
117
+ ...rc.config,
118
+ packageFile: path.join(dir, "package.json"),
119
+ cwd: dir,
120
+ upgrade: !isDry,
121
+ silent: true,
122
+ dep: ["prod", "dev", "optional", "peer"],
123
+ ...isWorkspaces && { workspaces: true, root: true }
124
+ });
125
+ const entries = isWorkspaces ? Object.values(result).flatMap(
126
+ (x) => Object.entries(x)
127
+ ) : Object.entries(result);
128
+ const rcNote = rc.file ? ` (using ${rc.file})` : "";
129
+ if (entries.length === 0)
130
+ return {
131
+ ...base,
132
+ pass: true,
133
+ changed: false,
134
+ detail: `up to date${rcNote}`
135
+ };
136
+ return {
137
+ ...base,
138
+ pass: true,
139
+ changed: true,
140
+ detail: entries.map(([k, v]) => `${k} \u2192 ${v}`).join("\n") + rcNote
141
+ };
142
+ } catch (error) {
143
+ return { ...base, pass: false, changed: false, detail: String(error) };
144
+ }
145
+ };
146
+ var stepPackageManager = async (dir, isDry) => {
147
+ const base = { id: "package-manager", desc: "packageManager field" };
148
+ try {
149
+ const file = path.join(dir, "package.json");
150
+ const json = readJsonFile(file);
151
+ if (typeof json.data.packageManager !== "string")
152
+ return {
153
+ ...base,
154
+ pass: true,
155
+ changed: false,
156
+ detail: "no packageManager field"
157
+ };
158
+ const match = PM_FIELD_RE.exec(json.data.packageManager);
159
+ if (!match)
160
+ return {
161
+ ...base,
162
+ pass: true,
163
+ changed: false,
164
+ detail: `unrecognized: ${json.data.packageManager}`
165
+ };
166
+ const [, name, current] = match;
167
+ const major = Number(current.split(".", 1)[0]);
168
+ const latest = await latestPackageVersion(pmRegistryName(name, major));
169
+ if (latest === current)
170
+ return {
171
+ ...base,
172
+ pass: true,
173
+ changed: false,
174
+ detail: `${name}@${current} is latest`
175
+ };
176
+ json.data.packageManager = `${name}@${latest}`;
177
+ if (!isDry) writeJsonFile(file, json);
178
+ return {
179
+ ...base,
180
+ pass: true,
181
+ changed: true,
182
+ detail: `${name} ${current} \u2192 ${latest}`
183
+ };
184
+ } catch (error) {
185
+ return { ...base, pass: false, changed: false, detail: String(error) };
186
+ }
187
+ };
188
+ var updateVersionFile = (file, latest, isDry) => {
189
+ const raw = readFileSync(file, "utf8");
190
+ const current = raw.trim();
191
+ const isVPrefixed = current.startsWith("v");
192
+ const next = isVPrefixed ? `v${latest}` : latest;
193
+ if (current === next)
194
+ return { changed: false, detail: `${path.basename(file)} is latest` };
195
+ if (!isDry)
196
+ writeFileSync(file, next + (raw.endsWith("\n") ? "\n" : ""), "utf8");
197
+ return {
198
+ changed: true,
199
+ detail: `${path.basename(file)} ${current} \u2192 ${next}`
200
+ };
201
+ };
202
+ var stepNodeVersion = async (dir, isDry) => {
203
+ const base = { id: "node-version", desc: "node version files" };
204
+ const files = [".nvmrc", ".node-version"].map((f) => path.join(dir, f)).filter((f) => existsSync(f));
205
+ if (files.length === 0)
206
+ return { ...base, pass: true, changed: false, detail: "no version file" };
207
+ try {
208
+ const response = await fetch("https://nodejs.org/dist/index.json");
209
+ if (!response.ok)
210
+ throw new Error(
211
+ `GET nodejs.org/dist/index.json \u2014 HTTP ${response.status}`
212
+ );
213
+ const index = await response.json();
214
+ const lts = index.find((v) => v.lts);
215
+ if (!lts) throw new Error("no LTS release found");
216
+ const latest = lts.version.replace(/^v/, "");
217
+ const results = files.map((f) => updateVersionFile(f, latest, isDry));
218
+ return {
219
+ ...base,
220
+ pass: true,
221
+ changed: results.some((r) => r.changed),
222
+ detail: results.map((r) => r.detail).join("\n")
223
+ };
224
+ } catch (error) {
225
+ return { ...base, pass: false, changed: false, detail: String(error) };
226
+ }
227
+ };
228
+ var stepBunVersion = async (dir, isDry) => {
229
+ const base = { id: "bun-version", desc: "bun version file" };
230
+ const file = path.join(dir, ".bun-version");
231
+ if (!existsSync(file))
232
+ return { ...base, pass: true, changed: false, detail: "no version file" };
233
+ try {
234
+ const latest = await latestPackageVersion("bun");
235
+ const result = updateVersionFile(file, latest, isDry);
236
+ return { ...base, pass: true, ...result };
237
+ } catch (error) {
238
+ return { ...base, pass: false, changed: false, detail: String(error) };
239
+ }
240
+ };
241
+ var LICENSE_FILES = ["LICENSE", "LICENSE.md", "LICENSE.txt"];
242
+ var COPYRIGHT_RE = /(copyright(?:\s+\(c\)|\s+©)?\s+)(\d{4})(?:\s*-\s*(\d{4}))?/i;
243
+ var stepLicenseYear = (dir, isDry) => {
244
+ const base = { id: "license-year", desc: "LICENSE copyright year" };
245
+ const file = LICENSE_FILES.map((f) => path.join(dir, f)).find(
246
+ (f) => existsSync(f)
247
+ );
248
+ if (!file)
249
+ return { ...base, pass: true, changed: false, detail: "no license file" };
250
+ const raw = readFileSync(file, "utf8");
251
+ const match = COPYRIGHT_RE.exec(raw);
252
+ if (!match?.[2])
253
+ return {
254
+ ...base,
255
+ pass: true,
256
+ changed: false,
257
+ detail: "no copyright year found"
258
+ };
259
+ const year = (/* @__PURE__ */ new Date()).getFullYear();
260
+ const start = match[2];
261
+ const end = Number(match[3] ?? start);
262
+ if (end >= year)
263
+ return {
264
+ ...base,
265
+ pass: true,
266
+ changed: false,
267
+ detail: `${start}${match[3] ? "-" + match[3] : ""} is current`
268
+ };
269
+ const current = match[0];
270
+ const next = `${match[1] ?? ""}${start}-${year}`;
271
+ if (!isDry)
272
+ writeFileSync(
273
+ file,
274
+ raw.replace(COPYRIGHT_RE, () => next),
275
+ "utf8"
276
+ );
277
+ return {
278
+ ...base,
279
+ pass: true,
280
+ changed: true,
281
+ detail: `${current.trim()} \u2192 ${next.trim()}`
282
+ };
283
+ };
284
+ var installCommands = (pm) => {
285
+ switch (pm.name) {
286
+ case "bun":
287
+ return [["bun", "update"]];
288
+ case "pnpm":
289
+ return [
290
+ ["pnpm", "install"],
291
+ ["pnpm", "update", "--recursive"],
292
+ ["pnpm", "dedupe"]
293
+ ];
294
+ case "yarn":
295
+ return pm.version?.startsWith("1.") ? [
296
+ ["yarn", "install"],
297
+ ["yarn", "upgrade"]
298
+ ] : [
299
+ ["yarn", "install"],
300
+ ["yarn", "up", "--recursive", "*"],
301
+ ["yarn", "dedupe"]
302
+ ];
303
+ case "npm":
304
+ return [
305
+ ["npm", "install"],
306
+ ["npm", "update"],
307
+ ["npm", "dedupe"]
308
+ ];
309
+ }
310
+ };
311
+ var stepInstall = (dir, pm) => {
312
+ const base = {
313
+ id: "install",
314
+ desc: `install & refresh lockfile (${pm.name})`
315
+ };
316
+ const lines = [];
317
+ for (const [command, ...arguments_] of installCommands(pm)) {
318
+ lines.push(`$ ${[command, ...arguments_].join(" ")}`);
319
+ const result = spawnSync(command, arguments_, {
320
+ cwd: dir,
321
+ stdio: "inherit"
322
+ });
323
+ if (result.status !== 0)
324
+ return {
325
+ ...base,
326
+ pass: false,
327
+ changed: true,
328
+ detail: `${lines.join("\n")}
329
+ exited with ${result.status ?? "signal"}`
330
+ };
331
+ }
332
+ return { ...base, pass: true, changed: true, detail: lines.join("\n") };
333
+ };
334
+ var runUpdate = async (options = {}) => {
335
+ const {
336
+ dir = process.cwd(),
337
+ dry: isDry = false,
338
+ install: isInstall = true
339
+ } = options;
340
+ const packageManager = detectPackageManager(dir);
341
+ const results = [
342
+ await stepDeps(dir, isDry),
343
+ await stepPackageManager(dir, isDry),
344
+ await stepNodeVersion(dir, isDry),
345
+ await stepBunVersion(dir, isDry),
346
+ stepLicenseYear(dir, isDry)
347
+ ];
348
+ if (isInstall && !isDry) results.push(stepInstall(dir, packageManager));
349
+ return { dir, packageManager, results };
350
+ };
351
+
352
+ // src/checks.ts
353
+ var _dirname = path2.dirname(fileURLToPath(import.meta.url));
354
+ var readPackage = (dir) => {
355
+ const f = path2.join(dir, "package.json");
356
+ if (!existsSync2(f)) return null;
18
357
  try {
19
- return JSON.parse(readFileSync(f, "utf8"));
358
+ return JSON.parse(readFileSync2(f, "utf8"));
20
359
  } catch {
21
360
  return null;
22
361
  }
23
362
  };
24
- var getWorkspacePatterns = (pkg) => {
25
- const ws = pkg.workspaces;
363
+ var getWorkspacePatterns = (package_) => {
364
+ const ws = package_.workspaces;
26
365
  if (Array.isArray(ws)) return ws;
27
- const wsPkg = ws;
28
- if (Array.isArray(wsPkg?.packages)) return wsPkg.packages;
366
+ const wsPackage = ws;
367
+ if (Array.isArray(wsPackage?.packages)) return wsPackage.packages;
29
368
  return [];
30
369
  };
31
370
  var patternToBase = (rootDir, pattern) => {
32
371
  const parts = pattern.split("/");
33
372
  if (parts.length === 2 && parts[1] === "*")
34
- return path.join(rootDir, parts[0] ?? "");
373
+ return path2.join(rootDir, parts[0] ?? "");
35
374
  if (parts.length === 1 && parts[0] === "*") return rootDir;
36
375
  return null;
37
376
  };
38
- var getWorkspaceDirs = (rootDir) => {
39
- const pkg = readPkg(rootDir);
40
- if (!pkg) return [];
41
- const dirs = [];
42
- for (const pattern of getWorkspacePatterns(pkg)) {
377
+ var getWorkspaceDirectories = (rootDir) => {
378
+ const package_ = readPackage(rootDir);
379
+ if (!package_) return [];
380
+ const directories = [];
381
+ for (const pattern of getWorkspacePatterns(package_)) {
43
382
  const base = patternToBase(rootDir, pattern);
44
- if (!base || !existsSync(base)) continue;
45
- for (const entry of readdirSync(base, { withFileTypes: true }))
46
- if (entry.isDirectory()) dirs.push(path.join(base, entry.name));
383
+ if (!base || !existsSync2(base)) continue;
384
+ const entries = readdirSync(base, { withFileTypes: true });
385
+ for (const entry of entries)
386
+ if (entry.isDirectory()) directories.push(path2.join(base, entry.name));
47
387
  }
48
- return dirs;
388
+ return directories;
49
389
  };
50
390
  var findBin = (name, startDir) => {
51
391
  let dir = startDir;
52
392
  while (true) {
53
- const bin = path.join(dir, "node_modules", ".bin", name);
54
- if (existsSync(bin)) return bin;
55
- const parent = path.join(dir, "..");
393
+ const bin = path2.join(dir, "node_modules", ".bin", name);
394
+ if (existsSync2(bin)) return bin;
395
+ const parent = path2.join(dir, "..");
56
396
  if (parent === dir) return null;
57
397
  dir = parent;
58
398
  }
59
399
  };
60
- var eachPublishedPkg = async (dir, fn) => {
61
- const pkg = readPkg(dir);
62
- if (!pkg) return { pass: true };
63
- if (pkg.private === true) {
64
- const wsDirs = getWorkspaceDirs(dir);
65
- if (wsDirs.length === 0) return { pass: true };
400
+ var eachPublishedPackage = async (dir, function_) => {
401
+ const package_ = readPackage(dir);
402
+ if (!package_) return { pass: true };
403
+ if (package_.private === true) {
404
+ const wsDirectories = getWorkspaceDirectories(dir);
405
+ if (wsDirectories.length === 0) return { pass: true };
66
406
  const results = await Promise.all(
67
- wsDirs.map((d) => eachPublishedPkg(d, fn))
407
+ wsDirectories.map((d) => eachPublishedPackage(d, function_))
68
408
  );
69
409
  const failures = results.filter((r) => !r.pass);
70
410
  if (failures.length === 0) return { pass: true };
71
411
  const detail = failures.flatMap((r) => r.detail ? [r.detail] : []).join("\n");
72
412
  return { pass: false, ...detail && { detail } };
73
413
  }
74
- return fn(dir);
414
+ return function_(dir);
75
415
  };
76
416
  var hasPublishedPkg = (dir) => {
77
- const pkg = readPkg(dir);
78
- if (!pkg) return false;
79
- if (pkg.private !== true) return true;
80
- return getWorkspaceDirs(dir).some((d) => hasPublishedPkg(d));
417
+ const package_ = readPackage(dir);
418
+ if (!package_) return false;
419
+ if (package_.private !== true) return true;
420
+ return getWorkspaceDirectories(dir).some((d) => hasPublishedPkg(d));
81
421
  };
82
422
  var readmeIncludes = (dir, needle) => {
83
- const f = path.join(dir, "README.md");
84
- return existsSync(f) && readFileSync(f, "utf8").includes(needle);
423
+ const f = path2.join(dir, "README.md");
424
+ return existsSync2(f) && readFileSync2(f, "utf8").includes(needle);
425
+ };
426
+ var FORGE_WORKFLOW_RE = /uses:\s*lzear\/forge\/\.github\/workflows\/ci\.yml@/;
427
+ var isForgeRepo = (dir) => getWorkspaceDirectories(dir).some(
428
+ (d) => readPackage(d)?.name === "@lzear/forge"
429
+ );
430
+ var callsForgeWorkflow = (dir) => {
431
+ const wfDir = path2.join(dir, ".github", "workflows");
432
+ if (!existsSync2(wfDir)) return false;
433
+ return readdirSync(wfDir).filter((f) => /\.ya?ml$/.test(f)).some(
434
+ (f) => FORGE_WORKFLOW_RE.test(readFileSync2(path2.join(wfDir, f), "utf8"))
435
+ );
436
+ };
437
+ var auditCommand = (pm) => {
438
+ switch (pm.name) {
439
+ case "npm":
440
+ return ["npm", "audit", "--omit", "dev", "--audit-level", "high"];
441
+ case "pnpm":
442
+ return ["pnpm", "audit", "--prod", "--audit-level", "high"];
443
+ case "bun":
444
+ return ["bun", "audit"];
445
+ case "yarn":
446
+ return pm.version?.startsWith("1.") ? ["yarn", "audit", "--level", "high"] : [
447
+ "yarn",
448
+ "npm",
449
+ "audit",
450
+ "--environment",
451
+ "production",
452
+ "--severity",
453
+ "high"
454
+ ];
455
+ }
456
+ };
457
+ var DEP_FIELDS = [
458
+ "dependencies",
459
+ "devDependencies",
460
+ "optionalDependencies",
461
+ "peerDependencies"
462
+ ];
463
+ var NON_REGISTRY_RANGE_RE = /^(?:workspace:|file:|link:|portal:|catalog:|git|https?:)/;
464
+ var collectEntry = (dependencies, name, range) => {
465
+ if (typeof range !== "string" || NON_REGISTRY_RANGE_RE.test(range)) return;
466
+ const alias = /^npm:(.+)@([^@]+)$/.exec(range);
467
+ const target = alias?.[1] ?? name;
468
+ const targetRange = alias?.[2] ?? range;
469
+ if (!dependencies.has(target)) dependencies.set(target, targetRange);
470
+ };
471
+ var collectFromPackage = (dependencies, packageDir) => {
472
+ const package_ = readPackage(packageDir);
473
+ for (const field of DEP_FIELDS) {
474
+ const section = package_?.[field];
475
+ if (typeof section !== "object" || section === null) continue;
476
+ for (const [name, range] of Object.entries(section))
477
+ collectEntry(dependencies, name, range);
478
+ }
479
+ };
480
+ var collectDependencies = (dir) => {
481
+ const dependencies = /* @__PURE__ */ new Map();
482
+ for (const packageDir of [dir, ...getWorkspaceDirectories(dir)])
483
+ collectFromPackage(dependencies, packageDir);
484
+ return dependencies;
485
+ };
486
+ var findDeprecated = async (name, range) => {
487
+ const response = await fetch(
488
+ `https://registry.npmjs.org/${name.replace("/", "%2F")}`,
489
+ {
490
+ headers: { accept: "application/vnd.npm.install-v1+json" },
491
+ signal: AbortSignal.timeout(1e4)
492
+ }
493
+ );
494
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
495
+ const data = await response.json();
496
+ const latest = data["dist-tags"]?.latest;
497
+ const resolved = latest && satisfies(latest, range) ? latest : maxSatisfying(Object.keys(data.versions), range);
498
+ if (!resolved) return null;
499
+ const deprecated = data.versions[resolved]?.deprecated;
500
+ return deprecated ? `${name}@${resolved} \u2014 ${deprecated.slice(0, 120)}` : null;
85
501
  };
86
502
  var LOCAL_CHECKS = [
87
503
  {
88
504
  id: "readme-exists",
89
- desc: "README.md exists",
505
+ desc: "README",
90
506
  type: "local",
91
- check: (dir) => existsSync(path.join(dir, "README.md"))
92
- },
93
- {
94
- id: "readme-codacy-grade-badge",
95
- desc: "README has Codacy grade badge",
96
- type: "local",
97
- publishedOnly: true,
98
- check: (dir) => readmeIncludes(dir, "project/badge/Grade/")
99
- },
100
- {
101
- id: "readme-codacy-coverage-badge",
102
- desc: "README has Codacy coverage badge",
103
- type: "local",
104
- publishedOnly: true,
105
- check: (dir) => readmeIncludes(dir, "project/badge/Coverage/")
507
+ check: (dir) => existsSync2(path2.join(dir, "README.md"))
106
508
  },
107
509
  {
108
510
  id: "readme-npm-badge",
109
- desc: "README has npm badge",
511
+ desc: "npm badge",
110
512
  type: "local",
111
513
  publishedOnly: true,
112
514
  check: (dir) => readmeIncludes(dir, "shields.io/npm/v/")
113
515
  },
114
516
  {
115
- id: "codacy-config",
116
- desc: ".codacy.yml exists",
517
+ id: "codacy",
518
+ desc: "Codacy",
117
519
  type: "local",
118
520
  publishedOnly: true,
119
- check: (dir) => existsSync(path.join(dir, ".codacy.yml"))
521
+ check: (dir) => {
522
+ const missing = [
523
+ readmeIncludes(dir, "project/badge/Grade/") ? null : "grade badge missing in README",
524
+ readmeIncludes(dir, "project/badge/Coverage/") ? null : "coverage badge missing in README",
525
+ existsSync2(path2.join(dir, ".codacy.yml")) ? null : ".codacy.yml missing"
526
+ ].filter((m) => m !== null);
527
+ if (missing.length === 0) return true;
528
+ return { pass: false, detail: missing.join("\n") };
529
+ }
120
530
  },
121
531
  {
122
532
  id: "license",
123
- desc: "LICENSE exists",
533
+ desc: "LICENSE",
124
534
  type: "local",
125
535
  publishedOnly: true,
126
- check: (dir) => existsSync(path.join(dir, "LICENSE"))
536
+ check: (dir) => existsSync2(path2.join(dir, "LICENSE"))
127
537
  },
128
538
  {
129
539
  id: "ci-workflow",
130
- desc: "CI workflow exists",
540
+ desc: "CI calls forge workflow",
131
541
  type: "local",
132
542
  publishedOnly: true,
133
- check: (dir) => existsSync(path.join(dir, ".github/workflows/ci.yml"))
543
+ check: (dir) => {
544
+ if (callsForgeWorkflow(dir) || isForgeRepo(dir)) return true;
545
+ return {
546
+ pass: false,
547
+ detail: "no workflow calls the forge reusable CI \u2014 replace the local ci.yml copy with:\n jobs:\n ci:\n uses: lzear/forge/.github/workflows/ci.yml@<sha> # vX.Y.Z\n(pin to a commit SHA; renovate keeps it fresh)"
548
+ };
549
+ }
134
550
  },
135
551
  {
136
552
  id: "renovate",
137
- desc: "renovate.json exists",
553
+ desc: "renovate.json",
138
554
  type: "local",
139
- check: (dir) => existsSync(path.join(dir, "renovate.json"))
555
+ check: (dir) => existsSync2(path2.join(dir, "renovate.json"))
140
556
  },
141
557
  {
142
558
  id: "pkg-publint",
143
- desc: "publint (all published packages)",
559
+ desc: "publint",
144
560
  type: "local",
145
561
  publishedOnly: true,
146
- check: (dir) => eachPublishedPkg(dir, async (pkgDir) => {
562
+ check: (dir) => eachPublishedPackage(dir, async (pkgDir) => {
147
563
  const { messages } = await publint({ pkgDir });
148
564
  const failures = messages.filter(
149
565
  (m) => m.type === "error" || m.type === "warning"
@@ -157,13 +573,13 @@ var LOCAL_CHECKS = [
157
573
  },
158
574
  {
159
575
  id: "pkg-attw",
160
- desc: "attw (all published packages)",
576
+ desc: "attw",
161
577
  type: "local",
162
578
  check: (dir) => {
163
579
  const bin = findBin("attw", _dirname);
164
580
  if (!bin) return { pass: false, detail: "attw not available" };
165
- return eachPublishedPkg(dir, (pkgDir) => {
166
- const r = spawnSync(
581
+ return eachPublishedPackage(dir, (pkgDir) => {
582
+ const r = spawnSync2(
167
583
  process.execPath,
168
584
  [bin, "--pack", "--profile", "esm-only"],
169
585
  {
@@ -179,12 +595,12 @@ var LOCAL_CHECKS = [
179
595
  },
180
596
  {
181
597
  id: "pkg-knip",
182
- desc: "knip (no unused exports/deps)",
598
+ desc: "knip",
183
599
  type: "local",
184
600
  check: (dir) => {
185
601
  const bin = findBin("knip", _dirname);
186
602
  if (!bin) return { pass: false, detail: "knip not available" };
187
- const r = spawnSync(process.execPath, [bin], {
603
+ const r = spawnSync2(process.execPath, [bin], {
188
604
  cwd: dir,
189
605
  encoding: "utf8",
190
606
  stdio: ["ignore", "pipe", "pipe"],
@@ -197,28 +613,90 @@ var LOCAL_CHECKS = [
197
613
  Run: npx knip` };
198
614
  }
199
615
  },
616
+ {
617
+ id: "monorepo-lint",
618
+ desc: "sherif (workspaces)",
619
+ type: "local",
620
+ check: (dir) => {
621
+ const package_ = readPackage(dir);
622
+ if (!package_ || getWorkspacePatterns(package_).length === 0)
623
+ return { pass: true, detail: "not a monorepo" };
624
+ const bin = findBin("sherif", _dirname);
625
+ if (!bin) return { pass: false, detail: "sherif not available" };
626
+ const r = spawnSync2(process.execPath, [bin], {
627
+ cwd: dir,
628
+ encoding: "utf8",
629
+ stdio: ["ignore", "pipe", "pipe"],
630
+ env: { ...process.env, NO_COLOR: "1" }
631
+ });
632
+ if (r.status === 0) return true;
633
+ const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
634
+ return { pass: false, detail };
635
+ }
636
+ },
637
+ {
638
+ id: "deps-audit",
639
+ desc: "audit",
640
+ type: "local",
641
+ check: (dir) => {
642
+ const pm = detectPackageManager(dir);
643
+ const [command, ...arguments_] = auditCommand(pm);
644
+ const r = spawnSync2(command, arguments_, {
645
+ cwd: dir,
646
+ encoding: "utf8",
647
+ stdio: ["ignore", "pipe", "pipe"]
648
+ });
649
+ if (r.error) return { pass: false, detail: String(r.error) };
650
+ if (r.status === 0) return true;
651
+ const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
652
+ return { pass: false, detail: detail || `audit exited ${r.status}` };
653
+ }
654
+ },
655
+ {
656
+ id: "deps-deprecated",
657
+ desc: "no deprecated deps",
658
+ type: "local",
659
+ check: async (dir) => {
660
+ const dependencies = collectDependencies(dir);
661
+ const failures = [];
662
+ await Promise.all(
663
+ [...dependencies].map(async ([name, range]) => {
664
+ try {
665
+ const deprecated = await findDeprecated(name, range);
666
+ if (deprecated) failures.push(deprecated);
667
+ } catch (error) {
668
+ failures.push(`${name} \u2014 check failed: ${String(error)}`);
669
+ }
670
+ })
671
+ );
672
+ if (failures.length === 0) return true;
673
+ return {
674
+ pass: false,
675
+ detail: failures.toSorted((a, b) => a.localeCompare(b)).join("\n")
676
+ };
677
+ }
678
+ },
200
679
  {
201
680
  id: "deps-fresh",
202
- desc: "dependencies up to date (ncu)",
681
+ desc: "deps up to date",
203
682
  type: "local",
204
683
  check: async (dir) => {
205
- const pkg = readPkg(dir);
206
- const hasWorkspaces = Array.isArray(pkg?.workspaces) && pkg.workspaces.length > 0;
684
+ const package_ = readPackage(dir);
685
+ const hasWorkspaces2 = Array.isArray(package_?.workspaces) && package_.workspaces.length > 0;
207
686
  try {
208
- const result = await ncuRun({
209
- packageFile: path.join(dir, "package.json"),
210
- ...hasWorkspaces ? { workspaces: true } : {},
687
+ const result = await ncuRun2({
688
+ packageFile: path2.join(dir, "package.json"),
689
+ ...hasWorkspaces2 && { workspaces: true },
211
690
  silent: true
212
691
  });
213
- const entries = hasWorkspaces ? Object.values(
692
+ const entries = hasWorkspaces2 ? Object.values(
214
693
  result
215
694
  ).flatMap((x) => Object.entries(x)) : Object.entries(result);
216
695
  if (entries.length === 0) return true;
217
696
  const lines = entries.map(([k, v]) => `${k} \u2192 ${v}`).join("\n");
218
- const cmd = hasWorkspaces ? "yarn dlx npm-check-updates --dep dev,optional,peer,prod,packageManager -u --workspaces && yarn install" : "yarn dlx npm-check-updates --dep dev,optional,peer,prod,packageManager -u && yarn install";
219
697
  return { pass: false, detail: `${lines}
220
698
 
221
- Run: ${cmd}` };
699
+ Run: forge update` };
222
700
  } catch (error) {
223
701
  return { pass: false, detail: String(error) };
224
702
  }
@@ -227,7 +705,7 @@ Run: ${cmd}` };
227
705
  ];
228
706
  var listSecrets = (repo) => {
229
707
  try {
230
- const result = spawnSync(
708
+ const result = spawnSync2(
231
709
  "gh",
232
710
  ["secret", "list", "--repo", repo, "--json", "name"],
233
711
  {
@@ -244,7 +722,7 @@ var listSecrets = (repo) => {
244
722
  var REMOTE_CHECKS = [
245
723
  {
246
724
  id: "secret-codacy-token",
247
- desc: "Secret CODACY_PROJECT_TOKEN set",
725
+ desc: "Codacy secret",
248
726
  type: "remote",
249
727
  publishedOnly: true,
250
728
  check: (repo) => {
@@ -265,19 +743,21 @@ var checkLocal = async (options = {}) => {
265
743
  stdio: ["ignore", "pipe", "ignore"]
266
744
  }).trim();
267
745
  const match = /github\.com[:/](.+?)(?:\.git)?$/.exec(remote);
268
- return match?.[1] ?? path2.basename(dir);
746
+ return match?.[1] ?? path3.basename(dir);
269
747
  } catch {
270
748
  return dir;
271
749
  }
272
750
  })();
273
- const published = hasPublishedPkg(dir);
751
+ const isPublished = hasPublishedPkg(dir);
274
752
  const localResults = await Promise.all(
275
- LOCAL_CHECKS.filter((c) => !c.publishedOnly || published).map(async (c) => {
276
- const raw = await c.check(dir);
277
- return typeof raw === "boolean" ? { id: c.id, desc: c.desc, pass: raw } : { id: c.id, desc: c.desc, ...raw };
278
- })
753
+ LOCAL_CHECKS.filter((c) => !c.publishedOnly || isPublished).map(
754
+ async (c) => {
755
+ const raw = await c.check(dir);
756
+ return typeof raw === "boolean" ? { id: c.id, desc: c.desc, pass: raw } : { id: c.id, desc: c.desc, ...raw };
757
+ }
758
+ )
279
759
  );
280
- const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly || published).map((c) => ({
760
+ const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly || isPublished).map((c) => ({
281
761
  id: c.id,
282
762
  desc: c.desc,
283
763
  pass: c.check(repo)
@@ -287,11 +767,11 @@ var checkLocal = async (options = {}) => {
287
767
  var checkRepo = async (repo, options = {}) => {
288
768
  const {
289
769
  token,
290
- baseDir = path2.join(tmpdir(), "repo-lint"),
770
+ baseDir = path3.join(tmpdir(), "repo-lint"),
291
771
  skipRemote = false
292
772
  } = options;
293
- const dir = path2.join(baseDir, repo.replace("/", "__"));
294
- if (existsSync2(dir))
773
+ const dir = path3.join(baseDir, repo.replace("/", "__"));
774
+ if (existsSync3(dir))
295
775
  execSync(`git -C ${dir} pull --quiet`, { stdio: "ignore" });
296
776
  else {
297
777
  const url = token ? `https://${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
@@ -315,5 +795,7 @@ export {
315
795
  LOCAL_CHECKS,
316
796
  REMOTE_CHECKS,
317
797
  checkLocal,
318
- checkRepo
798
+ checkRepo,
799
+ detectPackageManager,
800
+ runUpdate
319
801
  };