@rtorcato/repo-tooling 4.2.0 → 4.3.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
@@ -8,7 +8,7 @@ A one-package JavaScript / TypeScript tooling distribution. Ships every preset (
8
8
 
9
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
- The **ai-issue-loop** pipeline (the `loop` commands, its skills, and the label / agent-user / skills checks) lives in [`@rtorcato/repo-ai`](https://github.com/rtorcato/repo-ai) since #658. `repo-tooling loop` now only prints a pointer there. Its settings still sit in this package's `.repo-tooling.json` under `rules.aiLoop` and `rules.requiredSkills`, which repo-tooling carries forward without reading.
11
+ repo-tooling stands alone. The **ai-issue-loop** pipeline (the `loop` commands, its skills, and the label / agent-user / skills checks) is an optional companion package, [`@rtorcato/repo-ai`](https://rtorcato.github.io/repo-ai/), since #658. Nothing here requires it or checks for it; `repo-tooling loop` only prints a pointer there. Its settings sit in this package's `.repo-tooling.json` under `rules.aiLoop` and `rules.requiredSkills`, which repo-tooling carries forward without reading. See `apps/docs/docs/guides/ai-issue-loop.md` for how the two fit together.
12
12
 
13
13
  ## CLI surface (agent-friendly)
14
14
 
package/README.md CHANGED
@@ -137,6 +137,7 @@ full field reference. The keys `doctor` acts on:
137
137
  - `securityAutomation` — boolean (Dependabot + CodeQL)
138
138
  - `bundler` — `tsup` \| `esbuild` \| `vite` \| `none`
139
139
  - `aiSetup` — boolean (AGENTS.md, CLAUDE.md, Cursor/Copilot rules)
140
+ - `brand` — boolean (`brand/` banner + social-card SVG sources and `render.sh`)
140
141
 
141
142
  **How opt-outs actually work.** When the lockfile records that you declined an
142
143
  *optional* tool (e.g. `securityAutomation: false`), `doctor` demotes that check
@@ -190,19 +191,22 @@ throwaway fixtures) — in any session:
190
191
  /plugin install repo-tooling@repo-tooling
191
192
  ```
192
193
 
193
- ### The AI issue loop
194
+ ### Optional: agents on your issue queue (repo-ai)
194
195
 
195
- The label-driven `ai-ready` issue → PR pipeline (the `ai-workflow`,
196
- `ai-issue-loop`, `ai-issue` and `ai-loop-status` skills, and the `loop`
197
- commands they call) moved to its own package,
198
- [`@rtorcato/repo-ai`](https://github.com/rtorcato/repo-ai), so this one can be
199
- used without it:
196
+ repo-tooling works entirely on its own. If you also want agents working your
197
+ GitHub issues, the optional companion
198
+ [`@rtorcato/repo-ai`](https://rtorcato.github.io/repo-ai/) runs a label-driven
199
+ loop: an `ai-ready` issue becomes a worktree, a PR and two agent reviews, then
200
+ waits for you to merge. It builds on the repo standard repo-tooling sets up
201
+ (branch protection, the release gate, worktree config) and keeps its settings in
202
+ the same `.repo-tooling.json`:
200
203
 
201
204
  ```bash
202
- npx @rtorcato/repo-ai fix claude-skills # → ~/.claude/skills/{ai-issue-loop,ai-workflow,ai-issue,ai-loop-status}/SKILL.md
205
+ npx @rtorcato/repo-ai setup
203
206
  ```
204
207
 
205
- Its settings stay in `.repo-tooling.json` (`rules.aiLoop`, `rules.requiredSkills`).
208
+ See [Using with repo-ai](https://rtorcato.github.io/repo-tooling/guides/ai-issue-loop/)
209
+ for how the two fit together. The loop shipped inside repo-tooling until 4.0.0.
206
210
 
207
211
  ### Use with other AI tools (Cursor / Copilot / Codex)
208
212
 
@@ -266,6 +266,11 @@ export async function checkDependabot(dir) {
266
266
  if (!automerge.includes("package-ecosystem == 'github-actions'")) {
267
267
  deltas.push('auto-merge workflow leaves CI action bumps to a human (#452)');
268
268
  }
269
+ // #694: a declined PR used to get only a log notice — no assignee, no
270
+ // comment — and could sit green and ownerless indefinitely.
271
+ if (!automerge.includes("steps.merge.outcome == 'skipped'")) {
272
+ deltas.push('auto-merge workflow leaves declined PRs with no owner (#694)');
273
+ }
269
274
  }
270
275
  else {
271
276
  deltas.push('missing dependabot-automerge workflow');
@@ -217,7 +217,7 @@ async function checkBranchProtection(exec, nwo, branch) {
217
217
  * enabled only makes them available on the merge button, and one mis-click puts
218
218
  * every intermediate branch commit on the default branch: semantic-release then
219
219
  * reads subjects nobody reviewed (a stray `fix:` inside a docs PR cuts a
220
- * release), and `ai-issue-loop`'s Pass 2 stops finding the `(#N)` squash subject
220
+ * release), and repo-ai's `ai-loop` cleanup stops finding the `(#N)` squash subject
221
221
  * it uses to confirm work landed, so worktrees leak. Observed on `js-common`
222
222
  * #204, whose CONTRIBUTING already said squash-only — the rule existed, nothing
223
223
  * enforced it.
@@ -706,8 +706,8 @@ const PROTECTION_BODY = JSON.stringify({
706
706
  // any required_pull_request_reviews as drift, so `fix github-settings` PUTs it
707
707
  // back to null. A repo that deliberately requires approvals will have that
708
708
  // silently reverted by the next unrelated fix run, with nothing in the output
709
- // naming the rule that was removed. Known downstream case: the ai-issue-loop
710
- // pipeline works around it with approval labels precisely because of this.
709
+ // naming the rule that was removed. Known downstream case: @rtorcato/repo-ai's
710
+ // ai-loop pipeline works around it with approval labels precisely because of this.
711
711
  // Loosen the standard here first if a repo ever genuinely needs review gating.
712
712
  required_pull_request_reviews: null,
713
713
  restrictions: null,
@@ -173,6 +173,8 @@ export function declinedInLock(lock, checkName) {
173
173
  return c.turborepo === false;
174
174
  case 'Tailwind':
175
175
  return c.tailwind === false;
176
+ case 'Brand assets':
177
+ return c.brand === false;
176
178
  default:
177
179
  return false;
178
180
  }
@@ -246,6 +248,8 @@ export function lockfilePatchForTarget(target, lock) {
246
248
  return c.docsSite ? null : { docsSite: true };
247
249
  case 'bun':
248
250
  return c.bun ? null : { bun: true };
251
+ case 'brand':
252
+ return c.brand ? null : { brand: true };
249
253
  default:
250
254
  return null;
251
255
  }
@@ -59,6 +59,7 @@ export function buildPresetConfig(name, projectName) {
59
59
  semanticRelease: true,
60
60
  bundler: 'tsup',
61
61
  publint: true,
62
+ brand: true,
62
63
  };
63
64
  case 'web-app':
64
65
  return {
@@ -210,6 +211,7 @@ export const CONFIG_SCHEMA = {
210
211
  nx: { type: 'boolean' },
211
212
  tailwind: { type: 'boolean' },
212
213
  docsSite: { type: 'boolean' },
214
+ brand: { type: 'boolean' },
213
215
  bun: { type: 'boolean' },
214
216
  },
215
217
  };
@@ -315,6 +317,8 @@ export function computeFileList(config) {
315
317
  files.push('postcss.config.mjs', 'src/styles/globals.css');
316
318
  if (config.bun)
317
319
  files.push('bunfig.toml');
320
+ if (config.brand)
321
+ files.push('brand/banner.svg', 'brand/banner-mobile.svg', 'brand/social-card.svg', 'brand/render.sh');
318
322
  files.push('README.md');
319
323
  return files;
320
324
  }
@@ -22,6 +22,7 @@ const REVIEWABLE_FEATURES = [
22
22
  { key: 'securityAutomation', label: 'Security automation (Dependabot + CodeQL)' },
23
23
  { key: 'badges', label: 'README status badges' },
24
24
  { key: 'aiSetup', label: 'AI agent rules (AGENTS.md, CLAUDE.md, Cursor, Copilot)' },
25
+ { key: 'brand', label: 'Brand assets (brand/ banner + social-card SVGs, render.sh)' },
25
26
  ];
26
27
  /**
27
28
  * A preset run may only ask questions when a human is on both ends of the pipe.
@@ -457,6 +458,13 @@ async function promptForConfig(targetDir, seed) {
457
458
  message: '🤖 Add AI agent rules (AGENTS.md, CLAUDE.md, Cursor, Copilot, Claude skill)?',
458
459
  default: seed?.aiSetup ?? true,
459
460
  },
461
+ {
462
+ type: 'confirm',
463
+ name: 'brand',
464
+ message: '🎨 Add brand assets (brand/ banner + social-card SVG sources, render.sh)?',
465
+ // On by default for libraries — the ones that publish a README to npm.
466
+ default: (answers) => seed?.brand ?? answers.projectType === 'library',
467
+ },
460
468
  {
461
469
  type: 'select',
462
470
  name: 'orchestrator',
@@ -546,6 +554,7 @@ async function promptForConfig(targetDir, seed) {
546
554
  publint: answers.publint ?? false,
547
555
  badges: answers.badges ?? false,
548
556
  aiSetup: answers.aiSetup ?? false,
557
+ brand: answers.brand ?? false,
549
558
  turborepo: answers.orchestrator === 'turbo',
550
559
  nx: answers.orchestrator === 'nx',
551
560
  tailwind: answers.tailwind ?? false,
@@ -29,6 +29,9 @@ const SELF_RANGE = `^${selfPackageJson.version}`;
29
29
  const DOCS_APP = 'apps/docs';
30
30
  /** Docusaurus's neutral green — the default accent, meant to be branded over. */
31
31
  const DEFAULT_ACCENT = { light: '#2e8555', dark: '#25c2a0' };
32
+ // One range for every @docusaurus/* package; TypeScript tracks repo-tooling's own devDependency (tested).
33
+ export const DOCUSAURUS_RANGE = '^3.10.2';
34
+ export const TYPESCRIPT_RANGE = '~7.0.2';
32
35
  /**
33
36
  * Source modules to document with TypeDoc: single-segment subpath exports
34
37
  * (`./errors` → `errors`), which map to `src/<id>/index.ts`. Multi-segment
@@ -288,8 +291,8 @@ function docsPackageJson(meta, typedoc) {
288
291
  typecheck: 'tsc --noEmit',
289
292
  },
290
293
  dependencies: {
291
- '@docusaurus/core': '^3.10.2',
292
- '@docusaurus/preset-classic': '^3.8.1',
294
+ '@docusaurus/core': DOCUSAURUS_RANGE,
295
+ '@docusaurus/preset-classic': DOCUSAURUS_RANGE,
293
296
  '@easyops-cn/docusaurus-search-local': '^0.55.2',
294
297
  '@mdx-js/react': '^3.1.0',
295
298
  clsx: '^2.1.1',
@@ -298,12 +301,12 @@ function docsPackageJson(meta, typedoc) {
298
301
  'react-dom': '^19.0.0',
299
302
  },
300
303
  devDependencies: {
301
- '@docusaurus/module-type-aliases': '^3.10.2',
302
- '@docusaurus/tsconfig': '^3.8.1',
303
- '@docusaurus/types': '^3.10.2',
304
+ '@docusaurus/module-type-aliases': DOCUSAURUS_RANGE,
305
+ '@docusaurus/tsconfig': DOCUSAURUS_RANGE,
306
+ '@docusaurus/types': DOCUSAURUS_RANGE,
304
307
  '@rtorcato/repo-tooling': SELF_RANGE,
305
308
  '@types/react': '^19.0.0',
306
- typescript: '~5.6.3',
309
+ typescript: TYPESCRIPT_RANGE,
307
310
  ...typedocDevDeps,
308
311
  },
309
312
  browserslist: {
@@ -19,6 +19,7 @@ import { generateTailwind } from './tailwind.js';
19
19
  import { generateTurborepo } from './turborepo.js';
20
20
  import { generateNx } from './nx.js';
21
21
  import { generateBun } from './bun.js';
22
+ import { generateBrand } from './brand.js';
22
23
  const __filename = fileURLToPath(import.meta.url);
23
24
  const __dirname = path.dirname(__filename);
24
25
  export async function generateConfigs(config, targetDir) {
@@ -122,6 +123,11 @@ export async function generateConfigs(config, targetDir) {
122
123
  }
123
124
  // Generate README
124
125
  await generateReadme(config, targetDir);
126
+ // brand/ (#677) — the same generator as `fix brand`. After the README so the
127
+ // banner-path repoint sees it; no tagline yet, as the lockfile isn't written.
128
+ if (config.brand) {
129
+ await generateBrand(await fs.readJson(path.join(targetDir, 'package.json')), targetDir);
130
+ }
125
131
  // Copy ts-reset if TypeScript is enabled
126
132
  if (config.typescript.enabled) {
127
133
  await copyTSReset(targetDir);
@@ -163,6 +163,8 @@ export function dependabotIgnoreRules(content) {
163
163
  * Everything else falls through to a human: production bumps ship to consumers
164
164
  * of a published package (#423), majors are breaking by definition, and an
165
165
  * ungrouped npm PR reports an empty \`dependency-group\`, so the gate fails closed.
166
+ * A PR that falls through is assigned to the repo owner with one upserted comment
167
+ * saying why (#694), so it never sits green and ownerless.
166
168
  *
167
169
  * CI action bumps are the one ungrouped case that is allowed through, matched on
168
170
  * \`package-ecosystem\` (#452): they reach no consumer of the published package
@@ -226,11 +228,13 @@ jobs:
226
228
  # No names reported means we cannot verify anything — fail closed.
227
229
  if [ -z "\${NAMES//[, ]/}" ]; then
228
230
  echo "::notice::no dependency names reported — leaving this PR for a human"
231
+ echo "reason=Dependabot reported no dependency names, so nothing could be verified" >> "$GITHUB_OUTPUT"
229
232
  safe=false
230
233
  fi
231
234
  for name in \${NAMES//,/ }; do
232
235
  if printf '%s\\n' "$ships" | grep -qxF -- "$name"; then
233
236
  echo "::notice::$name ships to consumers — leaving this PR for a human"
237
+ echo "reason=$name ships to consumers of this package" >> "$GITHUB_OUTPUT"
234
238
  safe=false
235
239
  break
236
240
  fi
@@ -247,6 +251,7 @@ jobs:
247
251
  # "ci(deps): …", which cuts no release. Majors still fall through to a
248
252
  # human — the update-type clause below applies to them too.
249
253
  - name: Auto-merge dev-dependency and CI action patch and minor updates
254
+ id: merge
250
255
  if: |
251
256
  steps.gate.outputs.safe == 'true' &&
252
257
  (steps.metadata.outputs.dependency-group == 'dev-minor' ||
@@ -257,6 +262,46 @@ jobs:
257
262
  env:
258
263
  PR_URL: \${{ github.event.pull_request.html_url }}
259
264
  GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
265
+
266
+ # Everything the step above declines needs a human, and a notice in the
267
+ # Actions log is not one (#694). Assign the repo owner and say why in a
268
+ # single comment, edited in place on every later run rather than repeated.
269
+ - name: Hand the PR to a human
270
+ if: steps.merge.outcome == 'skipped'
271
+ env:
272
+ PR_URL: \${{ github.event.pull_request.html_url }}
273
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
274
+ REPO: \${{ github.repository }}
275
+ PR_NUMBER: \${{ github.event.pull_request.number }}
276
+ OWNER: \${{ github.repository_owner }}
277
+ REASON: \${{ steps.gate.outputs.reason }}
278
+ UPDATE_TYPE: \${{ steps.metadata.outputs.update-type }}
279
+ run: |
280
+ set -euo pipefail
281
+
282
+ if [ -z "$REASON" ]; then
283
+ case "$UPDATE_TYPE" in
284
+ version-update:semver-major) REASON="this is a major version bump" ;;
285
+ version-update:semver-minor | version-update:semver-patch)
286
+ REASON="this is neither a dev-minor group bump nor a CI action bump, so it may ship to consumers" ;;
287
+ *) REASON="Dependabot reported no update type, so nothing could be verified" ;;
288
+ esac
289
+ fi
290
+
291
+ marker='<!-- dependabot-automerge:manual-review -->'
292
+ body="$marker
293
+ Not auto-merged: $REASON. Leaving this for @$OWNER to review and merge by hand."
294
+
295
+ ids=$(gh api --paginate "repos/$REPO/issues/$PR_NUMBER/comments" \\
296
+ --jq ".[] | select(.body | startswith(\\"$marker\\")) | .id")
297
+ if [ -n "$ids" ]; then
298
+ gh api -X PATCH "repos/$REPO/issues/comments/\${ids%%$'\\n'*}" -f body="$body" > /dev/null
299
+ else
300
+ gh pr comment "$PR_URL" --body "$body"
301
+ fi
302
+
303
+ # An organisation login cannot be assigned; the comment still stands.
304
+ gh pr edit "$PR_URL" --add-assignee "$OWNER" || echo "::warning::could not assign $OWNER"
260
305
  `;
261
306
  /** Relative paths this generator owns, in a stable order for \`filesWritten\`. */
262
307
  export const DEPENDABOT_FILES = [
@@ -36,8 +36,6 @@ export const MCP_IMPORTANCE = ['nice-to-have', 'important', 'critical'];
36
36
  * be this tool asserting a rule on a repo whose humans have not stated any.
37
37
  */
38
38
  export const DEFAULT_RULES = {
39
- aiLoop: {},
40
- requiredSkills: [],
41
39
  mcp: { recommended: [] },
42
40
  exceptions: {},
43
41
  };
@@ -111,7 +109,8 @@ export function lockfileSchema() {
111
109
  aiLoop: {
112
110
  type: 'object',
113
111
  additionalProperties: false,
114
- description: 'Settings for the ai-issue-loop skills from @rtorcato/repo-ai. Repo-scoped on purpose: committed here they travel with the repo and survive a new laptop.',
112
+ deprecated: true,
113
+ description: 'Deprecated: moved to `.repo-ai.json`, owned by @rtorcato/repo-ai. Still accepted so existing files validate; removed in the next major. `npx @rtorcato/repo-ai fix config` moves it across.',
115
114
  properties: {
116
115
  agentUser: {
117
116
  type: 'string',
@@ -122,7 +121,8 @@ export function lockfileSchema() {
122
121
  requiredSkills: {
123
122
  type: 'array',
124
123
  items: { type: 'string' },
125
- description: "Agent skills this repo's workflows depend on. Audited by `npx @rtorcato/repo-ai doctor`, which reports a missing or stale installed copy; it never installs one for you.",
124
+ deprecated: true,
125
+ description: 'Deprecated: moved to `.repo-ai.json`, owned by @rtorcato/repo-ai. Still accepted so existing files validate; removed in the next major. `npx @rtorcato/repo-ai fix config` moves it across.',
126
126
  },
127
127
  mcp: {
128
128
  type: 'object',
@@ -1188,8 +1188,8 @@ export async function checkTreeshakeSetup(dir, pkg) {
1188
1188
  * checkout (#396). JS-only: the other language modules have nothing to symlink.
1189
1189
  *
1190
1190
  * Two consumers, one list (#527). Claude Code honours the setting only for
1191
- * worktrees it creates itself (`EnterWorktree`); the shipped `ai-issue-loop`
1192
- * skill creates its own with `git worktree add`, so it reads this same list and
1191
+ * worktrees it creates itself (`EnterWorktree`); @rtorcato/repo-ai's `ai-loop`
1192
+ * creates its own with `git worktree add`, so it reads this same list and
1193
1193
  * makes the symlinks itself. That is why the check is worth passing on a repo
1194
1194
  * running the loop, where the setting alone would govern nothing.
1195
1195
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "4.2.0",
3
+ "version": "4.3.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": [
@@ -248,6 +248,7 @@
248
248
  "@next/eslint-plugin-next": "^16.3.2",
249
249
  "@playwright/test": "^1.62.1",
250
250
  "@rollup/plugin-typescript": "^12.3.0",
251
+ "@rtorcato/repo-ai": "^3.1.1",
251
252
  "@semantic-release/commit-analyzer": "^13.0.1",
252
253
  "@semantic-release/exec": "^7.1.0",
253
254
  "@semantic-release/github": "^12.0.9",