@rtorcato/repo-tooling 3.15.3 → 3.16.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.
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 exec biome check --config-path=tooling/biome/biome.json src scripts`)
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).
@@ -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', '.gitignore');
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
- return buildPresetConfig(options.preset, projectName);
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
  }
@@ -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
- // Generate git configurations
57
- if (config.gitHooks || config.commitLint) {
58
- await generateGitConfigs(config, targetDir);
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)
@@ -35,55 +35,17 @@ export const BIOME_CONFIG = 'biome.json';
35
35
  */
36
36
  export const BIOME_LEGACY_CONFIG = 'biome.jsonc';
37
37
  /**
38
- * Used when nothing in the target repo says which Biome will read the config —
39
- * a bare directory mid-`setup`, before `pnpm install` has run. Matches the
40
- * `$schema` the shipped preset carries.
41
- */
42
- const BIOME_SCHEMA_FALLBACK = '2.5.0';
43
- async function readJsonOrNull(filepath) {
44
- try {
45
- return (await fs.readJson(filepath));
46
- }
47
- catch {
48
- return null;
49
- }
50
- }
51
- /**
52
- * The `@biomejs/biome` actually installed under `targetDir`, or null when
53
- * node_modules isn't populated. This is the binary that will parse the config
54
- * and emit the schema-mismatch warning, so it is the only version worth
55
- * comparing a written `$schema` against — a declared range like `^2.5.0` says
56
- * nothing about which 2.x resolved (#424).
57
- */
58
- export async function installedBiomeVersion(targetDir) {
59
- const installed = await readJsonOrNull(path.join(targetDir, 'node_modules', '@biomejs', 'biome', 'package.json'));
60
- const match = typeof installed?.version === 'string' ? /(\d+\.\d+\.\d+)/.exec(installed.version) : null;
61
- return match?.[1] ?? null;
62
- }
63
- /**
64
- * The Biome version the scaffolded `$schema` URL should name (#363).
38
+ * Unversioned on purpose (#468). A `$schema` naming an exact version is only
39
+ * ever right until the next `@biomejs/biome` bump, at which point Biome prints
40
+ * "The configuration schema version does not match the CLI version" on every
41
+ * invocation and doctor reports drift — so every Dependabot bump needed a hand
42
+ * edit before it could go green. Biome accepts `latest` and says nothing about
43
+ * it (verified against 2.5.5), which removes the failure class outright.
65
44
  *
66
- * A hardcoded version drifts the moment the consumer's Biome moves, and Biome
67
- * reports the mismatch on *every* invocation ("The configuration schema version
68
- * does not match the CLI version 2.5.7") — noise on a file the consumer never
69
- * wrote. The installed package wins because it is literally the binary that
70
- * will parse the file and emit that warning; the declared range is the next
71
- * best statement of intent when node_modules isn't populated yet.
45
+ * `latest` rather than dropping the key: editors still resolve a real schema
46
+ * for autocomplete and validation, which is the only reason the key is there.
72
47
  */
73
- export async function resolveBiomeSchemaVersion(targetDir) {
74
- const installed = await installedBiomeVersion(targetDir);
75
- if (installed)
76
- return installed;
77
- const pkg = await readJsonOrNull(path.join(targetDir, 'package.json'));
78
- const deps = {
79
- ...(pkg?.dependencies ?? {}),
80
- ...(pkg?.devDependencies ?? {}),
81
- };
82
- // A range ("^2.5.0") carries the x.y.z we need just as an exact version does.
83
- const declared = deps['@biomejs/biome'];
84
- const match = typeof declared === 'string' ? /(\d+\.\d+\.\d+)/.exec(declared) : null;
85
- return match?.[1] ?? BIOME_SCHEMA_FALLBACK;
86
- }
48
+ const BIOME_SCHEMA_URL = 'https://biomejs.dev/schemas/latest/schema.json';
87
49
  /**
88
50
  * The thin pointer config — the same shape this repo dogfoods. `fix biome` used
89
51
  * to copy the whole preset inline instead, which meant the scaffolded file
@@ -95,9 +57,8 @@ export async function generateBiomeConfig(targetDir) {
95
57
  // file globs via `files.includes`; emitting the old 1.x `include`/`ignore`
96
58
  // keys here forced consumers to run `biome migrate` before `biome check`
97
59
  // would run at all.
98
- const version = await resolveBiomeSchemaVersion(targetDir);
99
60
  const biomeConfig = {
100
- $schema: `https://biomejs.dev/schemas/${version}/schema.json`,
61
+ $schema: BIOME_SCHEMA_URL,
101
62
  extends: ['@rtorcato/repo-tooling/biome'],
102
63
  };
103
64
  await fs.writeJson(path.join(targetDir, BIOME_CONFIG), biomeConfig, { spaces: 2 });
@@ -123,13 +123,20 @@ export function dependabotIgnoreRules(content) {
123
123
  return rules;
124
124
  }
125
125
  /**
126
- * Auto-merges patch + minor Dependabot PRs **from the `dev-minor` group only**
127
- * once CI is green. Requires branch protection with required status checks on
128
- * the target branch — without it, \`gh pr merge --auto\` never fires.
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
- - name: Auto-merge dev-dependency patch and minor updates
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')
@@ -1,7 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'fs-extra';
3
3
  import { checkFile, hookHasUncommented } from '../../base/checks.js';
4
- import { installedBiomeVersion } from '../../cli/generators/linting.js';
5
4
  import { CLAUDE_SETTINGS_FILE, readClaudeSettings, workspaceSymlinkDirs, worktreeSymlinkDirs, } from '../../cli/generators/agent-rules.js';
6
5
  import { WORKSPACE_FILE, dependsOnEsbuild, familyGlob, missingPnpmSettings, } from '../../cli/generators/pnpm-workspace.js';
7
6
  const PACKAGE = '@rtorcato/repo-tooling';
@@ -163,38 +162,38 @@ export const BIOME_FILE_CHECK = {
163
162
  };
164
163
  /**
165
164
  * The Biome check plus the one thing a contents-only matcher can't see: whether
166
- * the `$schema` URL still names the Biome that will read it (#424).
165
+ * the `$schema` URL still pins a version (#424, #468).
167
166
  *
168
- * `fix biome` writes the URL from the version resolved at scaffold time, and
169
- * nothing re-checked it afterwards — so once the repo's Biome moved on, every
170
- * single invocation (the pre-commit hook included) printed "The configuration
171
- * schema version does not match the CLI version", while doctor stayed clean.
167
+ * It began as a drift check — the URL named the Biome resolved at scaffold
168
+ * time, nothing re-checked it, and once the repo's Biome moved on every single
169
+ * invocation (the pre-commit hook included) printed "The configuration schema
170
+ * version does not match the CLI version" while doctor stayed clean.
172
171
  *
173
- * Compared against the *installed* package only, never the declared range: an
174
- * exact match is what Biome itself demands, and `^2.5.0` doesn't say which 2.x
175
- * resolved. With no node_modules there is nothing to be wrong about, so the
176
- * check says nothing rather than guessing.
172
+ * Chasing the installed version was treating the symptom: an exact pin is
173
+ * *only ever* correct until the next bump, so every `@biomejs/biome` update
174
+ * failed the build until someone hand-edited a URL. `fix biome` now writes the
175
+ * unpinned `latest`, and this reads as a migration signal instead — any
176
+ * surviving pin is a repo that predates the unpinning, whatever version it
177
+ * names. That makes it independent of node_modules, so it also fires on a
178
+ * fresh clone, and it goes quiet for good once the repo is migrated.
177
179
  */
178
180
  export async function checkBiome(dir) {
179
181
  const result = await checkFile(dir, BIOME_FILE_CHECK);
180
182
  if (result.status !== 'ok')
181
183
  return result;
182
- const installed = await installedBiomeVersion(dir);
183
- if (!installed)
184
- return result;
185
184
  // The same candidate checkFile settled on — first one that exists wins.
186
185
  for (const candidate of BIOME_FILE_CHECK.candidates) {
187
186
  const filepath = path.join(dir, candidate);
188
187
  if (!(await fs.pathExists(filepath)))
189
188
  continue;
190
189
  const url = readSchemaUrl(await fs.readFile(filepath, 'utf8'));
191
- const targeted = url?.includes('biomejs.dev') ? schemaUrlVersion(url)?.join('.') : null;
192
- if (targeted && targeted !== installed) {
190
+ const pinned = url?.includes('biomejs.dev') ? schemaUrlVersion(url)?.join('.') : null;
191
+ if (pinned) {
193
192
  return {
194
193
  check: BIOME_FILE_CHECK.check,
195
194
  status: 'drift',
196
- detail: `${candidate} targets Biome ${targeted} but @biomejs/biome ${installed} is installed`,
197
- hint: 'Run `npx @rtorcato/repo-tooling fix biome` to rewrite the $schema URL',
195
+ detail: `${candidate} pins Biome ${pinned} in $schema — the next @biomejs/biome bump past it breaks every Biome run`,
196
+ hint: 'Run `npx @rtorcato/repo-tooling fix biome` to drop the version from the $schema URL',
198
197
  };
199
198
  }
200
199
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.15.3",
3
+ "version": "3.16.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": [
@@ -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 --config-path=biome.json src scripts",
31
- "format": "pnpm exec biome format --config-path=biome.json src scripts",
32
- "check:fix": "pnpm exec biome check --config-path=biome.json --fix src scripts",
33
- "check": "pnpm exec biome check --config-path=biome.json src scripts",
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",
@@ -237,15 +237,15 @@
237
237
  "inquirer": "^14.0.2"
238
238
  },
239
239
  "devDependencies": {
240
- "@biomejs/biome": "^2.5.5",
240
+ "@biomejs/biome": "^2.5.8",
241
241
  "@types/diff": "^8.0.0",
242
- "@commitlint/cli": "^21.2.1",
243
- "@commitlint/config-conventional": "^21.2.0",
242
+ "@commitlint/cli": "^21.2.2",
243
+ "@commitlint/config-conventional": "^21.2.2",
244
244
  "@commitlint/types": "^21.2.0",
245
245
  "@eslint/js": "^10.0.1",
246
246
  "@ianvs/prettier-plugin-sort-imports": "^4.4.2",
247
- "@next/eslint-plugin-next": "^16.2.11",
248
- "@playwright/test": "^1.62.0",
247
+ "@next/eslint-plugin-next": "^16.3.0",
248
+ "@playwright/test": "^1.62.1",
249
249
  "@rollup/plugin-typescript": "^12.0.0",
250
250
  "@semantic-release/commit-analyzer": "^13.0.1",
251
251
  "@semantic-release/exec": "^7.1.0",
@@ -254,43 +254,43 @@
254
254
  "@semantic-release/release-notes-generator": "^14.1.1",
255
255
  "@total-typescript/ts-reset": "0.6.1",
256
256
  "@types/fs-extra": "^11.0.4",
257
- "@types/node": "^26.1.1",
258
- "@typescript-eslint/eslint-plugin": "^8.65.0",
259
- "@typescript-eslint/parser": "^8.65.0",
260
- "@vitejs/plugin-react": "^6.0.4",
257
+ "@types/node": "^26.2.0",
258
+ "@typescript-eslint/eslint-plugin": "^8.67.0",
259
+ "@typescript-eslint/parser": "^8.67.0",
260
+ "@vitejs/plugin-react": "^6.0.5",
261
261
  "@vitest/coverage-v8": "^4.1.10",
262
262
  "commitizen": "^4.3.1",
263
- "conventional-changelog-conventionalcommits": "^10.2.1",
263
+ "conventional-changelog-conventionalcommits": "^10.3.0",
264
264
  "cz-conventional-changelog": "^3.3.0",
265
- "esbuild": "^0.28.0",
265
+ "esbuild": "^0.28.2",
266
266
  "esbuild-node-externals": "^2.0.0",
267
- "eslint": "10.8.0",
267
+ "eslint": "10.8.1",
268
268
  "eslint-plugin-import": "^2.32.0",
269
- "eslint-plugin-jest": "29.16.0",
269
+ "eslint-plugin-jest": "29.16.1",
270
270
  "husky": "^9.1.7",
271
271
  "is-ci": "^4.1.0",
272
272
  "jest": "^30.4.2",
273
273
  "jsdom": "^30.0.1",
274
- "knip": "^6.29.0",
274
+ "knip": "^6.32.2",
275
275
  "prettier": "^3.9.6",
276
276
  "rimraf": "6.1.3",
277
- "rollup": "^4.62.0",
278
- "semantic-release": "^25.0.8",
277
+ "rollup": "^4.62.4",
278
+ "semantic-release": "^25.0.9",
279
279
  "ts-jest": "^29.4.12",
280
280
  "tslib": "^2.8.0",
281
281
  "tsup": "8.5.1",
282
282
  "typescript": "^7.0.2",
283
- "typescript-eslint": "^8.65.0",
283
+ "typescript-eslint": "^8.67.0",
284
284
  "vitest": "^4.1.10"
285
285
  },
286
286
  "peerDependencies": {
287
287
  "@biomejs/biome": "^2.5.0",
288
288
  "@commitlint/cli": "^20.0.0 || ^21.0.0",
289
- "@commitlint/config-conventional": "^21.2.0",
289
+ "@commitlint/config-conventional": "^21.2.2",
290
290
  "@commitlint/types": "^21.2.0",
291
291
  "@eslint/js": "^9.0.0",
292
292
  "@ianvs/prettier-plugin-sort-imports": "^4.0.0",
293
- "@next/eslint-plugin-next": "^16.2.11",
293
+ "@next/eslint-plugin-next": "^16.3.0",
294
294
  "@rollup/plugin-typescript": "^12.0.0",
295
295
  "@semantic-release/changelog": "^6.0.0",
296
296
  "@semantic-release/commit-analyzer": "^13.0.0",
@@ -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**: the reviewers passed it, so the fix-round implementer would
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. The bar is a finding that
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 above — **including clearing your
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
- npx @rtorcato/repo-tooling setup --preset library -d ./my-lib --skip-install
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
@@ -1,63 +1,64 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.5.0/schema.json",
3
- "vcs": {
4
- "enabled": true,
5
- "clientKind": "git",
6
- "useIgnoreFile": true
7
- },
8
- "files": {
9
- "ignoreUnknown": false,
10
- "includes": [
11
- "**",
12
- "!**/node_modules",
13
- "!**/package.json",
14
- "!**/tsconfig*.json",
15
- "!**/dist",
16
- "!**/coverage",
17
- "!**/.git",
18
- "!**/.github",
19
- "!**/.vscode"
20
- ]
21
- },
22
- "formatter": {
23
- "enabled": true,
24
- "indentStyle": "tab",
25
- "lineWidth": 100
26
- },
27
- "linter": {
28
- "enabled": true,
29
- "rules": {
30
- "preset": "recommended",
31
- "style": {
32
- "noInferrableTypes": "off"
33
- },
34
- "complexity": {
35
- "useLiteralKeys": "off"
36
- },
37
- "suspicious": {
38
- "noExplicitAny": "off",
39
- "noTemplateCurlyInString": "off"
40
- }
41
- }
42
- },
43
- "assist": {
44
- "actions": {
45
- "source": {
46
- "organizeImports": "off"
47
- }
48
- }
49
- },
50
- "javascript": {
51
- "formatter": {
52
- "quoteStyle": "single",
53
- "trailingCommas": "es5",
54
- "semicolons": "asNeeded"
55
- }
56
- },
57
- "json": {
58
- "formatter": {
59
- "indentStyle": "space",
60
- "indentWidth": 2
61
- }
62
- }
2
+ "$schema": "https://biomejs.dev/schemas/latest/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
- "$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": []
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
- npx @rtorcato/repo-tooling setup --preset library -d ./my-lib --skip-install
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: ["@commitlint/config-conventional"],
2
+ extends: ['@commitlint/config-conventional'],
3
3
  ignores: [
4
- (commit) => commit.includes("[skip ci]"),
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
- "type-enum": [
15
+ 'type-enum': [
16
16
  2,
17
- "always",
17
+ 'always',
18
18
  [
19
- "build",
20
- "chore",
21
- "ci",
22
- "docs",
23
- "feat",
24
- "fix",
25
- "perf",
26
- "refactor",
27
- "revert",
28
- "style",
29
- "test",
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
- "header-max-length": [2, "always", 100],
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
- "body-max-line-length": [0],
42
- "footer-max-line-length": [0],
41
+ 'body-max-line-length': [0],
42
+ 'footer-max-line-length': [0],
43
43
  // Enforce case rules (allow common patterns)
44
- "subject-case": [0], // Disable case enforcement to allow flexibility
45
- "type-case": [2, "always", "lower-case"],
44
+ 'subject-case': [0], // Disable case enforcement to allow flexibility
45
+ 'type-case': [2, 'always', 'lower-case'],
46
46
  // Enforce required elements
47
- "type-empty": [2, "never"],
48
- "subject-empty": [2, "never"],
47
+ 'type-empty': [2, 'never'],
48
+ 'subject-empty': [2, 'never'],
49
49
  // Enforce punctuation rules
50
- "subject-full-stop": [2, "never", "."],
50
+ 'subject-full-stop': [2, 'never', '.'],
51
51
  // Enforce scope rules (optional but recommended)
52
- "scope-case": [2, "always", "lower-case"],
52
+ 'scope-case': [2, 'always', 'lower-case'],
53
53
  },
54
- };
54
+ }
@@ -1,24 +1,24 @@
1
1
  {
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
- }
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
- "$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"]
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,3 +1,3 @@
1
1
  {
2
- ".": "0.0.0"
2
+ ".": "0.0.0"
3
3
  }
@@ -1,9 +1,9 @@
1
1
  {
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
- }
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 { input = 'src/index.ts', outDir = 'dist', sourcemap = true, external = isBareImport } = options
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,