@rtorcato/repo-tooling 3.5.0 → 3.7.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/AGENTS.md CHANGED
@@ -6,7 +6,7 @@ Orientation for coding agents working with `@rtorcato/repo-tooling`. Human-reada
6
6
 
7
7
  A one-package JavaScript / TypeScript tooling distribution. Ships every preset (TypeScript, Biome, ESLint, Prettier, Vitest, Jest, Commitlint, semantic-release, tsup, esbuild, Vite, Playwright) plus a CLI to scaffold and audit projects. Consumers get one install.
8
8
 
9
- **Swift** repos (detected via `Package.swift`) are covered end to end: `setup --preset swift-library` scaffolds a SwiftPM package, and `doctor`/`fix` run the language-agnostic checks plus SwiftLint / Periphery / `.gitignore` / `Package.swift`. Python and Perl are audit-only for now. See `src/languages/` — one directory per language module, `src/base/` for what's shared.
9
+ **Swift** repos (detected via `Package.swift`) are covered end to end: `setup --preset swift-library` scaffolds a SwiftPM package, and `doctor`/`fix` run the language-agnostic checks plus SwiftLint / Periphery / `.gitignore` / `Package.swift`. **Python** repos (detected via `pyproject.toml` / `setup.py`) get `doctor`/`fix` — Ruff / mypy / pytest / `.gitignore` / CI / git hooks — but no `setup` preset yet. **Perl** distributions (detected via `cpanfile` / `Makefile.PL` / `dist.ini`) get the same deal: Perl::Critic / perltidy / `.gitignore` / CI / git hooks, no `setup` preset. See `src/languages/` — one directory per language module, `src/base/` for what's shared.
10
10
 
11
11
  ## CLI surface (agent-friendly)
12
12
 
@@ -233,7 +233,21 @@ export async function checkDependabot(dir) {
233
233
  hint: 'Run `npx @rtorcato/repo-tooling fix dependabot` (or `fix renovate`) to scaffold weekly dep updates',
234
234
  };
235
235
  }
236
- export async function checkCodeQL(dir) {
236
+ /**
237
+ * `languages` is the detected module's `codeqlLanguages`. An empty list means
238
+ * CodeQL ships no analyzer for this language at all (Perl, #289) — reporting it
239
+ * as missing would send the user to `fix codeql`, whose generator correctly
240
+ * writes nothing for an empty matrix, so they'd get a silent no-op and a check
241
+ * that never goes green.
242
+ */
243
+ export async function checkCodeQL(dir, languages) {
244
+ if (languages.length === 0) {
245
+ return {
246
+ check: 'CodeQL',
247
+ status: 'ok',
248
+ detail: 'not applicable — CodeQL has no analyzer for this language',
249
+ };
250
+ }
237
251
  const workflowsDir = path.join(dir, '.github', 'workflows');
238
252
  if (!(await fs.pathExists(workflowsDir))) {
239
253
  return {
@@ -11,6 +11,7 @@
11
11
  * language-shaped (CI steps, recorded tool choices), so each module ships its
12
12
  * own — see src/base/ci.ts for the shell they share.
13
13
  */
14
+ import chalk from 'chalk';
14
15
  import { installAgentRules, installAiSetup } from '../cli/generators/agent-rules.js';
15
16
  import { generateCommunityHealth } from '../cli/generators/community-health.js';
16
17
  import { generateCommitlintConfig } from '../cli/generators/git.js';
@@ -78,7 +79,14 @@ export const BASE_FIXERS = [
78
79
  appliesTo: ['CodeQL'],
79
80
  outputs: ['.github/workflows/codeql.yml'],
80
81
  async run({ targetDir }) {
81
- const { codeqlLanguages } = await moduleFor(targetDir);
82
+ const { codeqlLanguages, label } = await moduleFor(targetDir);
83
+ // Say so rather than reporting a successful fix that wrote nothing: the
84
+ // generator correctly emits no workflow for an empty matrix (Perl, #289),
85
+ // and a silent no-op reads as "done".
86
+ if (codeqlLanguages.length === 0) {
87
+ console.error(chalk.yellow(` skipped — CodeQL has no ${label} analyzer`));
88
+ return { filesWritten: [] };
89
+ }
82
90
  return { filesWritten: await generateCodeQLWorkflow(targetDir, codeqlLanguages) };
83
91
  },
84
92
  },
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Committed git hooks for languages that can't use Husky (#309).
3
+ *
4
+ * Husky is an npm package, so a repo whose toolchain has no node can't use it.
5
+ * The node-free equivalent is a committed hooks directory that git is pointed at
6
+ * with `core.hooksPath` — identical mechanics whatever the language; only the
7
+ * hook bodies differ. Swift and Python both land here rather than each carrying
8
+ * their own copy of the write-then-chmod-then-git-config dance.
9
+ */
10
+ import { execFile } from 'node:child_process';
11
+ import path from 'node:path';
12
+ import fs from 'fs-extra';
13
+ /** Best-effort `git config` — a missing git or a non-repo dir is not a failure. */
14
+ function gitConfig(cwd, key, value) {
15
+ return new Promise((resolve) => {
16
+ execFile('git', ['-C', cwd, 'config', key, value], (err) => resolve(!err));
17
+ });
18
+ }
19
+ /**
20
+ * Writes both hooks and points git at them. Returns the files written —
21
+ * `core.hooksPath` is per-clone local config, not a file, so it's reported
22
+ * separately rather than pretending to be a repo change.
23
+ */
24
+ export async function installGitHooks(targetDir, hooksDirName, scripts) {
25
+ const hooksDir = path.join(targetDir, hooksDirName);
26
+ await fs.ensureDir(hooksDir);
27
+ const filesWritten = [];
28
+ for (const [name, content] of [
29
+ ['pre-commit', scripts.preCommit],
30
+ ['pre-push', scripts.prePush],
31
+ ]) {
32
+ const hookPath = path.join(hooksDir, name);
33
+ await fs.writeFile(hookPath, content);
34
+ await fs.chmod(hookPath, 0o755);
35
+ filesWritten.push(`${hooksDirName}/${name}`);
36
+ }
37
+ // Guard on .git: `fix --diff` shadow-runs fixers in a temp copy that excludes
38
+ // .git, and that copy can land inside another repo — an unguarded `git config`
39
+ // there would rewrite the *parent* repo's hooksPath during a mere preview.
40
+ const isRepo = await fs.pathExists(path.join(targetDir, '.git'));
41
+ const hooksPathSet = isRepo ? await gitConfig(targetDir, 'core.hooksPath', hooksDirName) : false;
42
+ return { filesWritten, hooksPathSet };
43
+ }
@@ -4,6 +4,10 @@ import fs from 'fs-extra';
4
4
  import { renderGitHubWorkflow } from '../../base/ci.js';
5
5
  import { githubJobs } from '../../languages/js/ci.js';
6
6
  import { inferProjectConfig } from '../../languages/js/fixers.js';
7
+ import { PERL_GIT_HOOKS, runPerlChecks } from '../../languages/perl/checks.js';
8
+ import { readPerlProject, renderPerlWorkflow } from '../../languages/perl/ci.js';
9
+ import { PYTHON_GIT_HOOKS, runPythonChecks } from '../../languages/python/checks.js';
10
+ import { readPyproject, renderPythonWorkflow } from '../../languages/python/ci.js';
7
11
  import { resolveLanguageModule } from '../../languages/registry.js';
8
12
  import { SWIFT_GIT_HOOKS, runSwiftChecks } from '../../languages/swift/checks.js';
9
13
  import { readSwiftPackage, renderSwiftWorkflow } from '../../languages/swift/ci.js';
@@ -155,7 +159,7 @@ async function runBaseChecks(dir, lock, opts) {
155
159
  }
156
160
  results.push(await checkGitHubActions(dir, opts.presetWorkflow));
157
161
  results.push(await checkDependabot(dir));
158
- results.push(await checkCodeQL(dir));
162
+ results.push(await checkCodeQL(dir, opts.codeqlLanguages));
159
163
  // GitHub repo-settings drift (branch protection, merge settings, workflow
160
164
  // permissions). Read-only; self-skips as `ok` outside a live GitHub repo.
161
165
  results.push(...(await checkGitHubSettings(dir)));
@@ -172,12 +176,14 @@ export async function runDoctor(dir) {
172
176
  const lock = await readLockfile(targetDir);
173
177
  // Per-module dispatch (#285): the base checks (repo hygiene, CI, security,
174
178
  // GitHub settings) apply to any repo and run for every language. A supported
175
- // module layers its own checks on top; an unsupported one (Swift/Perl/Python
176
- // until their modules land) still gets the full base suite instead of the old
177
- // wholesale skip. 'unknown' (bare dir mid-setup) resolves to JS so a fresh
178
- // repo runs the full suite.
179
+ // module layers its own checks on top. 'unknown' (bare dir mid-setup)
180
+ // resolves to JS so a fresh repo runs the full suite.
179
181
  const language = await detectLanguage(targetDir);
180
182
  const languageModule = resolveLanguageModule(language);
183
+ // Every language in the registry has a module as of #289, so nothing reaches
184
+ // this today. It stays as the on-ramp: a language is added to the registry
185
+ // with `supported: false` first, and until its module lands its repos get the
186
+ // full base suite rather than the wholesale skip this replaced.
181
187
  if (!languageModule.supported) {
182
188
  const results = [
183
189
  {
@@ -185,14 +191,14 @@ export async function runDoctor(dir) {
185
191
  status: 'ok',
186
192
  detail: `detected ${languageModule.label} — running language-agnostic checks; ${languageModule.label}-specific checks land with its module (#139)`,
187
193
  },
188
- // hooks: null — nothing here encodes a Python/Perl hook convention yet,
189
- // and guessing one would nag every repo with a fix target that doesn't
190
- // exist. #289/#290 fill it in.
194
+ // hooks: null — guessing a hook convention would nag every repo with a
195
+ // fix target that doesn't exist.
191
196
  ...(await runBaseChecks(targetDir, lock, {
192
197
  hooks: null,
193
198
  badges: { audience: 'public', fixTarget: null },
194
199
  presetWorkflow: null,
195
200
  language,
201
+ codeqlLanguages: languageModule.codeqlLanguages,
196
202
  })),
197
203
  ];
198
204
  return demoteDeclined(results, lock);
@@ -214,11 +220,60 @@ export async function runDoctor(dir) {
214
220
  badges: { audience: 'public', fixTarget: null },
215
221
  presetWorkflow: renderSwiftWorkflow(await readSwiftPackage(targetDir)),
216
222
  language,
223
+ codeqlLanguages: languageModule.codeqlLanguages,
217
224
  })),
218
225
  ...(await runSwiftChecks(targetDir)),
219
226
  ];
220
227
  return demoteDeclined(results, lock);
221
228
  }
229
+ // Python suite (#290): same shape as Swift — base checks plus the module's
230
+ // own, and nothing JS-shaped, because a Python repo has no package.json.
231
+ if (languageModule.id === 'python') {
232
+ const results = [
233
+ {
234
+ check: 'language',
235
+ status: 'ok',
236
+ detail: 'detected Python (pyproject.toml / setup.py)',
237
+ },
238
+ ...(await runBaseChecks(targetDir, lock, {
239
+ hooks: PYTHON_GIT_HOOKS,
240
+ // A Python package on PyPI is public by default, so badges apply. No
241
+ // fixer: `fix badges` derives the block from package.json name and
242
+ // repository, which a Python repo hasn't got.
243
+ badges: { audience: 'public', fixTarget: null },
244
+ presetWorkflow: renderPythonWorkflow(await readPyproject(targetDir)),
245
+ language,
246
+ codeqlLanguages: languageModule.codeqlLanguages,
247
+ })),
248
+ ...(await runPythonChecks(targetDir)),
249
+ ];
250
+ return demoteDeclined(results, lock);
251
+ }
252
+ // Perl suite (#289): same shape as Swift and Python — base checks plus the
253
+ // module's own, and nothing JS-shaped, because a distribution has no
254
+ // package.json. CodeQL self-reports as not applicable: it ships no Perl
255
+ // analyzer, which `codeqlLanguages: []` in the registry encodes.
256
+ if (languageModule.id === 'perl') {
257
+ const results = [
258
+ {
259
+ check: 'language',
260
+ status: 'ok',
261
+ detail: 'detected Perl (cpanfile / Makefile.PL / dist.ini)',
262
+ },
263
+ ...(await runBaseChecks(targetDir, lock, {
264
+ hooks: PERL_GIT_HOOKS,
265
+ // A distribution released to CPAN is public by definition, so badges
266
+ // apply. No fixer: `fix badges` derives the block from package.json
267
+ // name and repository, which a Perl repo hasn't got.
268
+ badges: { audience: 'public', fixTarget: null },
269
+ presetWorkflow: renderPerlWorkflow(await readPerlProject(targetDir)),
270
+ language,
271
+ codeqlLanguages: languageModule.codeqlLanguages,
272
+ })),
273
+ ...(await runPerlChecks(targetDir)),
274
+ ];
275
+ return demoteDeclined(results, lock);
276
+ }
222
277
  // JS suite: the module's own checks, then the shared base ones. Only the
223
278
  // JS-shaped checks are listed here — re-listing the base suite is what made
224
279
  // any new base check silently skip JS repos (#309).
@@ -265,6 +320,7 @@ export async function runDoctor(dir) {
265
320
  badges: { audience: jsBadgeAudience(pkg), fixTarget: 'badges' },
266
321
  presetWorkflow: renderGitHubWorkflow(githubJobs(inferProjectConfig(pkg))),
267
322
  language,
323
+ codeqlLanguages: languageModule.codeqlLanguages,
268
324
  })));
269
325
  return demoteDeclined(results, lock);
270
326
  }
@@ -64,11 +64,43 @@ const SWIFT_FIX_TARGETS = {
64
64
  // (no `.testTarget(`) there's nothing to run, and rewriting Package.swift
65
65
  // isn't safe. The check's own hint covers both halves.
66
66
  };
67
+ /** The same shadowing for the Python module (#290). */
68
+ const PYTHON_FIX_TARGETS = {
69
+ 'Git hooks': 'python-git-hooks',
70
+ 'Pre-push hook': 'python-git-hooks',
71
+ 'GitHub Actions': 'python-ci',
72
+ 'GitLab CI': 'python-gitlab-ci',
73
+ Ruff: 'ruff',
74
+ mypy: 'mypy',
75
+ pytest: 'pytest',
76
+ 'Python .gitignore': 'python-gitignore',
77
+ // `pyproject.toml` and `Python tests` are deliberately absent, for the same
78
+ // reason `Swift tests` is: the fix is content only the project can write
79
+ // (package metadata, actual tests). Their own hints say what to add.
80
+ // `lockfile` too — see the note at the top of src/languages/python/fixers.ts.
81
+ };
82
+ /** The same shadowing for the Perl module (#289). */
83
+ const PERL_FIX_TARGETS = {
84
+ 'Git hooks': 'perl-git-hooks',
85
+ 'Pre-push hook': 'perl-git-hooks',
86
+ 'GitHub Actions': 'perl-ci',
87
+ 'GitLab CI': 'perl-gitlab-ci',
88
+ 'Perl::Critic': 'perlcritic',
89
+ perltidy: 'perltidy',
90
+ 'Perl .gitignore': 'perl-gitignore',
91
+ // `Perl distribution` and `Perl tests` are deliberately absent, for the same
92
+ // reason `Swift tests` is: the fix is content only the project can write
93
+ // (dependency metadata, actual tests). Their own hints say what to add.
94
+ // `lockfile` too — see the note at the top of src/languages/perl/fixers.ts.
95
+ };
96
+ const FIX_TARGETS_BY_LANGUAGE = {
97
+ swift: SWIFT_FIX_TARGETS,
98
+ python: PYTHON_FIX_TARGETS,
99
+ perl: PERL_FIX_TARGETS,
100
+ };
67
101
  export function getFixTargetForCheck(checkName, language) {
68
- if (language === 'swift' && SWIFT_FIX_TARGETS[checkName]) {
69
- return SWIFT_FIX_TARGETS[checkName];
70
- }
71
- return FIX_TARGETS[checkName] ?? null;
102
+ const overrides = language ? FIX_TARGETS_BY_LANGUAGE[language] : undefined;
103
+ return overrides?.[checkName] ?? FIX_TARGETS[checkName] ?? null;
72
104
  }
73
105
  /**
74
106
  * For a given doctor check name, returns true when the lockfile records that
@@ -168,6 +200,8 @@ export function lockfilePatchForTarget(target, lock) {
168
200
  return c.commitLint ? null : { commitLint: true };
169
201
  case 'husky':
170
202
  case 'swift-git-hooks':
203
+ case 'python-git-hooks':
204
+ case 'perl-git-hooks':
171
205
  return c.gitHooks ? null : { gitHooks: true };
172
206
  case 'semantic-release':
173
207
  case 'swift-release':
@@ -11,19 +11,32 @@ import { declinedInLock, lockfilePatchForTarget } from './fix-targets.js';
11
11
  import { computeFileList } from './setup-presets.js';
12
12
  import { BASE_FIXERS } from '../../base/fixers.js';
13
13
  import { FIXERS, readPackageJson } from '../../languages/js/fixers.js';
14
+ import { PERL_FIXERS } from '../../languages/perl/fixers.js';
15
+ import { PYTHON_FIXERS } from '../../languages/python/fixers.js';
14
16
  import { SWIFT_FIXERS } from '../../languages/swift/fixers.js';
15
17
  import { detectLanguage } from '../utils/detect-language.js';
18
+ /** The module-specific fixer set per detected language (#286, #289, #290, #303). */
19
+ const LANGUAGE_FIXERS = {
20
+ swift: SWIFT_FIXERS,
21
+ python: PYTHON_FIXERS,
22
+ perl: PERL_FIXERS,
23
+ };
16
24
  /**
17
- * The fixers that apply to a repo, by detected language (#286, #303): the
18
- * language-agnostic base set plus the module's own. Swift repos get the Swift
19
- * module; everything else (including a bare dir mid-setup) gets JS, the
20
- * historical default.
25
+ * The fixers that apply to a repo, by detected language: the language-agnostic
26
+ * base set plus the module's own. Anything without a module of its own
27
+ * (including a bare dir mid-setup) gets JS, the historical default.
21
28
  */
22
29
  function fixersForLanguage(language) {
23
- return [...BASE_FIXERS, ...(language === 'swift' ? SWIFT_FIXERS : FIXERS)];
30
+ return [...BASE_FIXERS, ...(LANGUAGE_FIXERS[language] ?? FIXERS)];
24
31
  }
25
32
  /** Every fixer across every language — for `--list` and the unknown-target hint. */
26
- const ALL_FIXERS = [...BASE_FIXERS, ...FIXERS, ...SWIFT_FIXERS];
33
+ const ALL_FIXERS = [
34
+ ...BASE_FIXERS,
35
+ ...FIXERS,
36
+ ...SWIFT_FIXERS,
37
+ ...PYTHON_FIXERS,
38
+ ...PERL_FIXERS,
39
+ ];
27
40
  export function getFixers() {
28
41
  return ALL_FIXERS;
29
42
  }
package/dist/cli/index.js CHANGED
@@ -251,6 +251,36 @@ const TOOL_CATALOG = [
251
251
  exports: [],
252
252
  fixTarget: 'periphery',
253
253
  },
254
+ {
255
+ name: 'Ruff',
256
+ description: 'Python linter and formatter in one tool (replaces flake8, isort, pyupgrade, black)',
257
+ exports: [],
258
+ fixTarget: 'ruff',
259
+ },
260
+ {
261
+ name: 'mypy',
262
+ description: 'Python static type checker configuration (strict on your code, lenient on untyped deps)',
263
+ exports: [],
264
+ fixTarget: 'mypy',
265
+ },
266
+ {
267
+ name: 'pytest',
268
+ description: 'Python test runner configuration (strict markers and config, warnings as errors)',
269
+ exports: [],
270
+ fixTarget: 'pytest',
271
+ },
272
+ {
273
+ name: 'Perl::Critic',
274
+ description: 'Perl linter configuration (severity 3, with the family rule exceptions)',
275
+ exports: [],
276
+ fixTarget: 'perlcritic',
277
+ },
278
+ {
279
+ name: 'perltidy',
280
+ description: 'Perl formatter configuration (Perl Best Practices layout at 100 columns)',
281
+ exports: [],
282
+ fixTarget: 'perltidy',
283
+ },
254
284
  {
255
285
  name: 'AI Agent Setup',
256
286
  description: 'Scaffold AGENTS.md, CLAUDE.md, Cursor/Copilot rules, Claude skill, MCP example',
@@ -51,6 +51,31 @@ export const PRESETS = {
51
51
  target: '.periphery.yml',
52
52
  desc: 'Periphery dead-code scan configuration (retains public API)',
53
53
  },
54
+ ruff: {
55
+ source: 'tooling/python/ruff.toml',
56
+ target: 'ruff.toml',
57
+ desc: 'Ruff configuration (lint + format — replaces flake8, isort, pyupgrade and black)',
58
+ },
59
+ mypy: {
60
+ source: 'tooling/python/mypy.ini',
61
+ target: 'mypy.ini',
62
+ desc: 'mypy configuration (strict on your code, lenient on untyped dependencies)',
63
+ },
64
+ pytest: {
65
+ source: 'tooling/python/pytest.ini',
66
+ target: 'pytest.ini',
67
+ desc: 'pytest configuration (strict markers and config, warnings as errors)',
68
+ },
69
+ perlcritic: {
70
+ source: 'tooling/perl/perlcriticrc',
71
+ target: '.perlcriticrc',
72
+ desc: 'Perl::Critic configuration (severity 3, with the family rule exceptions)',
73
+ },
74
+ perltidy: {
75
+ source: 'tooling/perl/perltidyrc',
76
+ target: '.perltidyrc',
77
+ desc: 'perltidy configuration (Perl Best Practices layout at 100 columns)',
78
+ },
54
79
  'claude-skill': {
55
80
  source: 'tooling/claude/repo-tooling.md',
56
81
  target: '.claude/skills/repo-tooling.md',
@@ -225,13 +225,13 @@ export const FIXERS = [
225
225
  async run({ targetDir, pkg }) {
226
226
  const pkgPath = path.join(targetDir, 'package.json');
227
227
  if (!pkg) {
228
- console.log(chalk.yellow(' no package.json found — skipping'));
228
+ console.error(chalk.yellow(' no package.json found — skipping'));
229
229
  return { filesWritten: [] };
230
230
  }
231
231
  const includeTreeshake = await fs.pathExists(path.join(targetDir, 'apps', 'treeshake-check', 'check.mjs'));
232
232
  const verify = composeVerifyScriptFromPkg(pkg, { includeTreeshake });
233
233
  if (!verify) {
234
- console.log(chalk.gray(' not enough tools enabled to compose a verify chain — skipping (need 2+ of typecheck/lint/tests)'));
234
+ console.error(chalk.gray(' not enough tools enabled to compose a verify chain — skipping (need 2+ of typecheck/lint/tests)'));
235
235
  return { filesWritten: [] };
236
236
  }
237
237
  const updated = { ...pkg };
@@ -256,7 +256,7 @@ export const FIXERS = [
256
256
  canFixDrift: true,
257
257
  async run({ targetDir, pkg }) {
258
258
  if (pkg?.private === true) {
259
- console.log(chalk.gray(' skipping — package is private'));
259
+ console.error(chalk.gray(' skipping — package is private'));
260
260
  return { filesWritten: [] };
261
261
  }
262
262
  const filesWritten = await generateSemanticReleaseConfig(targetDir);
@@ -315,7 +315,7 @@ export const FIXERS = [
315
315
  overwrite: result.check === 'GitHub Actions',
316
316
  });
317
317
  if (!filesWritten.includes(CI_WORKFLOW)) {
318
- console.log(chalk.yellow(` ${CI_WORKFLOW} differs from the preset — left as-is; run \`fix github-actions --diff\` to see the delta`));
318
+ console.error(chalk.yellow(` ${CI_WORKFLOW} differs from the preset — left as-is; run \`fix github-actions --diff\` to see the delta`));
319
319
  }
320
320
  return { filesWritten };
321
321
  },
@@ -518,17 +518,17 @@ export const FIXERS = [
518
518
  canFixDrift: false,
519
519
  async run({ targetDir, pkg }) {
520
520
  if (!pkg) {
521
- console.log(chalk.yellow(' no package.json found — skipping'));
521
+ console.error(chalk.yellow(' no package.json found — skipping'));
522
522
  return { filesWritten: [] };
523
523
  }
524
524
  const workspaceName = pkg.name ?? null;
525
525
  if (!workspaceName) {
526
- console.log(chalk.yellow(' package.json has no `name` — skipping'));
526
+ console.error(chalk.yellow(' package.json has no `name` — skipping'));
527
527
  return { filesWritten: [] };
528
528
  }
529
529
  const { allCandidates, defaultAllowed } = inferSubpathsFromExports(pkg);
530
530
  if (allCandidates.length < 2 || !defaultAllowed) {
531
- console.log(chalk.yellow(' package.json does not expose ≥2 subpath exports — tree-shake check needs multiple subpaths to be meaningful. Skipping.'));
531
+ console.error(chalk.yellow(' package.json does not expose ≥2 subpath exports — tree-shake check needs multiple subpaths to be meaningful. Skipping.'));
532
532
  return { filesWritten: [] };
533
533
  }
534
534
  const allowedSubpath = defaultAllowed;
@@ -538,7 +538,7 @@ export const FIXERS = [
538
538
  allowedSubpath,
539
539
  forbiddenSubpaths,
540
540
  });
541
- console.log(chalk.dim(` Wired '${allowedSubpath}' as allowed; forbidden = [${forbiddenSubpaths.join(', ')}]. Edit apps/treeshake-check/check.mjs to tune.`));
541
+ console.error(chalk.dim(` Wired '${allowedSubpath}' as allowed; forbidden = [${forbiddenSubpaths.join(', ')}]. Edit apps/treeshake-check/check.mjs to tune.`));
542
542
  return { filesWritten: written };
543
543
  },
544
544
  },
@@ -615,7 +615,7 @@ export const FIXERS = [
615
615
  async run({ targetDir, pkg }) {
616
616
  const pkgPath = path.join(targetDir, 'package.json');
617
617
  if (!pkg) {
618
- console.log(chalk.yellow(' no package.json found — skipping'));
618
+ console.error(chalk.yellow(' no package.json found — skipping'));
619
619
  return { filesWritten: [] };
620
620
  }
621
621
  const updated = { ...pkg };
@@ -645,7 +645,7 @@ export const FIXERS = [
645
645
  async run({ targetDir, pkg }) {
646
646
  const pkgPath = path.join(targetDir, 'package.json');
647
647
  if (!pkg) {
648
- console.log(chalk.yellow(' no package.json found — skipping'));
648
+ console.error(chalk.yellow(' no package.json found — skipping'));
649
649
  return { filesWritten: [] };
650
650
  }
651
651
  const updated = { ...pkg };
@@ -674,7 +674,7 @@ export const FIXERS = [
674
674
  canFixDrift: true,
675
675
  async run({ targetDir, pkg }) {
676
676
  if (!pkg) {
677
- console.log(chalk.yellow(' no package.json found — skipping'));
677
+ console.error(chalk.yellow(' no package.json found — skipping'));
678
678
  return { filesWritten: [] };
679
679
  }
680
680
  const p = pkg;
@@ -687,7 +687,7 @@ export const FIXERS = [
687
687
  isPrivate: p.private === true,
688
688
  });
689
689
  if (!block) {
690
- console.log(chalk.yellow(' package.json has no name/repository to build badges from — skipping'));
690
+ console.error(chalk.yellow(' package.json has no name/repository to build badges from — skipping'));
691
691
  return { filesWritten: [] };
692
692
  }
693
693
  const readmePath = path.join(targetDir, 'README.md');
@@ -708,7 +708,7 @@ export const FIXERS = [
708
708
  async run({ targetDir, pkg }) {
709
709
  const pkgPath = path.join(targetDir, 'package.json');
710
710
  if (!pkg) {
711
- console.log(chalk.yellow(' no package.json found — skipping'));
711
+ console.error(chalk.yellow(' no package.json found — skipping'));
712
712
  return { filesWritten: [] };
713
713
  }
714
714
  const updated = { ...pkg };
@@ -718,7 +718,7 @@ export const FIXERS = [
718
718
  devDeps['@rtorcato/repo-tooling'] = 'latest';
719
719
  updated.devDependencies = devDeps;
720
720
  await fs.writeJson(pkgPath, updated, { spaces: 2 });
721
- console.log(chalk.dim(' reminder: run `pnpm install` to install the new dep'));
721
+ console.error(chalk.dim(' reminder: run `pnpm install` to install the new dep'));
722
722
  return { filesWritten: ['package.json'] };
723
723
  },
724
724
  },
@@ -731,7 +731,7 @@ export const FIXERS = [
731
731
  canFixDrift: false,
732
732
  async run({ targetDir, pkg }) {
733
733
  if (!pkg) {
734
- console.log(chalk.yellow(' no package.json found — skipping'));
734
+ console.error(chalk.yellow(' no package.json found — skipping'));
735
735
  return { filesWritten: [] };
736
736
  }
737
737
  const config = inferProjectConfig(pkg);