@lzear/repo-lint 4.2.2 → 4.3.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,548 @@
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) => {
18
78
  try {
19
- return JSON.parse(readFileSync(f, "utf8"));
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;
357
+ try {
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 auditCommand = (pm) => {
427
+ switch (pm.name) {
428
+ case "npm":
429
+ return ["npm", "audit", "--omit", "dev", "--audit-level", "high"];
430
+ case "pnpm":
431
+ return ["pnpm", "audit", "--prod", "--audit-level", "high"];
432
+ case "bun":
433
+ return ["bun", "audit"];
434
+ case "yarn":
435
+ return pm.version?.startsWith("1.") ? ["yarn", "audit", "--level", "high"] : [
436
+ "yarn",
437
+ "npm",
438
+ "audit",
439
+ "--environment",
440
+ "production",
441
+ "--severity",
442
+ "high"
443
+ ];
444
+ }
445
+ };
446
+ var DEP_FIELDS = [
447
+ "dependencies",
448
+ "devDependencies",
449
+ "optionalDependencies",
450
+ "peerDependencies"
451
+ ];
452
+ var NON_REGISTRY_RANGE_RE = /^(?:workspace:|file:|link:|portal:|catalog:|git|https?:)/;
453
+ var collectEntry = (dependencies, name, range) => {
454
+ if (typeof range !== "string" || NON_REGISTRY_RANGE_RE.test(range)) return;
455
+ const alias = /^npm:(.+)@([^@]+)$/.exec(range);
456
+ const target = alias?.[1] ?? name;
457
+ const targetRange = alias?.[2] ?? range;
458
+ if (!dependencies.has(target)) dependencies.set(target, targetRange);
459
+ };
460
+ var collectFromPackage = (dependencies, packageDir) => {
461
+ const package_ = readPackage(packageDir);
462
+ for (const field of DEP_FIELDS) {
463
+ const section = package_?.[field];
464
+ if (typeof section !== "object" || section === null) continue;
465
+ for (const [name, range] of Object.entries(section))
466
+ collectEntry(dependencies, name, range);
467
+ }
468
+ };
469
+ var collectDependencies = (dir) => {
470
+ const dependencies = /* @__PURE__ */ new Map();
471
+ for (const packageDir of [dir, ...getWorkspaceDirectories(dir)])
472
+ collectFromPackage(dependencies, packageDir);
473
+ return dependencies;
474
+ };
475
+ var findDeprecated = async (name, range) => {
476
+ const response = await fetch(
477
+ `https://registry.npmjs.org/${name.replace("/", "%2F")}`,
478
+ {
479
+ headers: { accept: "application/vnd.npm.install-v1+json" },
480
+ signal: AbortSignal.timeout(1e4)
481
+ }
482
+ );
483
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
484
+ const data = await response.json();
485
+ const latest = data["dist-tags"]?.latest;
486
+ const resolved = latest && satisfies(latest, range) ? latest : maxSatisfying(Object.keys(data.versions), range);
487
+ if (!resolved) return null;
488
+ const deprecated = data.versions[resolved]?.deprecated;
489
+ return deprecated ? `${name}@${resolved} \u2014 ${deprecated.slice(0, 120)}` : null;
85
490
  };
86
491
  var LOCAL_CHECKS = [
87
492
  {
88
493
  id: "readme-exists",
89
- desc: "README.md exists",
90
- 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",
494
+ desc: "README",
103
495
  type: "local",
104
- publishedOnly: true,
105
- check: (dir) => readmeIncludes(dir, "project/badge/Coverage/")
496
+ check: (dir) => existsSync2(path2.join(dir, "README.md"))
106
497
  },
107
498
  {
108
499
  id: "readme-npm-badge",
109
- desc: "README has npm badge",
500
+ desc: "npm badge",
110
501
  type: "local",
111
502
  publishedOnly: true,
112
503
  check: (dir) => readmeIncludes(dir, "shields.io/npm/v/")
113
504
  },
114
505
  {
115
- id: "codacy-config",
116
- desc: ".codacy.yml exists",
506
+ id: "codacy",
507
+ desc: "Codacy",
117
508
  type: "local",
118
509
  publishedOnly: true,
119
- check: (dir) => existsSync(path.join(dir, ".codacy.yml"))
510
+ check: (dir) => {
511
+ const missing = [
512
+ readmeIncludes(dir, "project/badge/Grade/") ? null : "grade badge missing in README",
513
+ readmeIncludes(dir, "project/badge/Coverage/") ? null : "coverage badge missing in README",
514
+ existsSync2(path2.join(dir, ".codacy.yml")) ? null : ".codacy.yml missing"
515
+ ].filter((m) => m !== null);
516
+ if (missing.length === 0) return true;
517
+ return { pass: false, detail: missing.join("\n") };
518
+ }
120
519
  },
121
520
  {
122
521
  id: "license",
123
- desc: "LICENSE exists",
522
+ desc: "LICENSE",
124
523
  type: "local",
125
524
  publishedOnly: true,
126
- check: (dir) => existsSync(path.join(dir, "LICENSE"))
525
+ check: (dir) => existsSync2(path2.join(dir, "LICENSE"))
127
526
  },
128
527
  {
129
528
  id: "ci-workflow",
130
- desc: "CI workflow exists",
529
+ desc: "CI workflow",
131
530
  type: "local",
132
531
  publishedOnly: true,
133
- check: (dir) => existsSync(path.join(dir, ".github/workflows/ci.yml"))
532
+ check: (dir) => existsSync2(path2.join(dir, ".github/workflows/ci.yml"))
134
533
  },
135
534
  {
136
535
  id: "renovate",
137
- desc: "renovate.json exists",
536
+ desc: "renovate.json",
138
537
  type: "local",
139
- check: (dir) => existsSync(path.join(dir, "renovate.json"))
538
+ check: (dir) => existsSync2(path2.join(dir, "renovate.json"))
140
539
  },
141
540
  {
142
541
  id: "pkg-publint",
143
- desc: "publint (all published packages)",
542
+ desc: "publint",
144
543
  type: "local",
145
544
  publishedOnly: true,
146
- check: (dir) => eachPublishedPkg(dir, async (pkgDir) => {
545
+ check: (dir) => eachPublishedPackage(dir, async (pkgDir) => {
147
546
  const { messages } = await publint({ pkgDir });
148
547
  const failures = messages.filter(
149
548
  (m) => m.type === "error" || m.type === "warning"
@@ -157,13 +556,13 @@ var LOCAL_CHECKS = [
157
556
  },
158
557
  {
159
558
  id: "pkg-attw",
160
- desc: "attw (all published packages)",
559
+ desc: "attw",
161
560
  type: "local",
162
561
  check: (dir) => {
163
562
  const bin = findBin("attw", _dirname);
164
563
  if (!bin) return { pass: false, detail: "attw not available" };
165
- return eachPublishedPkg(dir, (pkgDir) => {
166
- const r = spawnSync(
564
+ return eachPublishedPackage(dir, (pkgDir) => {
565
+ const r = spawnSync2(
167
566
  process.execPath,
168
567
  [bin, "--pack", "--profile", "esm-only"],
169
568
  {
@@ -179,12 +578,12 @@ var LOCAL_CHECKS = [
179
578
  },
180
579
  {
181
580
  id: "pkg-knip",
182
- desc: "knip (no unused exports/deps)",
581
+ desc: "knip",
183
582
  type: "local",
184
583
  check: (dir) => {
185
584
  const bin = findBin("knip", _dirname);
186
585
  if (!bin) return { pass: false, detail: "knip not available" };
187
- const r = spawnSync(process.execPath, [bin], {
586
+ const r = spawnSync2(process.execPath, [bin], {
188
587
  cwd: dir,
189
588
  encoding: "utf8",
190
589
  stdio: ["ignore", "pipe", "pipe"],
@@ -197,28 +596,90 @@ var LOCAL_CHECKS = [
197
596
  Run: npx knip` };
198
597
  }
199
598
  },
599
+ {
600
+ id: "monorepo-lint",
601
+ desc: "sherif (workspaces)",
602
+ type: "local",
603
+ check: (dir) => {
604
+ const package_ = readPackage(dir);
605
+ if (!package_ || getWorkspacePatterns(package_).length === 0)
606
+ return { pass: true, detail: "not a monorepo" };
607
+ const bin = findBin("sherif", _dirname);
608
+ if (!bin) return { pass: false, detail: "sherif not available" };
609
+ const r = spawnSync2(process.execPath, [bin], {
610
+ cwd: dir,
611
+ encoding: "utf8",
612
+ stdio: ["ignore", "pipe", "pipe"],
613
+ env: { ...process.env, NO_COLOR: "1" }
614
+ });
615
+ if (r.status === 0) return true;
616
+ const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
617
+ return { pass: false, detail };
618
+ }
619
+ },
620
+ {
621
+ id: "deps-audit",
622
+ desc: "audit",
623
+ type: "local",
624
+ check: (dir) => {
625
+ const pm = detectPackageManager(dir);
626
+ const [command, ...arguments_] = auditCommand(pm);
627
+ const r = spawnSync2(command, arguments_, {
628
+ cwd: dir,
629
+ encoding: "utf8",
630
+ stdio: ["ignore", "pipe", "pipe"]
631
+ });
632
+ if (r.error) return { pass: false, detail: String(r.error) };
633
+ if (r.status === 0) return true;
634
+ const detail = (r.stdout + r.stderr).trim().split("\n").slice(0, 20).join("\n");
635
+ return { pass: false, detail: detail || `audit exited ${r.status}` };
636
+ }
637
+ },
638
+ {
639
+ id: "deps-deprecated",
640
+ desc: "no deprecated deps",
641
+ type: "local",
642
+ check: async (dir) => {
643
+ const dependencies = collectDependencies(dir);
644
+ const failures = [];
645
+ await Promise.all(
646
+ [...dependencies].map(async ([name, range]) => {
647
+ try {
648
+ const deprecated = await findDeprecated(name, range);
649
+ if (deprecated) failures.push(deprecated);
650
+ } catch (error) {
651
+ failures.push(`${name} \u2014 check failed: ${String(error)}`);
652
+ }
653
+ })
654
+ );
655
+ if (failures.length === 0) return true;
656
+ return {
657
+ pass: false,
658
+ detail: failures.toSorted((a, b) => a.localeCompare(b)).join("\n")
659
+ };
660
+ }
661
+ },
200
662
  {
201
663
  id: "deps-fresh",
202
- desc: "dependencies up to date (ncu)",
664
+ desc: "deps up to date",
203
665
  type: "local",
204
666
  check: async (dir) => {
205
- const pkg = readPkg(dir);
206
- const hasWorkspaces = Array.isArray(pkg?.workspaces) && pkg.workspaces.length > 0;
667
+ const package_ = readPackage(dir);
668
+ const hasWorkspaces2 = Array.isArray(package_?.workspaces) && package_.workspaces.length > 0;
207
669
  try {
208
- const result = await ncuRun({
209
- packageFile: path.join(dir, "package.json"),
210
- ...hasWorkspaces ? { workspaces: true } : {},
670
+ const result = await ncuRun2({
671
+ packageFile: path2.join(dir, "package.json"),
672
+ ...hasWorkspaces2 && { workspaces: true },
211
673
  silent: true
212
674
  });
213
- const entries = hasWorkspaces ? Object.values(
675
+ const entries = hasWorkspaces2 ? Object.values(
214
676
  result
215
677
  ).flatMap((x) => Object.entries(x)) : Object.entries(result);
216
678
  if (entries.length === 0) return true;
217
679
  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
680
  return { pass: false, detail: `${lines}
220
681
 
221
- Run: ${cmd}` };
682
+ Run: forge update` };
222
683
  } catch (error) {
223
684
  return { pass: false, detail: String(error) };
224
685
  }
@@ -227,7 +688,7 @@ Run: ${cmd}` };
227
688
  ];
228
689
  var listSecrets = (repo) => {
229
690
  try {
230
- const result = spawnSync(
691
+ const result = spawnSync2(
231
692
  "gh",
232
693
  ["secret", "list", "--repo", repo, "--json", "name"],
233
694
  {
@@ -244,7 +705,7 @@ var listSecrets = (repo) => {
244
705
  var REMOTE_CHECKS = [
245
706
  {
246
707
  id: "secret-codacy-token",
247
- desc: "Secret CODACY_PROJECT_TOKEN set",
708
+ desc: "Codacy secret",
248
709
  type: "remote",
249
710
  publishedOnly: true,
250
711
  check: (repo) => {
@@ -265,19 +726,21 @@ var checkLocal = async (options = {}) => {
265
726
  stdio: ["ignore", "pipe", "ignore"]
266
727
  }).trim();
267
728
  const match = /github\.com[:/](.+?)(?:\.git)?$/.exec(remote);
268
- return match?.[1] ?? path2.basename(dir);
729
+ return match?.[1] ?? path3.basename(dir);
269
730
  } catch {
270
731
  return dir;
271
732
  }
272
733
  })();
273
- const published = hasPublishedPkg(dir);
734
+ const isPublished = hasPublishedPkg(dir);
274
735
  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
- })
736
+ LOCAL_CHECKS.filter((c) => !c.publishedOnly || isPublished).map(
737
+ async (c) => {
738
+ const raw = await c.check(dir);
739
+ return typeof raw === "boolean" ? { id: c.id, desc: c.desc, pass: raw } : { id: c.id, desc: c.desc, ...raw };
740
+ }
741
+ )
279
742
  );
280
- const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly || published).map((c) => ({
743
+ const remoteResults = skipRemote ? [] : REMOTE_CHECKS.filter((c) => !c.publishedOnly || isPublished).map((c) => ({
281
744
  id: c.id,
282
745
  desc: c.desc,
283
746
  pass: c.check(repo)
@@ -287,11 +750,11 @@ var checkLocal = async (options = {}) => {
287
750
  var checkRepo = async (repo, options = {}) => {
288
751
  const {
289
752
  token,
290
- baseDir = path2.join(tmpdir(), "repo-lint"),
753
+ baseDir = path3.join(tmpdir(), "repo-lint"),
291
754
  skipRemote = false
292
755
  } = options;
293
- const dir = path2.join(baseDir, repo.replace("/", "__"));
294
- if (existsSync2(dir))
756
+ const dir = path3.join(baseDir, repo.replace("/", "__"));
757
+ if (existsSync3(dir))
295
758
  execSync(`git -C ${dir} pull --quiet`, { stdio: "ignore" });
296
759
  else {
297
760
  const url = token ? `https://${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
@@ -315,5 +778,7 @@ export {
315
778
  LOCAL_CHECKS,
316
779
  REMOTE_CHECKS,
317
780
  checkLocal,
318
- checkRepo
781
+ checkRepo,
782
+ detectPackageManager,
783
+ runUpdate
319
784
  };