@rtorcato/repo-tooling 3.15.2 → 3.16.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 +2 -2
- package/README.md +8 -2
- package/dist/base/checks.js +6 -0
- package/dist/cli/commands/setup-presets.js +30 -2
- package/dist/cli/commands/setup.js +68 -11
- package/dist/cli/generators/index.js +5 -4
- package/dist/cli/generators/security.js +20 -6
- package/dist/cli/index.js +1 -0
- package/package.json +5 -5
- package/skills/ai-issue-loop/SKILL.md +31 -6
- package/skills/repo-tooling/SKILL.md +3 -2
- package/tooling/biome/biome.json +62 -61
- package/tooling/changesets/config.json +9 -9
- package/tooling/claude/repo-tooling.md +3 -2
- package/tooling/commitlint/commitlint.mjs +25 -25
- package/tooling/nx/nx.json +22 -22
- package/tooling/oxlint/oxlintrc.json +26 -26
- package/tooling/release-please/.release-please-manifest.json +1 -1
- package/tooling/release-please/release-please-config.json +7 -7
- package/tooling/rolldown/rolldown.config.mjs +6 -1
package/AGENTS.md
CHANGED
|
@@ -14,7 +14,7 @@ Every command supports `--json` and a non-interactive mode. Combine with `--yes`
|
|
|
14
14
|
|
|
15
15
|
| Command | Non-interactive | JSON output | Use case |
|
|
16
16
|
|---|---|---|---|
|
|
17
|
-
| `setup --preset <name>` | ✅ | `--dry-run` only | Scaffold a new project. Presets: `library`, `web-app`, `node-api`, `nextjs-app`, `react-app`, `swift-library`. |
|
|
17
|
+
| `setup --preset <name>` | ✅ (non-TTY, or with `--yes`) | `--dry-run` only | Scaffold a new project. Presets: `minimal`, `library`, `web-app`, `node-api`, `nextjs-app`, `react-app`, `swift-library`. In an interactive terminal it first prints the file list and lets you deselect; pass `--yes` for the one-shot behaviour. |
|
|
18
18
|
| `setup --config <path>` | ✅ | `--dry-run` only | Scaffold with a full `ProjectConfig` JSON file. See `setup --config-schema`. |
|
|
19
19
|
| `setup --config-schema` | ✅ | ✅ (JSON Schema) | Print the JSON Schema for `ProjectConfig`. Use to validate configs before scaffolding. |
|
|
20
20
|
| `setup --dry-run` | ✅ | ✅ | Print resolved config + file list without writing. Pair with `--preset` or `--config`. |
|
|
@@ -110,7 +110,7 @@ A fixer may also **refuse** — the target file holds something the generator ca
|
|
|
110
110
|
## Conventions in this repo
|
|
111
111
|
|
|
112
112
|
- Conventional commits enforced via commitlint; header max 100 chars, body/footer line length unenforced
|
|
113
|
-
- Biome for lint + format (run via `pnpm
|
|
113
|
+
- Biome for lint + format (run via `pnpm run check`, i.e. `biome check .` over the whole repo — the same script this package emits to consumers)
|
|
114
114
|
- Tests live alongside source in `tests/`; vitest with no separate config
|
|
115
115
|
- semantic-release runs on push to `main`; `fix:` → patch, `feat:` → minor, `chore:` / `docs:` → no release
|
|
116
116
|
|
package/README.md
CHANGED
|
@@ -35,10 +35,16 @@ npx @rtorcato/repo-tooling setup
|
|
|
35
35
|
Non-interactive — scaffold from a named preset in one shot (CI-friendly):
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
npx @rtorcato/repo-tooling setup --preset library -d ./my-lib --skip-install
|
|
39
|
-
# presets: library | web-app | node-api | nextjs-app | react-app | swift-library
|
|
38
|
+
npx @rtorcato/repo-tooling setup --preset library -d ./my-lib --skip-install --yes
|
|
39
|
+
# presets: minimal | library | web-app | node-api | nextjs-app | react-app | swift-library
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
Run without `--yes` in a terminal and the preset first prints every file it would
|
|
43
|
+
write and lets you uncheck the parts you don't want (git hooks, commitlint,
|
|
44
|
+
releases, Dependabot/CodeQL, badges, AI agent rules). Piped or redirected — CI —
|
|
45
|
+
it writes everything in one shot, exactly as before. `minimal` is tsconfig +
|
|
46
|
+
Biome + Vitest and nothing else.
|
|
47
|
+
|
|
42
48
|
`swift-library` scaffolds a SwiftPM package (manifest, sources, tests, SwiftLint,
|
|
43
49
|
Periphery, macOS CI) instead of an npm one — see the
|
|
44
50
|
[Swift guide](https://rtorcato.github.io/repo-tooling/guides/swift).
|
package/dist/base/checks.js
CHANGED
|
@@ -266,6 +266,12 @@ export async function checkDependabot(dir) {
|
|
|
266
266
|
if (!automerge.includes("steps.gate.outputs.safe == 'true'")) {
|
|
267
267
|
deltas.push('auto-merge workflow trusts the dev-minor group name (missing peerDependencies check)');
|
|
268
268
|
}
|
|
269
|
+
// #452: a repo that took the #423 pair has CI action bumps waiting on a
|
|
270
|
+
// human for no benefit — they reach no consumer. Drift in the
|
|
271
|
+
// convenience direction, not the safety one, but still drift.
|
|
272
|
+
if (!automerge.includes("package-ecosystem == 'github-actions'")) {
|
|
273
|
+
deltas.push('auto-merge workflow leaves CI action bumps to a human (#452)');
|
|
274
|
+
}
|
|
269
275
|
}
|
|
270
276
|
else {
|
|
271
277
|
deltas.push('missing dependabot-automerge workflow');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { swiftFileList } from '../../languages/swift/scaffold.js';
|
|
2
2
|
import { BIOME_CONFIG } from '../generators/linting.js';
|
|
3
3
|
export const PRESET_NAMES = [
|
|
4
|
+
'minimal',
|
|
4
5
|
'library',
|
|
5
6
|
'web-app',
|
|
6
7
|
'node-api',
|
|
@@ -22,6 +23,33 @@ const BASE = {
|
|
|
22
23
|
};
|
|
23
24
|
export function buildPresetConfig(name, projectName) {
|
|
24
25
|
switch (name) {
|
|
26
|
+
// tsconfig + biome + vitest, nothing else (#461). Deliberately not spread
|
|
27
|
+
// from BASE: BASE hardcodes git hooks, commitlint, security automation,
|
|
28
|
+
// badges and the AI agent files, and every other case only ever adds to
|
|
29
|
+
// it — so no preset built on BASE can produce a small repo.
|
|
30
|
+
//
|
|
31
|
+
// `node-api` is the only projectType that adds nothing of its own:
|
|
32
|
+
// `library` would pull in publint, attw, a size-limit budget and a
|
|
33
|
+
// dist/-rooted exports map that `bundler: 'none'` never builds, and the
|
|
34
|
+
// three app types each seed a framework. It says "plain Node package",
|
|
35
|
+
// which is what this preset scaffolds.
|
|
36
|
+
case 'minimal':
|
|
37
|
+
return {
|
|
38
|
+
projectName,
|
|
39
|
+
language: 'js',
|
|
40
|
+
projectType: 'node-api',
|
|
41
|
+
typescript: { enabled: true, config: 'base' },
|
|
42
|
+
linting: { tool: 'biome' },
|
|
43
|
+
formatting: { tool: 'biome' },
|
|
44
|
+
testing: { framework: 'vitest', environment: 'node' },
|
|
45
|
+
gitHooks: false,
|
|
46
|
+
commitLint: false,
|
|
47
|
+
semanticRelease: false,
|
|
48
|
+
securityAutomation: false,
|
|
49
|
+
bundler: 'none',
|
|
50
|
+
badges: false,
|
|
51
|
+
aiSetup: false,
|
|
52
|
+
};
|
|
25
53
|
case 'library':
|
|
26
54
|
return {
|
|
27
55
|
...BASE,
|
|
@@ -208,7 +236,7 @@ export function computeFileList(config) {
|
|
|
208
236
|
if (config.language === 'swift')
|
|
209
237
|
return swiftFileList(config);
|
|
210
238
|
const files = ['package.json', '.repo-tooling.json'];
|
|
211
|
-
files.push('.editorconfig', '.nvmrc', 'knip.json', '.vscode/extensions.json');
|
|
239
|
+
files.push('.editorconfig', '.nvmrc', '.gitignore', 'knip.json', '.vscode/extensions.json');
|
|
212
240
|
if (config.typescript.enabled) {
|
|
213
241
|
files.push('tsconfig.json', 'reset.d.ts');
|
|
214
242
|
}
|
|
@@ -238,7 +266,7 @@ export function computeFileList(config) {
|
|
|
238
266
|
files.push('cypress.config.ts', 'cypress/support/e2e.ts', 'cypress/support/commands.ts', 'tests/e2e/example.cy.ts');
|
|
239
267
|
}
|
|
240
268
|
if (config.gitHooks) {
|
|
241
|
-
files.push('.husky/pre-commit'
|
|
269
|
+
files.push('.husky/pre-commit');
|
|
242
270
|
}
|
|
243
271
|
if (config.commitLint) {
|
|
244
272
|
files.push('.husky/commit-msg', 'commitlint.config.mjs');
|
|
@@ -9,6 +9,60 @@ import { formatGeneratedFiles } from '../utils/format.js';
|
|
|
9
9
|
import { installDependencies } from '../utils/install.js';
|
|
10
10
|
import { LOCKFILE_NAME, writeLockfile } from '../utils/lockfile.js';
|
|
11
11
|
import { buildPresetConfig, computeFileList, CONFIG_SCHEMA, PRESET_NAMES, validateProjectConfig, } from './setup-presets.js';
|
|
12
|
+
/**
|
|
13
|
+
* The opinionated extras a preset turns on, in the order they're offered for
|
|
14
|
+
* review. Each one writes files (or a README block) the user never asked for by
|
|
15
|
+
* name, which is the complaint in #461.
|
|
16
|
+
*/
|
|
17
|
+
const REVIEWABLE_FEATURES = [
|
|
18
|
+
{ key: 'gitHooks', label: 'Git hooks (Husky + lint-staged)' },
|
|
19
|
+
{ key: 'commitLint', label: 'Conventional commit linting (commitlint)' },
|
|
20
|
+
{ key: 'semanticRelease', label: 'Automated releases (semantic-release)' },
|
|
21
|
+
{ key: 'securityAutomation', label: 'Security automation (Dependabot + CodeQL)' },
|
|
22
|
+
{ key: 'badges', label: 'README status badges' },
|
|
23
|
+
{ key: 'aiSetup', label: 'AI agent rules (AGENTS.md, CLAUDE.md, Cursor, Copilot)' },
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* A preset run may only ask questions when a human is on both ends of the pipe.
|
|
27
|
+
* Redirect either direction and this is CI — the case `--preset` exists for —
|
|
28
|
+
* where the one-shot behaviour has to stay exactly as it was.
|
|
29
|
+
*/
|
|
30
|
+
function isInteractiveTerminal() {
|
|
31
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Show what the preset resolved to and let the user drop parts of it before a
|
|
35
|
+
* single file is written (#461).
|
|
36
|
+
*/
|
|
37
|
+
async function reviewPresetConfig(config) {
|
|
38
|
+
const files = computeFileList(config);
|
|
39
|
+
console.log(chalk.cyan(`\n📄 This preset writes ${files.length} files:\n`));
|
|
40
|
+
for (const file of files)
|
|
41
|
+
console.log(chalk.gray(` ${file}`));
|
|
42
|
+
console.log();
|
|
43
|
+
const enabled = REVIEWABLE_FEATURES.filter((feature) => config[feature.key] === true);
|
|
44
|
+
if (enabled.length === 0)
|
|
45
|
+
return config;
|
|
46
|
+
const { features } = await inquirer.prompt([
|
|
47
|
+
{
|
|
48
|
+
type: 'checkbox',
|
|
49
|
+
name: 'features',
|
|
50
|
+
message: '🧹 Uncheck anything you do not want:',
|
|
51
|
+
choices: enabled.map((feature) => ({
|
|
52
|
+
name: feature.label,
|
|
53
|
+
value: feature.key,
|
|
54
|
+
checked: true,
|
|
55
|
+
})),
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
const kept = new Set(features);
|
|
59
|
+
const reviewed = { ...config };
|
|
60
|
+
for (const feature of enabled) {
|
|
61
|
+
if (!kept.has(feature.key))
|
|
62
|
+
reviewed[feature.key] = false;
|
|
63
|
+
}
|
|
64
|
+
return reviewed;
|
|
65
|
+
}
|
|
12
66
|
/** `null` means the run was declined, not that it failed — see promptForConfig. */
|
|
13
67
|
async function resolveConfig(options) {
|
|
14
68
|
if (options.config && options.preset) {
|
|
@@ -38,7 +92,10 @@ async function resolveConfig(options) {
|
|
|
38
92
|
throw new Error(`Unknown preset: ${options.preset}. Available: ${PRESET_NAMES.join(', ')}`);
|
|
39
93
|
}
|
|
40
94
|
const projectName = path.basename(path.resolve(options.directory));
|
|
41
|
-
|
|
95
|
+
const config = buildPresetConfig(options.preset, projectName);
|
|
96
|
+
if (options.dryRun || options.yes || !isInteractiveTerminal())
|
|
97
|
+
return config;
|
|
98
|
+
return reviewPresetConfig(config);
|
|
42
99
|
}
|
|
43
100
|
return promptForConfig(path.resolve(options.directory));
|
|
44
101
|
}
|
|
@@ -110,7 +167,7 @@ async function promptForLanguage(targetDir) {
|
|
|
110
167
|
const detected = await detectLanguage(targetDir);
|
|
111
168
|
const { language } = await inquirer.prompt([
|
|
112
169
|
{
|
|
113
|
-
type: '
|
|
170
|
+
type: 'select',
|
|
114
171
|
name: 'language',
|
|
115
172
|
message: '🗣️ What is the primary language of this repo?',
|
|
116
173
|
choices: Object.values(LANGUAGES).map((module) => ({
|
|
@@ -178,7 +235,7 @@ async function promptForConfig(targetDir) {
|
|
|
178
235
|
validate: (input) => input.trim().length > 0 || 'Project name is required',
|
|
179
236
|
},
|
|
180
237
|
{
|
|
181
|
-
type: '
|
|
238
|
+
type: 'select',
|
|
182
239
|
name: 'projectType',
|
|
183
240
|
message: '🏗️ What type of project are you building?',
|
|
184
241
|
choices: [
|
|
@@ -196,7 +253,7 @@ async function promptForConfig(targetDir) {
|
|
|
196
253
|
default: true,
|
|
197
254
|
},
|
|
198
255
|
{
|
|
199
|
-
type: '
|
|
256
|
+
type: 'select',
|
|
200
257
|
name: 'tsConfig',
|
|
201
258
|
message: '⚙️ Which TypeScript configuration?',
|
|
202
259
|
choices: (answers) => {
|
|
@@ -221,7 +278,7 @@ async function promptForConfig(targetDir) {
|
|
|
221
278
|
when: (answers) => answers.useTypeScript,
|
|
222
279
|
},
|
|
223
280
|
{
|
|
224
|
-
type: '
|
|
281
|
+
type: 'select',
|
|
225
282
|
name: 'lintingTool',
|
|
226
283
|
message: '🔍 Which linting/formatting tool?',
|
|
227
284
|
choices: [
|
|
@@ -233,7 +290,7 @@ async function promptForConfig(targetDir) {
|
|
|
233
290
|
default: 'biome',
|
|
234
291
|
},
|
|
235
292
|
{
|
|
236
|
-
type: '
|
|
293
|
+
type: 'select',
|
|
237
294
|
name: 'eslintConfig',
|
|
238
295
|
message: '🔧 Which ESLint configuration?',
|
|
239
296
|
choices: (answers) => {
|
|
@@ -253,7 +310,7 @@ async function promptForConfig(targetDir) {
|
|
|
253
310
|
when: (answers) => answers.lintingTool !== 'none',
|
|
254
311
|
},
|
|
255
312
|
{
|
|
256
|
-
type: '
|
|
313
|
+
type: 'select',
|
|
257
314
|
name: 'testingFramework',
|
|
258
315
|
message: '🧪 Which testing framework?',
|
|
259
316
|
choices: [
|
|
@@ -266,7 +323,7 @@ async function promptForConfig(targetDir) {
|
|
|
266
323
|
default: 'vitest',
|
|
267
324
|
},
|
|
268
325
|
{
|
|
269
|
-
type: '
|
|
326
|
+
type: 'select',
|
|
270
327
|
name: 'testEnvironment',
|
|
271
328
|
message: '🌍 Test environment?',
|
|
272
329
|
choices: [
|
|
@@ -291,7 +348,7 @@ async function promptForConfig(targetDir) {
|
|
|
291
348
|
when: (answers) => answers.gitHooks,
|
|
292
349
|
},
|
|
293
350
|
{
|
|
294
|
-
type: '
|
|
351
|
+
type: 'select',
|
|
295
352
|
name: 'releaseTool',
|
|
296
353
|
message: '🚀 Automated release tool?',
|
|
297
354
|
choices: [
|
|
@@ -336,7 +393,7 @@ async function promptForConfig(targetDir) {
|
|
|
336
393
|
default: true,
|
|
337
394
|
},
|
|
338
395
|
{
|
|
339
|
-
type: '
|
|
396
|
+
type: 'select',
|
|
340
397
|
name: 'orchestrator',
|
|
341
398
|
message: '🚀 Monorepo task orchestrator?',
|
|
342
399
|
choices: [
|
|
@@ -358,7 +415,7 @@ async function promptForConfig(targetDir) {
|
|
|
358
415
|
answers.projectType === 'nextjs-app',
|
|
359
416
|
},
|
|
360
417
|
{
|
|
361
|
-
type: '
|
|
418
|
+
type: 'select',
|
|
362
419
|
name: 'bundler',
|
|
363
420
|
message: '📦 Which bundler/build tool?',
|
|
364
421
|
choices: (answers) => {
|
|
@@ -53,10 +53,11 @@ export async function generateConfigs(config, targetDir) {
|
|
|
53
53
|
if (config.testing.framework !== 'none') {
|
|
54
54
|
await generateTestingConfigs(config, targetDir);
|
|
55
55
|
}
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
// Unconditional: generateGitConfigs already gates husky and commitlint on
|
|
57
|
+
// their own flags, and the .gitignore it also writes has nothing to do with
|
|
58
|
+
// either. Gating the whole call meant any scaffold that declined git hooks —
|
|
59
|
+
// which is every `minimal` run (#461) — got no .gitignore at all.
|
|
60
|
+
await generateGitConfigs(config, targetDir);
|
|
60
61
|
// Generate GitHub Actions workflow
|
|
61
62
|
await generateGitHubActions(config, targetDir);
|
|
62
63
|
// Generate security automation (Dependabot + CodeQL)
|
|
@@ -123,13 +123,20 @@ export function dependabotIgnoreRules(content) {
|
|
|
123
123
|
return rules;
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
-
* Auto-merges patch + minor Dependabot PRs
|
|
127
|
-
* once CI is green. Requires branch protection with
|
|
128
|
-
* the target branch — without it, \`gh pr merge --auto\`
|
|
126
|
+
* Auto-merges patch + minor Dependabot PRs from the \`dev-minor\` group **and the
|
|
127
|
+
* \`github-actions\` ecosystem** once CI is green. Requires branch protection with
|
|
128
|
+
* required status checks on the target branch — without it, \`gh pr merge --auto\`
|
|
129
|
+
* never fires.
|
|
129
130
|
*
|
|
130
131
|
* Everything else falls through to a human: production bumps ship to consumers
|
|
131
132
|
* of a published package (#423), majors are breaking by definition, and an
|
|
132
|
-
* ungrouped PR reports an empty \`dependency-group\`, so the gate fails closed.
|
|
133
|
+
* ungrouped npm PR reports an empty \`dependency-group\`, so the gate fails closed.
|
|
134
|
+
*
|
|
135
|
+
* CI action bumps are the one ungrouped case that is allowed through, matched on
|
|
136
|
+
* \`package-ecosystem\` (#452): they reach no consumer of the published package
|
|
137
|
+
* and land as \`ci(deps): …\`, which cuts no release. The counter-argument is real
|
|
138
|
+
* — a compromised action runs in CI holding \`GITHUB_TOKEN\` — so the majors
|
|
139
|
+
* exclusion and required status checks are doing the work here.
|
|
133
140
|
*
|
|
134
141
|
* The group name is the one \`dependabotConfig()\` writes — the two files are a
|
|
135
142
|
* paired unit and have to move together.
|
|
@@ -201,10 +208,17 @@ jobs:
|
|
|
201
208
|
# Belt and braces: the dev-minor group is already declared minor+patch in
|
|
202
209
|
# dependabot.yml, but this workflow is the security gate and shouldn't
|
|
203
210
|
# trust a config file a consumer repo can edit independently.
|
|
204
|
-
|
|
211
|
+
#
|
|
212
|
+
# github-actions bumps are ungrouped, so they report an empty
|
|
213
|
+
# dependency-group and are matched by ecosystem instead (#452). They reach
|
|
214
|
+
# no consumer of the published package, and their squash subject is
|
|
215
|
+
# "ci(deps): …", which cuts no release. Majors still fall through to a
|
|
216
|
+
# human — the update-type clause below applies to them too.
|
|
217
|
+
- name: Auto-merge dev-dependency and CI action patch and minor updates
|
|
205
218
|
if: |
|
|
206
219
|
steps.gate.outputs.safe == 'true' &&
|
|
207
|
-
steps.metadata.outputs.dependency-group == 'dev-minor'
|
|
220
|
+
(steps.metadata.outputs.dependency-group == 'dev-minor' ||
|
|
221
|
+
steps.metadata.outputs.package-ecosystem == 'github-actions') &&
|
|
208
222
|
(steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
|
|
209
223
|
steps.metadata.outputs.update-type == 'version-update:semver-minor')
|
|
210
224
|
run: gh pr merge --auto --squash "$PR_URL"
|
package/dist/cli/index.js
CHANGED
|
@@ -29,6 +29,7 @@ program
|
|
|
29
29
|
.option('-d, --directory <path>', 'Target directory for setup', process.cwd())
|
|
30
30
|
.option('--skip-install', 'Skip installing dependencies')
|
|
31
31
|
.option('--preset <name>', 'Skip prompts; use defaults for a project type')
|
|
32
|
+
.option('-y, --yes', 'With --preset in a terminal, skip the file-list review and write everything')
|
|
32
33
|
.option('--config <path>', 'Skip prompts; read a ProjectConfig or .repo-tooling.json lockfile from <path>')
|
|
33
34
|
.option('--dry-run', 'Print the resolved config and file list, write nothing')
|
|
34
35
|
.option('--config-schema', 'Print the JSON Schema for ProjectConfig and exit')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtorcato/repo-tooling",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.16.0",
|
|
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": [
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"dev": "pnpm --filter @rtorcato/docs dev",
|
|
28
28
|
"docs:build": "pnpm --filter @rtorcato/docs build",
|
|
29
29
|
"==================== Common ====================": "",
|
|
30
|
-
"lint": "pnpm exec biome lint
|
|
31
|
-
"format": "pnpm exec biome format
|
|
32
|
-
"check:fix": "pnpm exec biome check --
|
|
33
|
-
"check": "pnpm exec biome check
|
|
30
|
+
"lint": "pnpm exec biome lint .",
|
|
31
|
+
"format": "pnpm exec biome format .",
|
|
32
|
+
"check:fix": "pnpm exec biome check --fix .",
|
|
33
|
+
"check": "pnpm exec biome check .",
|
|
34
34
|
"prepare": "is-ci || husky",
|
|
35
35
|
"typecheck": "tsc --noEmit --project src/cli/tsconfig.json",
|
|
36
36
|
"test": "vitest",
|
|
@@ -52,6 +52,20 @@ header names the agent *and* says why it is wearing a human's face:
|
|
|
52
52
|
|
|
53
53
|
`🤖 *Automated — <which agent> via ai-issue-loop. Posted under the owner's account by an agent; not a human message. There is no separate GitHub account for AI agents, so this appears under @<owner>'s avatar.*`
|
|
54
54
|
|
|
55
|
+
**Comment budget: ≤10 lines, and a clean outcome gets no comment at all.** A
|
|
56
|
+
40-line comment on every PR trains the reader to skip all of them, including the
|
|
57
|
+
one that matters — the same failure mode as `ai-notes` on every PR, below. Link
|
|
58
|
+
the reviewer's `### Before merging` rather than restating it; a paraphrase is
|
|
59
|
+
drift with a second copy to maintain.
|
|
60
|
+
|
|
61
|
+
| Outcome | Comment |
|
|
62
|
+
|---|---|
|
|
63
|
+
| Clean and ready | **None.** `ai-ok-code, ai-ok-sec` + assigned + no `ai-review` already says it. |
|
|
64
|
+
| `ai-notes` | ≤10 lines; link the reviewer's `### Before merging`. |
|
|
65
|
+
| `ai-changes`, CI red, `ai-blocked` | ≤10 lines, action first, then the specific cause. |
|
|
66
|
+
| Reviewer verdict | `### Before merging` plus at most 3 short paragraphs above it. |
|
|
67
|
+
| Declining an issue | The one exception — a hard handoff needs its reasoning; see Pass 4. |
|
|
68
|
+
|
|
55
69
|
## Labels
|
|
56
70
|
|
|
57
71
|
| Label | On | Meaning |
|
|
@@ -250,7 +264,10 @@ It lands in the user's *Assigned to you* view, and the labels then read as state
|
|
|
250
264
|
than noise — `ai-ok-code, ai-ok-sec` with no `ai-review` means **waiting on you**. Both
|
|
251
265
|
halves matter: Pass 3 only ever *adds* the `ai-ok-*` labels, so without the removal a
|
|
252
266
|
finished PR keeps wearing `ai-review` forever and looks mid-review. Idempotent, so
|
|
253
|
-
re-running a tick is harmless. Take no other action — do not merge
|
|
267
|
+
re-running a tick is harmless. Take no other action — do not merge, and **post no
|
|
268
|
+
comment**: nothing is wrong, so those three labels are the whole message. A
|
|
269
|
+
comment is how the loop records what a label cannot; a clean PR has nothing to
|
|
270
|
+
record.
|
|
254
271
|
|
|
255
272
|
**Never strip `ai-notes` here.** It is the whole point of the handoff: it has to
|
|
256
273
|
survive to the moment of merging, which is the moment it is for. A ready PR reads
|
|
@@ -281,7 +298,8 @@ a required check, so nothing in the check list looked wrong either.
|
|
|
281
298
|
|
|
282
299
|
Diff-scoped reviewers cannot catch this — they never see CI. So when a
|
|
283
300
|
both-passed PR is not `CLEAN`, do not assign it as ready. Send it back, and
|
|
284
|
-
**comment why
|
|
301
|
+
**comment why** — ≤10 lines, leading with what must change, then the failing
|
|
302
|
+
check and its error. The reviewers passed it, so the fix-round implementer would
|
|
285
303
|
otherwise read the comments and find no instruction to act on.
|
|
286
304
|
|
|
287
305
|
```bash
|
|
@@ -506,7 +524,10 @@ Reviewer prompt template:
|
|
|
506
524
|
>
|
|
507
525
|
> That section is what a human reads at merge time, so put anything you would
|
|
508
526
|
> want them to know there rather than leaving it in the prose above — a finding
|
|
509
|
-
> buried mid-paragraph does not survive the handoff.
|
|
527
|
+
> buried mid-paragraph does not survive the handoff. For the same reason, **cap
|
|
528
|
+
> the body at that section plus at most 3 short paragraphs above it**: no process
|
|
529
|
+
> narration, no restating the diff, no listing what you checked and found fine.
|
|
530
|
+
> The bar is a finding that
|
|
510
531
|
> **changes what a human would do**: a semver implication, a deliberate
|
|
511
532
|
> omission, a follow-up that must be filed. Not observations, not praise, not
|
|
512
533
|
> restating the diff. Writing `Nothing.` is a real verdict and the common one —
|
|
@@ -543,7 +564,8 @@ Reviewer prompt template:
|
|
|
543
564
|
> a Dependabot PR it suppresses auto-merge outright. Use `ai-changes` only when
|
|
544
565
|
> you can name a concrete change an agent could make.
|
|
545
566
|
>
|
|
546
|
-
> Say nothing else and return a one-line summary
|
|
567
|
+
> Say nothing else and return a one-line summary — that governs your reply to the
|
|
568
|
+
> orchestrator; the comment body is capped separately, above.
|
|
547
569
|
|
|
548
570
|
**Dependabot PRs use a different prompt** — the one above would burn the tick on a
|
|
549
571
|
lockfile. `js-common` #148 bumps 20 packages and its *entire* diff is
|
|
@@ -611,7 +633,8 @@ package/from/to table survives because it sits at the top; classify from that.
|
|
|
611
633
|
> State in your comment which rule fired, name the packages that tripped it, and say
|
|
612
634
|
> whether the body was truncated so the reader knows what you could and couldn't see.
|
|
613
635
|
> Same `🤖 *Automated review — …*` header line, same closing `### Before merging`
|
|
614
|
-
> section, and same one-verdict-label rule as
|
|
636
|
+
> section, same 3-paragraph cap on the body, and same one-verdict-label rule as
|
|
637
|
+
> above — **including clearing your
|
|
615
638
|
> `<ai-reviewing-code|ai-reviewing-sec>` claim label in the same `gh pr edit`**.
|
|
616
639
|
> Pass 3 claimed you with it before spawning you, and a claim left behind wedges
|
|
617
640
|
> your half of the review until Pass 2 reaps it.
|
|
@@ -716,7 +739,9 @@ real work to reach is lost.
|
|
|
716
739
|
|
|
717
740
|
The comment opens with the standard `🤖 *Automated …*` header — see the top of this
|
|
718
741
|
file; it must state that no GitHub account exists for AI agents, so the comment
|
|
719
|
-
wears the owner's avatar. Then, in the body
|
|
742
|
+
wears the owner's avatar. Then, in the body — **this is the one comment exempt
|
|
743
|
+
from the ≤10-line budget, and only this one.** Declining is a hard handoff whose
|
|
744
|
+
whole value is the reasoning; do not reach for this shape on a PR handoff:
|
|
720
745
|
|
|
721
746
|
- **Why an agent cannot finish it**, concretely. "Not suitable" is useless. Name
|
|
722
747
|
the blocker: binary assets it cannot author, a force-push past branch
|
|
@@ -47,8 +47,9 @@ npx @rtorcato/repo-tooling fix <target> --diff # unified diff before c
|
|
|
47
47
|
## Scaffolding a new project
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
|
-
# Quick: from a named preset (library | web-app | node-api | nextjs-app | react-app | swift-library)
|
|
51
|
-
|
|
50
|
+
# Quick: from a named preset (minimal | library | web-app | node-api | nextjs-app | react-app | swift-library)
|
|
51
|
+
# --yes skips the interactive file-list review; without it, a terminal lets you deselect.
|
|
52
|
+
npx @rtorcato/repo-tooling setup --preset library -d ./my-lib --skip-install --yes
|
|
52
53
|
|
|
53
54
|
# Full control: validate a config against the schema, preview, then write
|
|
54
55
|
npx @rtorcato/repo-tooling setup --config-schema > project-config.schema.json
|
package/tooling/biome/biome.json
CHANGED
|
@@ -1,63 +1,64 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
|
|
3
|
+
"root": false,
|
|
4
|
+
"vcs": {
|
|
5
|
+
"enabled": true,
|
|
6
|
+
"clientKind": "git",
|
|
7
|
+
"useIgnoreFile": true
|
|
8
|
+
},
|
|
9
|
+
"files": {
|
|
10
|
+
"ignoreUnknown": false,
|
|
11
|
+
"includes": [
|
|
12
|
+
"**",
|
|
13
|
+
"!**/node_modules",
|
|
14
|
+
"!**/package.json",
|
|
15
|
+
"!**/tsconfig*.json",
|
|
16
|
+
"!**/dist",
|
|
17
|
+
"!**/coverage",
|
|
18
|
+
"!**/.git",
|
|
19
|
+
"!**/.github",
|
|
20
|
+
"!**/.vscode"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"formatter": {
|
|
24
|
+
"enabled": true,
|
|
25
|
+
"indentStyle": "tab",
|
|
26
|
+
"lineWidth": 100
|
|
27
|
+
},
|
|
28
|
+
"linter": {
|
|
29
|
+
"enabled": true,
|
|
30
|
+
"rules": {
|
|
31
|
+
"preset": "recommended",
|
|
32
|
+
"style": {
|
|
33
|
+
"noInferrableTypes": "off"
|
|
34
|
+
},
|
|
35
|
+
"complexity": {
|
|
36
|
+
"useLiteralKeys": "off"
|
|
37
|
+
},
|
|
38
|
+
"suspicious": {
|
|
39
|
+
"noExplicitAny": "off",
|
|
40
|
+
"noTemplateCurlyInString": "off"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"assist": {
|
|
45
|
+
"actions": {
|
|
46
|
+
"source": {
|
|
47
|
+
"organizeImports": "off"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"javascript": {
|
|
52
|
+
"formatter": {
|
|
53
|
+
"quoteStyle": "single",
|
|
54
|
+
"trailingCommas": "es5",
|
|
55
|
+
"semicolons": "asNeeded"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"json": {
|
|
59
|
+
"formatter": {
|
|
60
|
+
"indentStyle": "space",
|
|
61
|
+
"indentWidth": 2
|
|
62
|
+
}
|
|
63
|
+
}
|
|
63
64
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
"$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json",
|
|
3
|
+
"changelog": "@changesets/cli/changelog",
|
|
4
|
+
"commit": false,
|
|
5
|
+
"fixed": [],
|
|
6
|
+
"linked": [],
|
|
7
|
+
"access": "public",
|
|
8
|
+
"baseBranch": "main",
|
|
9
|
+
"updateInternalDependencies": "patch",
|
|
10
|
+
"ignore": []
|
|
11
11
|
}
|
|
@@ -51,8 +51,9 @@ npx @rtorcato/repo-tooling fix <target> --diff # unified diff before c
|
|
|
51
51
|
## Scaffolding a new project
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
# Quick: from a named preset (library | web-app | node-api | nextjs-app | react-app | swift-library)
|
|
55
|
-
|
|
54
|
+
# Quick: from a named preset (minimal | library | web-app | node-api | nextjs-app | react-app | swift-library)
|
|
55
|
+
# --yes skips the interactive file-list review; without it, a terminal lets you deselect.
|
|
56
|
+
npx @rtorcato/repo-tooling setup --preset library -d ./my-lib --skip-install --yes
|
|
56
57
|
|
|
57
58
|
# Full control: validate a config against the schema, preview, then write
|
|
58
59
|
npx @rtorcato/repo-tooling setup --config-schema > project-config.schema.json
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
extends: [
|
|
2
|
+
extends: ['@commitlint/config-conventional'],
|
|
3
3
|
ignores: [
|
|
4
|
-
(commit) => commit.includes(
|
|
4
|
+
(commit) => commit.includes('[skip ci]'),
|
|
5
5
|
// Bot commits are machine-written end to end, including headers long
|
|
6
6
|
// enough to trip header-max-length. Skipping them wholesale keeps bot
|
|
7
7
|
// PRs green — squash-merge uses the PR title, which is still linted on
|
|
@@ -12,43 +12,43 @@ export default {
|
|
|
12
12
|
],
|
|
13
13
|
rules: {
|
|
14
14
|
// Enforce strict type validation
|
|
15
|
-
|
|
15
|
+
'type-enum': [
|
|
16
16
|
2,
|
|
17
|
-
|
|
17
|
+
'always',
|
|
18
18
|
[
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
'build',
|
|
20
|
+
'chore',
|
|
21
|
+
'ci',
|
|
22
|
+
'docs',
|
|
23
|
+
'feat',
|
|
24
|
+
'fix',
|
|
25
|
+
'perf',
|
|
26
|
+
'refactor',
|
|
27
|
+
'revert',
|
|
28
|
+
'style',
|
|
29
|
+
'test',
|
|
30
30
|
],
|
|
31
31
|
],
|
|
32
32
|
// 100 is the conventional-commits/semantic-release default. 72 was too
|
|
33
33
|
// tight: GitHub appends " (#NN)" to squash commits, overflowing the
|
|
34
34
|
// header on main and skipping the release.
|
|
35
|
-
|
|
35
|
+
'header-max-length': [2, 'always', 100],
|
|
36
36
|
// Body/footer length is unenforced. Machine-written commits (agents,
|
|
37
37
|
// bots) don't wrap, and a `BREAKING CHANGE:` footer — the input
|
|
38
38
|
// semantic-release reads to cut a major — is the worst thing to make
|
|
39
39
|
// someone hand-rewrap. The subject rules below decide the release
|
|
40
40
|
// type and stay enforced.
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
'body-max-line-length': [0],
|
|
42
|
+
'footer-max-line-length': [0],
|
|
43
43
|
// Enforce case rules (allow common patterns)
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
'subject-case': [0], // Disable case enforcement to allow flexibility
|
|
45
|
+
'type-case': [2, 'always', 'lower-case'],
|
|
46
46
|
// Enforce required elements
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
'type-empty': [2, 'never'],
|
|
48
|
+
'subject-empty': [2, 'never'],
|
|
49
49
|
// Enforce punctuation rules
|
|
50
|
-
|
|
50
|
+
'subject-full-stop': [2, 'never', '.'],
|
|
51
51
|
// Enforce scope rules (optional but recommended)
|
|
52
|
-
|
|
52
|
+
'scope-case': [2, 'always', 'lower-case'],
|
|
53
53
|
},
|
|
54
|
-
}
|
|
54
|
+
}
|
package/tooling/nx/nx.json
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/nrwl/nx/master/packages/nx/schemas/nx-schema.json",
|
|
3
|
+
"namedInputs": {
|
|
4
|
+
"default": ["{projectRoot}/**/*", "sharedGlobals"],
|
|
5
|
+
"production": ["default"],
|
|
6
|
+
"sharedGlobals": []
|
|
7
|
+
},
|
|
8
|
+
"targetDefaults": {
|
|
9
|
+
"build": {
|
|
10
|
+
"dependsOn": ["^build"],
|
|
11
|
+
"outputs": ["{projectRoot}/dist"],
|
|
12
|
+
"cache": true
|
|
13
|
+
},
|
|
14
|
+
"lint": { "cache": true },
|
|
15
|
+
"typecheck": {
|
|
16
|
+
"dependsOn": ["^build"],
|
|
17
|
+
"cache": true
|
|
18
|
+
},
|
|
19
|
+
"test": {
|
|
20
|
+
"dependsOn": ["^build"],
|
|
21
|
+
"cache": true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
24
|
}
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
|
|
3
|
+
"categories": {
|
|
4
|
+
"correctness": "error",
|
|
5
|
+
"perf": "warn",
|
|
6
|
+
"suspicious": "warn",
|
|
7
|
+
"pedantic": "off",
|
|
8
|
+
"style": "off",
|
|
9
|
+
"restriction": "off",
|
|
10
|
+
"nursery": "off"
|
|
11
|
+
},
|
|
12
|
+
"plugins": ["typescript", "unicorn", "oxc", "import"],
|
|
13
|
+
"rules": {
|
|
14
|
+
"no-console": "warn",
|
|
15
|
+
"no-debugger": "error",
|
|
16
|
+
"no-empty": "warn",
|
|
17
|
+
"no-unused-vars": "off",
|
|
18
|
+
"@typescript-eslint/no-unused-vars": [
|
|
19
|
+
"warn",
|
|
20
|
+
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
|
|
21
|
+
],
|
|
22
|
+
"unicorn/filename-case": "off",
|
|
23
|
+
"unicorn/no-null": "off",
|
|
24
|
+
"unicorn/prevent-abbreviations": "off",
|
|
25
|
+
"import/no-default-export": "off"
|
|
26
|
+
},
|
|
27
|
+
"ignorePatterns": ["node_modules", "dist", "build", "coverage", ".next", "*.d.ts"]
|
|
28
28
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
|
+
"packages": {
|
|
4
|
+
".": {
|
|
5
|
+
"release-type": "node",
|
|
6
|
+
"changelog-path": "CHANGELOG.md"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
9
|
}
|
|
@@ -24,7 +24,12 @@ import { defineConfig } from 'rolldown'
|
|
|
24
24
|
* @param {import('./rolldown.config.d.mts').GetConfigOptions} [options]
|
|
25
25
|
*/
|
|
26
26
|
export function getConfig(options = {}) {
|
|
27
|
-
const {
|
|
27
|
+
const {
|
|
28
|
+
input = 'src/index.ts',
|
|
29
|
+
outDir = 'dist',
|
|
30
|
+
sourcemap = true,
|
|
31
|
+
external = isBareImport,
|
|
32
|
+
} = options
|
|
28
33
|
|
|
29
34
|
return defineConfig({
|
|
30
35
|
input,
|