@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 +1 -1
- package/dist/base/checks.js +15 -1
- package/dist/base/fixers.js +9 -1
- package/dist/base/git-hooks.js +43 -0
- package/dist/cli/commands/doctor.js +64 -8
- package/dist/cli/commands/fix-targets.js +38 -4
- package/dist/cli/commands/fix.js +19 -6
- package/dist/cli/index.js +30 -0
- package/dist/cli/utils/copy-preset.js +25 -0
- package/dist/languages/js/fixers.js +15 -15
- package/dist/languages/perl/checks.js +177 -0
- package/dist/languages/perl/ci.js +191 -0
- package/dist/languages/perl/fixers.js +93 -0
- package/dist/languages/perl/git-hooks.js +53 -0
- package/dist/languages/perl/gitignore.js +65 -0
- package/dist/languages/perl/sources.js +22 -0
- package/dist/languages/python/checks.js +234 -0
- package/dist/languages/python/ci.js +153 -0
- package/dist/languages/python/fixers.js +104 -0
- package/dist/languages/python/git-hooks.js +35 -0
- package/dist/languages/python/gitignore.js +55 -0
- package/dist/languages/registry.js +2 -2
- package/dist/languages/swift/fixers.js +2 -2
- package/dist/languages/swift/git-hooks.js +7 -35
- package/package.json +5 -1
- package/tooling/perl/perlcriticrc +35 -0
- package/tooling/perl/perltidyrc +23 -0
- package/tooling/python/mypy.ini +21 -0
- package/tooling/python/pytest.ini +15 -0
- package/tooling/python/ruff.toml +32 -0
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
|
|
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
|
|
package/dist/base/checks.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/dist/base/fixers.js
CHANGED
|
@@ -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
|
|
176
|
-
//
|
|
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 —
|
|
189
|
-
//
|
|
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
|
-
|
|
69
|
-
|
|
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':
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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 = [
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
734
|
+
console.error(chalk.yellow(' no package.json found — skipping'));
|
|
735
735
|
return { filesWritten: [] };
|
|
736
736
|
}
|
|
737
737
|
const config = inferProjectConfig(pkg);
|