@rtorcato/repo-tooling 3.38.0 → 3.38.1

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.
@@ -720,7 +720,9 @@ export async function checkPrePushHook(dir, p) {
720
720
  }
721
721
  export async function checkReadmeBadges(dir, audience,
722
722
  /** `fix` target that rebuilds the badge block, or null when the language has none. */
723
- fixTarget) {
723
+ fixTarget,
724
+ /** The repo's own package name — only its badges can 404; others' resolve fine (#634). */
725
+ name) {
724
726
  const check = 'README badges';
725
727
  const hint = fixTarget
726
728
  ? `Run \`npx @rtorcato/repo-tooling fix ${fixTarget}\` to add CI/npm/coverage/license badges`
@@ -729,7 +731,7 @@ fixTarget) {
729
731
  const readme = (await fs.pathExists(readmePath)) ? await fs.readFile(readmePath, 'utf8') : '';
730
732
  if (audience === 'private') {
731
733
  // Only a problem if a private/app repo carries badges that would 404.
732
- if (readme && hasPublicOnlyBadges(readme)) {
734
+ if (readme && hasPublicOnlyBadges(readme, name)) {
733
735
  return {
734
736
  check,
735
737
  status: 'drift',
@@ -244,7 +244,7 @@ async function runBaseChecks(dir, lock, opts) {
244
244
  if (lock?.rules?.mcp?.recommended?.length) {
245
245
  results.push(await checkRecommendedMcp(dir, lock.rules.mcp.recommended));
246
246
  }
247
- results.push(await checkReadmeBadges(dir, opts.badges.audience, opts.badges.fixTarget));
247
+ results.push(await checkReadmeBadges(dir, opts.badges.audience, opts.badges.fixTarget, opts.badges.name));
248
248
  results.push(await checkCoverageUpload(dir));
249
249
  return results;
250
250
  }
@@ -407,7 +407,11 @@ export async function runDoctor(dir, skillsDir) {
407
407
  }
408
408
  results.push(...(await runBaseChecks(targetDir, lock, {
409
409
  hooks: jsGitHooksProfile(pkg),
410
- badges: { audience: jsBadgeAudience(pkg), fixTarget: 'badges' },
410
+ badges: {
411
+ audience: jsBadgeAudience(pkg),
412
+ fixTarget: 'badges',
413
+ name: typeof pkg?.name === 'string' ? pkg.name : undefined,
414
+ },
411
415
  // Same `scripts` gating the fixer applies, or the workflow doctor
412
416
  // compares against would reference steps the fixer never writes (#364).
413
417
  presetWorkflow: renderGitHubWorkflow(githubJobs(inferProjectConfig(pkg), { scripts: scriptsOf(pkg) })),
@@ -489,7 +489,8 @@ export async function fixCommand(target, options = {}) {
489
489
  // A fixer that refuses (e.g. dependabot, when the existing config carries
490
490
  // repo-local `ignore:` rules the template can't reproduce) is a skip, not a
491
491
  // crash — the remaining findings still deserve their fixers. The reason goes
492
- // to stderr so it survives `--json`.
492
+ // to stderr so it survives `--json`. An unexpected error is skipped the same
493
+ // way (#636): rethrowing it killed the walk mid-run with no JSON on stdout.
493
494
  let outcome;
494
495
  try {
495
496
  outcome = await applyFixer(fixer, result, targetDir, pkg, lock, dryRun, silent, {
@@ -499,12 +500,15 @@ export async function fixCommand(target, options = {}) {
499
500
  });
500
501
  }
501
502
  catch (err) {
502
- if (!(err instanceof FixerAbort))
503
- throw err;
504
503
  actions.push(recordFor(fixer.target, result.check, result.status, 'skipped', [], conflict));
505
- console.error(chalk.red(` refused — ${err.message}`));
506
- if (err.hint)
507
- console.error(chalk.gray(` ${err.hint}`));
504
+ if (err instanceof FixerAbort) {
505
+ console.error(chalk.red(` refused — ${err.message}`));
506
+ if (err.hint)
507
+ console.error(chalk.gray(` ${err.hint}`));
508
+ }
509
+ else {
510
+ console.error(chalk.red(` failed — ${err instanceof Error ? err.message : String(err)}`));
511
+ }
508
512
  skippedCount++;
509
513
  continue;
510
514
  }
@@ -56,9 +56,29 @@ export function buildBadgeBlock(inputs) {
56
56
  const row = buildBadgeRow(inputs);
57
57
  return row ? `${BADGE_START}\n${row}\n${BADGE_END}` : '';
58
58
  }
59
- /** True when the text carries npm/bundlephobia/codecov badges (which 404 on private/unpublished). */
60
- export function hasPublicOnlyBadges(text) {
61
- return /img\.shields\.io\/npm\/|bundlephobia|codecov\.io/.test(text);
59
+ /**
60
+ * True when the text carries npm/bundlephobia/codecov badges (which 404 on
61
+ * private/unpublished). Given `name`, npm/bundlephobia badges count only when
62
+ * they point at that package — a README may advertise *other* packages (#634).
63
+ */
64
+ export function hasPublicOnlyBadges(text, name) {
65
+ if (/codecov\.io/.test(text))
66
+ return true;
67
+ if (!name)
68
+ return /img\.shields\.io\/npm\/|bundlephobia/.test(text);
69
+ const refs = text.matchAll(/(?:img\.shields\.io\/npm\/[\w-]+|bundlephobia\/[\w-]+|bundlephobia\.com\/package)\/([^\s)"'?#]+)/g);
70
+ for (const [, raw = ''] of refs) {
71
+ let ref = raw;
72
+ try {
73
+ ref = decodeURIComponent(raw);
74
+ }
75
+ catch {
76
+ // Malformed escape — compare the raw text.
77
+ }
78
+ if (ref === name || ref.startsWith(`${name}/`))
79
+ return true;
80
+ }
81
+ return false;
62
82
  }
63
83
  /**
64
84
  * Insert or refresh the badge block in a README. Replaces an existing delimited
@@ -5,6 +5,10 @@ import fs from 'fs-extra';
5
5
  * wins. One language per repo root; multi-language monorepos stay out of scope
6
6
  * (#139, reaffirmed in #317 — the audit is root-only, but says so via
7
7
  * `detectNestedLanguages`). A dir with no marker → 'unknown' (base checks only).
8
+ *
9
+ * One exception to the order: `Package.swift` beats a `package.json` that ships
10
+ * no JS (no `main`/`exports`/`bin`) — a Swift package whose root `package.json`
11
+ * only drives its docs site is a Swift repo (#633).
8
12
  */
9
13
  const MARKERS = [
10
14
  ['js', ['package.json']],
@@ -12,7 +16,14 @@ const MARKERS = [
12
16
  ['perl', ['cpanfile', 'Makefile.PL', 'dist.ini']],
13
17
  ['python', ['pyproject.toml', 'setup.py']],
14
18
  ];
19
+ /** True when `package.json` declares a JS entry point — the repo ships JS, not just tooling. */
20
+ async function shipsJs(dir) {
21
+ const pkg = await fs.readJson(path.join(dir, 'package.json')).catch(() => null);
22
+ return Boolean(pkg && (pkg.main || pkg.exports || pkg.bin));
23
+ }
15
24
  export async function detectLanguage(dir) {
25
+ if ((await fs.pathExists(path.join(dir, 'Package.swift'))) && !(await shipsJs(dir)))
26
+ return 'swift';
16
27
  for (const [language, candidates] of MARKERS) {
17
28
  for (const candidate of candidates) {
18
29
  if (await fs.pathExists(path.join(dir, candidate)))
@@ -1147,7 +1147,10 @@ export async function checkExportsBuildable(dir, pkg, exec) {
1147
1147
  }
1148
1148
  /** Who this package's README badges are for — feeds the base badge check (#309). */
1149
1149
  export function jsBadgeAudience(pkg) {
1150
- if (!pkg || pkg.private === true)
1150
+ // No package.json: nothing here is published, so no npm badge can be ours (#634).
1151
+ if (!pkg)
1152
+ return 'not-applicable';
1153
+ if (pkg.private === true)
1151
1154
  return 'private';
1152
1155
  return isPublishableLibrary(pkg) ? 'public' : 'not-applicable';
1153
1156
  }
@@ -304,6 +304,11 @@ export const FIXERS = [
304
304
  canFixDrift: true,
305
305
  async run({ targetDir, pkg }) {
306
306
  const pkgPath = path.join(targetDir, 'package.json');
307
+ // Refuse before writing any hook: lint-staged lives in package.json, so a
308
+ // repo without a readable one cannot be wired (#636).
309
+ if (!pkg) {
310
+ throw new FixerAbort('no-package-json', 'no readable package.json — husky needs one for lint-staged', 'add a package.json, then re-run');
311
+ }
307
312
  const existingLintStaged = pkg?.['lint-staged'] ?? {};
308
313
  await generateHuskyConfig(inferProjectConfig(pkg), targetDir);
309
314
  const updated = (await fs.readJson(pkgPath));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.38.0",
3
+ "version": "3.38.1",
4
4
  "description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
5
5
  "type": "module",
6
6
  "keywords": [