@rtorcato/repo-tooling 2.59.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.
Files changed (136) hide show
  1. package/AGENTS.md +113 -0
  2. package/LICENSE +21 -0
  3. package/README.md +201 -0
  4. package/dist/cli/commands/doctor.js +336 -0
  5. package/dist/cli/commands/fix-targets.js +192 -0
  6. package/dist/cli/commands/fix.js +440 -0
  7. package/dist/cli/commands/setup-presets.js +281 -0
  8. package/dist/cli/commands/setup.js +501 -0
  9. package/dist/cli/generators/agent-rules.js +103 -0
  10. package/dist/cli/generators/badges.js +88 -0
  11. package/dist/cli/generators/build.js +216 -0
  12. package/dist/cli/generators/bun.js +25 -0
  13. package/dist/cli/generators/community-health.js +145 -0
  14. package/dist/cli/generators/docs-site.js +436 -0
  15. package/dist/cli/generators/git.js +164 -0
  16. package/dist/cli/generators/github-actions.js +35 -0
  17. package/dist/cli/generators/github-workflows.js +24 -0
  18. package/dist/cli/generators/gitlab-ci.js +10 -0
  19. package/dist/cli/generators/index.js +123 -0
  20. package/dist/cli/generators/linting.js +54 -0
  21. package/dist/cli/generators/misc.js +298 -0
  22. package/dist/cli/generators/nx.js +16 -0
  23. package/dist/cli/generators/package-json.js +317 -0
  24. package/dist/cli/generators/pnpm-workspace.js +126 -0
  25. package/dist/cli/generators/postcss.js +24 -0
  26. package/dist/cli/generators/readme.js +268 -0
  27. package/dist/cli/generators/security.js +156 -0
  28. package/dist/cli/generators/skills-install.js +70 -0
  29. package/dist/cli/generators/tailwind.js +34 -0
  30. package/dist/cli/generators/testing.js +88 -0
  31. package/dist/cli/generators/treeshake.js +148 -0
  32. package/dist/cli/generators/tsconfig.js +27 -0
  33. package/dist/cli/generators/turborepo.js +35 -0
  34. package/dist/cli/generators/typedoc.js +40 -0
  35. package/dist/cli/index.js +332 -0
  36. package/dist/cli/utils/copy-preset.js +98 -0
  37. package/dist/cli/utils/detect-language.js +22 -0
  38. package/dist/cli/utils/format.js +32 -0
  39. package/dist/cli/utils/install.js +28 -0
  40. package/dist/cli/utils/lockfile.js +84 -0
  41. package/dist/languages/js/checks.js +949 -0
  42. package/dist/languages/js/ci.js +251 -0
  43. package/dist/languages/js/fixers.js +735 -0
  44. package/dist/languages/registry.js +37 -0
  45. package/dist/languages/swift/checks.js +127 -0
  46. package/dist/languages/swift/ci.js +165 -0
  47. package/dist/languages/swift/fixers.js +102 -0
  48. package/dist/languages/swift/git-hooks.js +63 -0
  49. package/dist/languages/swift/gitignore.js +43 -0
  50. package/dist/languages/swift/scaffold.js +244 -0
  51. package/package.json +461 -0
  52. package/tooling/biome/README.md +90 -0
  53. package/tooling/biome/biome.json +63 -0
  54. package/tooling/bun/bunfig.toml +14 -0
  55. package/tooling/changesets/README.md +35 -0
  56. package/tooling/changesets/config.json +11 -0
  57. package/tooling/claude/repo-tooling.md +87 -0
  58. package/tooling/commitlint/commitlint.d.mts +4 -0
  59. package/tooling/commitlint/commitlint.mjs +40 -0
  60. package/tooling/cypress/cypress.config.d.mts +4 -0
  61. package/tooling/cypress/cypress.config.mjs +11 -0
  62. package/tooling/docusaurus/index.d.mts +17 -0
  63. package/tooling/docusaurus/index.mjs +38 -0
  64. package/tooling/docusaurus/sync-changelog.mjs +42 -0
  65. package/tooling/docusaurus/theme-tokens.css +79 -0
  66. package/tooling/docusaurus/theme.css +378 -0
  67. package/tooling/esbuild/index.d.mts +6 -0
  68. package/tooling/esbuild/index.mjs +102 -0
  69. package/tooling/eslint/base.d.mts +6 -0
  70. package/tooling/eslint/base.mjs +122 -0
  71. package/tooling/eslint/nextjs.d.mts +4 -0
  72. package/tooling/eslint/nextjs.mjs +22 -0
  73. package/tooling/eslint/types.d.ts +58 -0
  74. package/tooling/github-actions/workflows/cloudflare-pages.yml +42 -0
  75. package/tooling/github-actions/workflows/docker-publish.yml +44 -0
  76. package/tooling/github-actions/workflows/preview-deployments.yml +54 -0
  77. package/tooling/github-actions/workflows/vercel-deploy.yml +40 -0
  78. package/tooling/jest-presets/browser/jest-preset.d.mts +4 -0
  79. package/tooling/jest-presets/browser/jest-preset.mjs +14 -0
  80. package/tooling/jest-presets/node/jest-preset.d.mts +4 -0
  81. package/tooling/jest-presets/node/jest-preset.mjs +13 -0
  82. package/tooling/mcp/mcp.json.example +27 -0
  83. package/tooling/nx/nx.json +24 -0
  84. package/tooling/oxlint/README.md +25 -0
  85. package/tooling/oxlint/oxlintrc.json +28 -0
  86. package/tooling/playwright/playwright.config.d.mts +4 -0
  87. package/tooling/playwright/playwright.config.mjs +19 -0
  88. package/tooling/prettier/index.d.mts +4 -0
  89. package/tooling/prettier/index.mjs +36 -0
  90. package/tooling/release-please/.release-please-manifest.json +3 -0
  91. package/tooling/release-please/release-please-config.json +9 -0
  92. package/tooling/rolldown/rolldown.config.d.mts +18 -0
  93. package/tooling/rolldown/rolldown.config.mjs +59 -0
  94. package/tooling/rollup/rollup.config.d.mts +20 -0
  95. package/tooling/rollup/rollup.config.mjs +70 -0
  96. package/tooling/semantic-release/docker.d.mts +4 -0
  97. package/tooling/semantic-release/docker.mjs +59 -0
  98. package/tooling/semantic-release/github.d.mts +4 -0
  99. package/tooling/semantic-release/github.mjs +79 -0
  100. package/tooling/semantic-release/index.d.mts +4 -0
  101. package/tooling/semantic-release/index.mjs +80 -0
  102. package/tooling/swift/periphery.yml +4 -0
  103. package/tooling/swift/swiftlint.yml +23 -0
  104. package/tooling/tests/exports-resolution.d.mts +14 -0
  105. package/tooling/tests/exports-resolution.mjs +68 -0
  106. package/tooling/tests/ssr-safety.d.mts +14 -0
  107. package/tooling/tests/ssr-safety.mjs +53 -0
  108. package/tooling/tsup/index.d.mts +8 -0
  109. package/tooling/tsup/index.mjs +33 -0
  110. package/tooling/typedoc/typedoc.json +7 -0
  111. package/tooling/typescript/README.md +49 -0
  112. package/tooling/typescript/reset.d.ts +9 -0
  113. package/tooling/typescript/tsconfig.base.json +84 -0
  114. package/tooling/typescript/tsconfig.build.json +11 -0
  115. package/tooling/typescript/tsconfig.bun.json +9 -0
  116. package/tooling/typescript/tsconfig.express.json +9 -0
  117. package/tooling/typescript/tsconfig.next.json +20 -0
  118. package/tooling/typescript/tsconfig.node.json +9 -0
  119. package/tooling/typescript/tsconfig.react.json +15 -0
  120. package/tooling/typescript/tsconfig.test.json +8 -0
  121. package/tooling/typescript/v1/tsconfig.base.json +81 -0
  122. package/tooling/typescript/v1/tsconfig.express.json +9 -0
  123. package/tooling/typescript/v1/tsconfig.next.json +19 -0
  124. package/tooling/typescript/v1/tsconfig.node.json +9 -0
  125. package/tooling/typescript/v1/tsconfig.react.json +14 -0
  126. package/tooling/typescript/v1/tsconfig.test.json +8 -0
  127. package/tooling/vite/vite.config.d.mts +4 -0
  128. package/tooling/vite/vite.config.mjs +18 -0
  129. package/tooling/vitest/jsdom-shims.d.mts +1 -0
  130. package/tooling/vitest/jsdom-shims.mjs +58 -0
  131. package/tooling/vitest/vitest.config.d.mts +4 -0
  132. package/tooling/vitest/vitest.config.mjs +25 -0
  133. package/tooling/vitest/vitest.config.react.d.mts +4 -0
  134. package/tooling/vitest/vitest.config.react.mjs +27 -0
  135. package/tooling/vitest/vitest.setup.d.mts +1 -0
  136. package/tooling/vitest/vitest.setup.mjs +3 -0
package/AGENTS.md ADDED
@@ -0,0 +1,113 @@
1
+ # AGENTS.md
2
+
3
+ Orientation for coding agents working with `@rtorcato/repo-tooling`. Human-readable docs live at https://rtorcato.github.io/repo-tooling/.
4
+
5
+ ## What this is
6
+
7
+ A one-package JavaScript / TypeScript tooling distribution. Ships every preset (TypeScript, Biome, ESLint, Prettier, Vitest, Jest, Commitlint, semantic-release, tsup, esbuild, Vite, Playwright) plus a CLI to scaffold and audit projects. Consumers get one install.
8
+
9
+ **Swift** repos (detected via `Package.swift`) are covered end to end: `setup --preset swift-library` scaffolds a SwiftPM package, and `doctor`/`fix` run the language-agnostic checks plus SwiftLint / Periphery / `.gitignore` / `Package.swift`. Python and Perl are audit-only for now. See `src/languages/` — one directory per language module, `src/base/` for what's shared.
10
+
11
+ ## CLI surface (agent-friendly)
12
+
13
+ Every command supports `--json` and a non-interactive mode. Combine with `--yes` for fully autonomous use.
14
+
15
+ | Command | Non-interactive | JSON output | Use case |
16
+ |---|---|---|---|
17
+ | `setup --preset <name>` | ✅ | `--dry-run` only | Scaffold a new project. Presets: `library`, `web-app`, `node-api`, `nextjs-app`, `react-app`, `swift-library`. |
18
+ | `setup --config <path>` | ✅ | `--dry-run` only | Scaffold with a full `ProjectConfig` JSON file. See `setup --config-schema`. |
19
+ | `setup --config-schema` | ✅ | ✅ (JSON Schema) | Print the JSON Schema for `ProjectConfig`. Use to validate configs before scaffolding. |
20
+ | `setup --dry-run` | ✅ | ✅ | Print resolved config + file list without writing. Pair with `--preset` or `--config`. |
21
+ | `doctor --json` | ✅ | ✅ | Audit a project. Returns `{ directory, results: [{ check, status, detail, hint? }] }`. Status: `ok` / `drift` / `missing` / `optional-missing`. |
22
+ | `fix --json --yes` | ✅ | ✅ | Walk every doctor finding, apply fixers. Returns `FixActionRecord[]` with `status: applied | dry-run | skipped | already-ok | unsupported`. |
23
+ | `fix <target> --json --yes` | ✅ | ✅ | Apply one fixer. Targets from `list --json`. |
24
+ | `fix --dry-run` | ✅ | ✅ | Print what each fixer would write without writing. Combine with `--json`. |
25
+ | `list --json` | ✅ | ✅ | Enumerate the library's surface area. Each entry has `{ name, description, exports, fixTarget }`. |
26
+ | `copy <name>` | ✅ | text only | Copy a single preset (`biome`, `tsconfig`) into the current directory. |
27
+
28
+ ## Recommended workflows
29
+
30
+ ### Scaffolding a new project from scratch
31
+
32
+ ```bash
33
+ npx @rtorcato/repo-tooling setup --preset library -d ./my-lib --skip-install
34
+ ```
35
+
36
+ For full control:
37
+
38
+ ```bash
39
+ # 1. Get the schema
40
+ npx @rtorcato/repo-tooling setup --config-schema > project-config.schema.json
41
+
42
+ # 2. Write a config matching it
43
+ cat > project.json <<EOF
44
+ {
45
+ "projectName": "my-lib",
46
+ "projectType": "library",
47
+ "typescript": {"enabled": true, "config": "base"},
48
+ "linting": {"tool": "biome"},
49
+ "formatting": {"tool": "biome"},
50
+ "testing": {"framework": "vitest", "environment": "node"},
51
+ "gitHooks": true,
52
+ "commitLint": true,
53
+ "semanticRelease": true,
54
+ "securityAutomation": true,
55
+ "bundler": "tsup"
56
+ }
57
+ EOF
58
+
59
+ # 3. Preview, then scaffold
60
+ npx @rtorcato/repo-tooling setup --config project.json --dry-run
61
+ npx @rtorcato/repo-tooling setup --config project.json -d ./my-lib --skip-install
62
+ ```
63
+
64
+ ### Auditing an existing project
65
+
66
+ ```bash
67
+ # Get findings
68
+ npx @rtorcato/repo-tooling doctor --json -d ./existing-repo > doctor.json
69
+
70
+ # Apply every fixable finding (no prompts, no surprises)
71
+ npx @rtorcato/repo-tooling fix --yes --json -d ./existing-repo > applied.json
72
+
73
+ # Re-audit to confirm clean
74
+ npx @rtorcato/repo-tooling doctor --json -d ./existing-repo
75
+ ```
76
+
77
+ ### Targeted fixes
78
+
79
+ ```bash
80
+ # Apply one fixer from the list (run `list --json` for every target)
81
+ npx @rtorcato/repo-tooling fix dependabot --yes --json
82
+ npx @rtorcato/repo-tooling fix engines --yes --json
83
+ npx @rtorcato/repo-tooling fix docs-site --yes --json # scaffold a Docusaurus docs site under apps/docs
84
+ npx @rtorcato/repo-tooling fix bun --yes --json # Bun runtime/test config
85
+ ```
86
+
87
+ ## Drift policy (important)
88
+
89
+ `fix` defaults the confirm prompt to **No** for drift cases (existing file that doesn't extend our preset). The `--yes` flag is required to overwrite drift. Safe-merge fixers (`engines`, `husky`, `package-json`) never overwrite — they add/merge — and use friendlier prompt wording. `fix --json` implies `--yes` (prompts would corrupt JSON output).
90
+
91
+ ## Source-of-truth files in the repo
92
+
93
+ - `src/cli/commands/setup.ts` — `ProjectConfig` interface and the setup orchestrator
94
+ - `src/cli/commands/setup-presets.ts` — preset definitions, JSON Schema, config validator, `computeFileList`
95
+ - `src/cli/commands/doctor.ts` — all checks and the public `runDoctor(dir)` / `evaluateNodeVersion(version)` / `nextStepSuggestions(results)`
96
+ - `src/cli/commands/fix.ts` — `Fixer` interface, fixer registry, `fixCommand`
97
+ - `src/cli/commands/fix-targets.ts` — shared check → fix target map (used by both doctor's footer and fix's lookup)
98
+ - `src/cli/generators/` — one file per concern (linting, testing, build, git, github-actions, security, misc)
99
+ - `tooling/` — every shipped preset, mirrored 1:1 with `package.json` `exports`
100
+
101
+ ## Conventions in this repo
102
+
103
+ - Conventional commits enforced via commitlint; header max 72 chars
104
+ - Biome for lint + format (run via `pnpm exec biome check --config-path=tooling/biome/biome.json src scripts`)
105
+ - Tests live alongside source in `tests/`; vitest with no separate config
106
+ - semantic-release runs on push to `main`; `fix:` → patch, `feat:` → minor, `chore:` / `docs:` → no release
107
+
108
+ ## Pointers
109
+
110
+ - Site index for LLMs: https://rtorcato.github.io/repo-tooling/llms.txt
111
+ - Full CLI guide: https://rtorcato.github.io/repo-tooling/guides/cli/
112
+ - For AI agents: https://rtorcato.github.io/repo-tooling/guides/for-ai-agents/
113
+ - Source: https://github.com/rtorcato/repo-tooling
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Richard Torcato
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,201 @@
1
+ <picture>
2
+ <source media="(max-width: 640px)" srcset="./banner-mobile.png">
3
+ <img src="./banner.png" alt="repo-tooling banner" width="1600">
4
+ </picture>
5
+
6
+ <br>
7
+
8
+ [![CI](https://github.com/rtorcato/repo-tooling/actions/workflows/ci.yml/badge.svg)](https://github.com/rtorcato/repo-tooling/actions/workflows/ci.yml)
9
+ [![npm version](https://badge.fury.io/js/@rtorcato%2Frepo-tooling.svg)](https://badge.fury.io/js/@rtorcato%2Frepo-tooling)
10
+ [![npm downloads](https://img.shields.io/npm/dm/@rtorcato%2Frepo-tooling)](https://www.npmjs.com/package/@rtorcato/repo-tooling)
11
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/@rtorcato/repo-tooling)](https://bundlephobia.com/package/@rtorcato/repo-tooling)
12
+ [![Coverage](https://codecov.io/gh/rtorcato/repo-tooling/branch/main/graph/badge.svg)](https://codecov.io/gh/rtorcato/repo-tooling)
13
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
14
+
15
+
16
+ One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.
17
+
18
+
19
+ Most tooling libraries give you one piece — just TypeScript configs, or just an ESLint preset. **repo-tooling** covers the entire lifecycle: TypeScript, Biome/ESLint, Vitest/Jest, Commitlint, Husky, Semantic Release, GitHub Actions CI, and supply-chain security (Dependabot + CodeQL) — all wired together. The interactive `setup` wizard scaffolds everything in one shot; `doctor` checks an existing project for drift; `fix` applies the missing pieces incrementally.
20
+
21
+ **[Full documentation →](https://rtorcato.github.io/repo-tooling/)**
22
+
23
+ > **Package manager: pnpm, by design.** Every generator scaffolds pnpm
24
+ > workflows, workspace files, and scripts. repo-tooling targets pnpm only for
25
+ > now — npm, yarn, and Bun aren't generated.
26
+
27
+ ## Start a new project
28
+
29
+ Interactive wizard — answers every prompt, scaffolds the whole project:
30
+
31
+ ```bash
32
+ npx @rtorcato/repo-tooling setup
33
+ ```
34
+
35
+ Non-interactive — scaffold from a named preset in one shot (CI-friendly):
36
+
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
40
+ ```
41
+
42
+ `swift-library` scaffolds a SwiftPM package (manifest, sources, tests, SwiftLint,
43
+ Periphery, macOS CI) instead of an npm one — see the
44
+ [Swift guide](https://rtorcato.github.io/repo-tooling/guides/swift).
45
+
46
+ Just one config file? Use `copy`:
47
+
48
+ ```bash
49
+ npx @rtorcato/repo-tooling copy biome # → biome.json
50
+ npx @rtorcato/repo-tooling copy tsconfig # → tsconfig.json
51
+ npx @rtorcato/repo-tooling copy changesets # → .changeset/config.json
52
+ npx @rtorcato/repo-tooling copy oxlint # → .oxlintrc.json
53
+ npx @rtorcato/repo-tooling copy claude-skill # → .claude/skills/repo-tooling.md
54
+ ```
55
+
56
+ **Already have a project?** Don't rerun `setup` — use `doctor` + `fix`:
57
+
58
+ ```bash
59
+ npx @rtorcato/repo-tooling doctor # find what's missing or drifted
60
+ npx @rtorcato/repo-tooling fix # apply scaffolders, prompting per item
61
+ ```
62
+
63
+ See the [Getting Started guide](https://rtorcato.github.io/repo-tooling/guides/getting-started/) for the full walkthrough.
64
+
65
+ ## Commands
66
+
67
+ | Command | What it does | Example |
68
+ | --- | --- | --- |
69
+ | `setup` | Interactive wizard that scaffolds a whole new project (add `--preset` to run non-interactively). | `npx @rtorcato/repo-tooling setup` |
70
+ | `list` | List every tooling configuration this package can scaffold (`--json` for machine output). | `npx @rtorcato/repo-tooling list` |
71
+ | `copy <config>` | Copy a single config file into the current project. | `npx @rtorcato/repo-tooling copy biome` |
72
+ | `doctor` | Diagnose an existing project for missing or drifted tooling. | `npx @rtorcato/repo-tooling doctor` |
73
+ | `fix [target]` | Apply scaffolders for what `doctor` flagged (`--yes`, `--dry-run`, `--diff`). | `npx @rtorcato/repo-tooling fix` |
74
+
75
+ Every command takes `-d, --directory <path>`; run any with `--help` for its full flags. Run `list` (or `list --json`) for the full set of `fix` targets — it's the source of truth. Notable ones include `fix docs-site` (scaffold a [Docusaurus docs site](https://rtorcato.github.io/repo-tooling/guides/docs-site/)) and `fix bun` (Bun runtime config).
76
+
77
+ ## The `.repo-tooling.json` lockfile
78
+
79
+ An **optional** manifest that records the tooling choices you adopted. Nothing
80
+ reads it at build/lint/test time — it exists only so `doctor` can tell an
81
+ *intentional opt-out* from *drift*. repo-tooling works fine without it, which is
82
+ why `doctor` reports a missing one as `not configured`, not an error.
83
+
84
+ Generate or refresh it from what's currently on disk:
85
+
86
+ ```bash
87
+ npx @rtorcato/repo-tooling fix lockfile
88
+ ```
89
+
90
+ **It's the only config file you need.** Don't keep a separate hand-authored
91
+ `.repo-tooling.config.json` next to it — the lockfile already embeds the full
92
+ `ProjectConfig` under `config`, and `setup --config` accepts the lockfile
93
+ directly, so you can re-run a non-interactive setup from it:
94
+
95
+ ```bash
96
+ npx @rtorcato/repo-tooling setup --config .repo-tooling.json
97
+ ```
98
+
99
+ Each `config.*` key mirrors a setup answer — see the
100
+ [schema](https://rtorcato.github.io/repo-tooling/schemas/lockfile.json) for the
101
+ full field reference. The keys `doctor` acts on:
102
+
103
+ - `typescript.enabled` / `typescript.config` — `base` \| `react` \| `next` \| `node` \| `express`
104
+ - `linting.tool` — `biome` \| `eslint` \| `both` \| `none`
105
+ - `formatting.tool` — `biome` \| `prettier` \| `none`
106
+ - `testing.framework` — `vitest` \| `jest` \| `playwright` \| `none`
107
+ - `gitHooks`, `commitLint`, `semanticRelease` — booleans
108
+ - `securityAutomation` — boolean (Dependabot + CodeQL)
109
+ - `bundler` — `tsup` \| `esbuild` \| `vite` \| `none`
110
+ - `aiSetup` — boolean (AGENTS.md, CLAUDE.md, Cursor/Copilot rules)
111
+
112
+ **How opt-outs actually work.** When the lockfile records that you declined an
113
+ *optional* tool (e.g. `securityAutomation: false`), `doctor` demotes that check
114
+ from `not configured` to `ok — intentionally declined` instead of nagging you to
115
+ add it. This applies only to optional checks — it does **not** silence *drift*:
116
+ if a config you adopted diverges from the shared base (say your `tsconfig.json`
117
+ extends a different preset), `doctor` still flags it, because drift is a mismatch
118
+ in a tool you're using, not an opt-out. There's no field to suppress drift today.
119
+
120
+ **Adoption vs. standalone.** `fix lockfile` infers adoption from what's on disk,
121
+ so on a repo that deliberately runs standalone configs (no
122
+ `@rtorcato/repo-tooling` dependency) it can record `typescript.config: "base"` and
123
+ friends — asserting you extend the shared bases when you don't. If you're not
124
+ adopting repo-tooling's configs, skip the lockfile; it won't stop the `extends`
125
+ drift warnings.
126
+
127
+ ## AI agent rules
128
+
129
+ The package ships rules that teach AI coding agents to drive the CLI
130
+ (`doctor` / `fix` / `setup`) non-interactively. Install for your agent — all
131
+ generated from one source, so guidance never drifts between them:
132
+
133
+ ```bash
134
+ npx @rtorcato/repo-tooling fix claude-skill --yes # → .claude/skills/repo-tooling.md
135
+ npx @rtorcato/repo-tooling fix cursor-rules --yes # → .cursor/rules/repo-tooling.mdc
136
+ npx @rtorcato/repo-tooling fix copilot-instructions --yes # → .github/copilot-instructions.md
137
+ npx @rtorcato/repo-tooling fix agents-md --yes # → AGENTS.md
138
+ ```
139
+
140
+ `copilot-instructions` and `agents-md` upsert a delimited block, so your own
141
+ content in those shared files is never clobbered. Re-running updates the block
142
+ in place on upgrade.
143
+
144
+ Prefer a symlink that auto-syncs the Claude skill on every upgrade?
145
+
146
+ ```bash
147
+ mkdir -p .claude/skills
148
+ ln -sf ../../node_modules/@rtorcato/repo-tooling/tooling/claude/repo-tooling.md \
149
+ .claude/skills/repo-tooling.md
150
+ ```
151
+
152
+ ### Use with Claude Code (plugin)
153
+
154
+ This repo is also a self-hosted Claude Code marketplace. Install the plugin to
155
+ get two skills — `repo-tooling` (adopt/audit the presets via the CLI) and
156
+ `npm-publish` (the family's release rules) — in any session:
157
+
158
+ ```
159
+ /plugin marketplace add rtorcato/repo-tooling
160
+ /plugin install repo-tooling@repo-tooling
161
+ ```
162
+
163
+ ### Use with other AI tools (Cursor / Copilot / Codex)
164
+
165
+ [`AGENTS.md`](AGENTS.md) at the repo root carries the same guidance in the
166
+ cross-tool convention many agents read, and ships in the npm tarball so tools
167
+ scanning `node_modules/@rtorcato/repo-tooling` can find it.
168
+
169
+ ## What's new
170
+
171
+ See [CHANGELOG.md](CHANGELOG.md) for the full history.
172
+
173
+ ## Related packages
174
+
175
+ - [@rtorcato/js-common](https://github.com/rtorcato/js-common) — General TypeScript/JS utilities (strings, dates, numbers, async, errors)
176
+ - [@rtorcato/browser-common](https://github.com/rtorcato/browser-common) — Browser Web API wrappers (clipboard, observers, storage, etc.)
177
+
178
+ ## Roadmap
179
+
180
+ Direction and progress are tracked entirely on GitHub — see the
181
+ [milestones](https://github.com/rtorcato/repo-tooling/milestones) and
182
+ [open issues](https://github.com/rtorcato/repo-tooling/issues).
183
+
184
+ ## Contributing
185
+
186
+ Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
187
+
188
+ ## License
189
+
190
+ MIT — see [LICENSE](LICENSE).
191
+
192
+ <!-- js-tooling:skills:start -->
193
+ ## Install the skills (`npx skills`)
194
+
195
+ Any agent that supports the [`skills`](https://www.npmjs.com/package/skills) CLI can install this repo's skills straight from GitHub — no clone, no package install:
196
+
197
+ ```bash
198
+ npx skills add https://github.com/rtorcato/repo-tooling --skill repo-tooling
199
+ npx skills add https://github.com/rtorcato/repo-tooling --skill npm-publish
200
+ ```
201
+ <!-- js-tooling:skills:end -->
@@ -0,0 +1,336 @@
1
+ import path from 'node:path';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs-extra';
4
+ import { resolveLanguageModule } from '../../languages/registry.js';
5
+ import { SWIFT_GIT_HOOKS, runSwiftChecks } from '../../languages/swift/checks.js';
6
+ import { detectLanguage } from '../utils/detect-language.js';
7
+ import { checkGitHubSettings } from '../../base/github-settings.js';
8
+ import { LOCKFILE_VERSION, readLockfile } from '../utils/lockfile.js';
9
+ import { declinedInLock, getFixTargetForCheck } from './fix-targets.js';
10
+ import { checkAiSetup, checkCodeowners, checkCodeQL, checkCommunityHealth, checkCoverageUpload, checkDependabot, checkEditorConfig, checkFile, checkGitHooks, checkGitHubActions, checkGitLabCI, checkPrePushHook, checkReadmeBadges, COMMITLINT_FILE_CHECK, } from '../../base/checks.js';
11
+ import { allDeps, checkAreTheTypesWrong, checkDocsSite, checkEnginesNode, checkKnip, checkLintStaged, checkNodeVersionConsistency, checkNodeVersionPin, checkPackageJson, checkPublint, checkSemanticRelease, checkSizeLimit, checkTailwind, checkPnpmWorkspace, checkTreeshakeSetup, checkTurborepo, checkTypedoc, checkVerifyScript, checkVscodeExtensions, evaluateNodeVersion, FILE_CHECKS, findDocsAppDir, jsBadgeAudience, jsGitHooksProfile, readPackageJson, } from '../../languages/js/checks.js';
12
+ export { evaluateNodeVersion };
13
+ const PACKAGE = '@rtorcato/repo-tooling';
14
+ // Detects the broken-release-on-protected-main footgun: a workflow that runs
15
+ // semantic-release but only hands it GITHUB_TOKEN, which can't push the version
16
+ // commit + tag past branch protection. The fix is an admin PAT (RELEASE_TOKEN)
17
+ // with a GITHUB_TOKEN fallback.
18
+ async function checkReleaseToken(dir) {
19
+ const workflowsDir = path.join(dir, '.github', 'workflows');
20
+ if (!(await fs.pathExists(workflowsDir))) {
21
+ return { check: 'Release token', status: 'optional-missing', detail: 'no .github/workflows/' };
22
+ }
23
+ try {
24
+ const files = await fs.readdir(workflowsDir);
25
+ for (const f of files) {
26
+ if (!(f.endsWith('.yml') || f.endsWith('.yaml')))
27
+ continue;
28
+ const content = await fs.readFile(path.join(workflowsDir, f), 'utf-8');
29
+ if (!/semantic-release/.test(content))
30
+ continue;
31
+ if (/RELEASE_TOKEN/.test(content)) {
32
+ return {
33
+ check: 'Release token',
34
+ status: 'ok',
35
+ detail: `${f} uses RELEASE_TOKEN (with GITHUB_TOKEN fallback)`,
36
+ };
37
+ }
38
+ return {
39
+ check: 'Release token',
40
+ status: 'drift',
41
+ detail: `${f} runs semantic-release with bare GITHUB_TOKEN`,
42
+ hint: 'GITHUB_TOKEN cannot push to a protected main. Set the checkout `token:` and the semantic-release `GITHUB_TOKEN` env to `${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}` and add a RELEASE_TOKEN admin PAT secret',
43
+ };
44
+ }
45
+ return {
46
+ check: 'Release token',
47
+ status: 'optional-missing',
48
+ detail: 'no semantic-release workflow found',
49
+ };
50
+ }
51
+ catch {
52
+ return {
53
+ check: 'Release token',
54
+ status: 'optional-missing',
55
+ detail: 'unable to read .github/workflows/',
56
+ };
57
+ }
58
+ }
59
+ // Flags a release workflow still authenticating npm publish with a long-lived
60
+ // NPM_TOKEN secret instead of OIDC trusted publishing (#201). npm is deprecating
61
+ // 2FA-bypass tokens; OIDC needs no secret and adds provenance for free. Only
62
+ // relevant for public packages that actually publish to npm.
63
+ async function checkNpmOidcPublish(dir, pkg) {
64
+ const check = 'npm OIDC publish';
65
+ if (!pkg || pkg.private === true) {
66
+ return { check, status: 'optional-missing', detail: 'private package — no npm publish' };
67
+ }
68
+ const workflowsDir = path.join(dir, '.github', 'workflows');
69
+ if (!(await fs.pathExists(workflowsDir))) {
70
+ return { check, status: 'optional-missing', detail: 'no .github/workflows/' };
71
+ }
72
+ try {
73
+ const files = await fs.readdir(workflowsDir);
74
+ for (const f of files) {
75
+ if (!(f.endsWith('.yml') || f.endsWith('.yaml')))
76
+ continue;
77
+ const content = await fs.readFile(path.join(workflowsDir, f), 'utf-8');
78
+ if (!/semantic-release/.test(content))
79
+ continue;
80
+ if (/secrets\.NPM_TOKEN/.test(content)) {
81
+ return {
82
+ check,
83
+ status: 'drift',
84
+ detail: `${f} authenticates npm publish with NPM_TOKEN`,
85
+ hint: 'Migrate to OIDC trusted publishing: add a Trusted Publisher for each published package on npmjs.com (Settings → Trusted Publisher), then run `fix github-actions` to drop NPM_TOKEN (the release job keeps `id-token: write`). npm is deprecating 2FA-bypass tokens.',
86
+ };
87
+ }
88
+ return { check, status: 'ok', detail: `${f} publishes via OIDC (no NPM_TOKEN)` };
89
+ }
90
+ return { check, status: 'optional-missing', detail: 'no semantic-release workflow found' };
91
+ }
92
+ catch {
93
+ return { check, status: 'optional-missing', detail: 'unable to read .github/workflows/' };
94
+ }
95
+ }
96
+ function checkLockfile(lock) {
97
+ if (!lock) {
98
+ return {
99
+ check: 'lockfile',
100
+ status: 'optional-missing',
101
+ detail: 'no .repo-tooling.json — doctor cannot tell intentional opt-outs from drift',
102
+ hint: 'Run `npx @rtorcato/repo-tooling fix lockfile` to record current choices',
103
+ };
104
+ }
105
+ if (lock.version > LOCKFILE_VERSION) {
106
+ return {
107
+ check: 'lockfile',
108
+ status: 'drift',
109
+ detail: `.repo-tooling.json version ${lock.version} is newer than this CLI supports (v${LOCKFILE_VERSION})`,
110
+ hint: 'Upgrade @rtorcato/repo-tooling to a release that supports this lockfile version',
111
+ };
112
+ }
113
+ return {
114
+ check: 'lockfile',
115
+ status: 'ok',
116
+ detail: `.repo-tooling.json v${lock.version} (written by ${lock.writtenBy})`,
117
+ };
118
+ }
119
+ // Lockfile-driven demotion: if the lock records an intentional opt-out for a
120
+ // check that's currently optional-missing, demote it to ok with a clear detail.
121
+ function demoteDeclined(results, lock) {
122
+ if (!lock)
123
+ return results;
124
+ return results.map((r) => {
125
+ if (r.status !== 'optional-missing')
126
+ return r;
127
+ if (!declinedInLock(lock, r.check))
128
+ return r;
129
+ return {
130
+ check: r.check,
131
+ status: 'ok',
132
+ detail: 'intentionally declined (.repo-tooling.json)',
133
+ };
134
+ });
135
+ }
136
+ // The language-agnostic checks (src/base): repo hygiene, git hooks, CI,
137
+ // security, and GitHub repo-settings that apply to any repo regardless of
138
+ // language. Declared once and run for every project — a language module layers
139
+ // its own checks on top rather than re-listing these.
140
+ async function runBaseChecks(dir, lock, opts) {
141
+ const results = [];
142
+ results.push(checkLockfile(lock));
143
+ results.push(await checkEditorConfig(dir));
144
+ results.push(await checkFile(dir, COMMITLINT_FILE_CHECK));
145
+ if (opts.hooks) {
146
+ results.push(await checkGitHooks(dir, opts.hooks));
147
+ results.push(await checkPrePushHook(dir, opts.hooks));
148
+ }
149
+ results.push(await checkGitHubActions(dir));
150
+ results.push(await checkDependabot(dir));
151
+ results.push(await checkCodeQL(dir));
152
+ // GitHub repo-settings drift (branch protection, merge settings, workflow
153
+ // permissions). Read-only; self-skips as `ok` outside a live GitHub repo.
154
+ results.push(...(await checkGitHubSettings(dir)));
155
+ results.push(await checkGitLabCI(dir));
156
+ results.push(await checkCodeowners(dir));
157
+ results.push(await checkCommunityHealth(dir));
158
+ results.push(await checkAiSetup(dir));
159
+ results.push(await checkReadmeBadges(dir, opts.badges.audience, opts.badges.fixTarget));
160
+ results.push(await checkCoverageUpload(dir));
161
+ return results;
162
+ }
163
+ export async function runDoctor(dir) {
164
+ const targetDir = path.resolve(dir);
165
+ const lock = await readLockfile(targetDir);
166
+ // Per-module dispatch (#285): the base checks (repo hygiene, CI, security,
167
+ // GitHub settings) apply to any repo and run for every language. A supported
168
+ // module layers its own checks on top; an unsupported one (Swift/Perl/Python
169
+ // until their modules land) still gets the full base suite instead of the old
170
+ // wholesale skip. 'unknown' (bare dir mid-setup) resolves to JS so a fresh
171
+ // repo runs the full suite.
172
+ const language = await detectLanguage(targetDir);
173
+ const languageModule = resolveLanguageModule(language);
174
+ if (!languageModule.supported) {
175
+ const results = [
176
+ {
177
+ check: 'language',
178
+ status: 'ok',
179
+ detail: `detected ${languageModule.label} — running language-agnostic checks; ${languageModule.label}-specific checks land with its module (#139)`,
180
+ },
181
+ // hooks: null — nothing here encodes a Python/Perl hook convention yet,
182
+ // and guessing one would nag every repo with a fix target that doesn't
183
+ // exist. #289/#290 fill it in.
184
+ ...(await runBaseChecks(targetDir, lock, {
185
+ hooks: null,
186
+ badges: { audience: 'public', fixTarget: null },
187
+ })),
188
+ ];
189
+ return demoteDeclined(results, lock);
190
+ }
191
+ // Swift suite (#286): base checks plus the module's own. Swift repos have no
192
+ // package.json, so nothing JS-shaped runs.
193
+ if (languageModule.id === 'swift') {
194
+ const results = [
195
+ {
196
+ check: 'language',
197
+ status: 'ok',
198
+ detail: 'detected Swift (Package.swift)',
199
+ },
200
+ ...(await runBaseChecks(targetDir, lock, {
201
+ hooks: SWIFT_GIT_HOOKS,
202
+ // A SwiftPM package is distributed as public source over git tags, so
203
+ // badges always apply. No fixer: `fix badges` derives the block from
204
+ // package.json name/repository, which a Swift repo hasn't got.
205
+ badges: { audience: 'public', fixTarget: null },
206
+ })),
207
+ ...(await runSwiftChecks(targetDir)),
208
+ ];
209
+ return demoteDeclined(results, lock);
210
+ }
211
+ // JS suite: the module's own checks, then the shared base ones. Only the
212
+ // JS-shaped checks are listed here — re-listing the base suite is what made
213
+ // any new base check silently skip JS repos (#309).
214
+ const pkg = await readPackageJson(targetDir);
215
+ const results = [];
216
+ results.push(evaluateNodeVersion(process.version));
217
+ results.push(checkPackageJson(pkg));
218
+ results.push(checkEnginesNode(pkg));
219
+ results.push(await checkVscodeExtensions(targetDir));
220
+ results.push(await checkNodeVersionPin(targetDir));
221
+ results.push(await checkNodeVersionConsistency(targetDir, pkg));
222
+ for (const spec of FILE_CHECKS) {
223
+ results.push(await checkFile(targetDir, spec));
224
+ }
225
+ results.push(await checkLintStaged(targetDir, pkg));
226
+ results.push(await checkVerifyScript(targetDir, pkg));
227
+ results.push(await checkSemanticRelease(targetDir, pkg));
228
+ results.push(await checkKnip(targetDir, pkg));
229
+ results.push(await checkSizeLimit(targetDir, pkg));
230
+ results.push(await checkReleaseToken(targetDir));
231
+ results.push(await checkNpmOidcPublish(targetDir, pkg));
232
+ results.push(await checkTypedoc(targetDir, pkg));
233
+ results.push(await checkAreTheTypesWrong(targetDir, pkg));
234
+ results.push(await checkPublint(targetDir, pkg));
235
+ results.push(await checkTreeshakeSetup(targetDir, pkg));
236
+ results.push(await checkPnpmWorkspace(targetDir, pkg));
237
+ // Turborepo is monorepo-only — only surface the check when a workspace exists.
238
+ if (await fs.pathExists(path.join(targetDir, 'pnpm-workspace.yaml'))) {
239
+ results.push(await checkTurborepo(targetDir));
240
+ }
241
+ // Docs site is opt-in — only surface the check when a Docusaurus site exists.
242
+ const docsAppDir = await findDocsAppDir(targetDir);
243
+ if (docsAppDir) {
244
+ results.push(await checkDocsSite(targetDir, docsAppDir));
245
+ }
246
+ // Tailwind is opt-in — only surface the check when the repo actually depends on it.
247
+ if ('tailwindcss' in allDeps(pkg)) {
248
+ results.push(await checkTailwind(targetDir, pkg));
249
+ }
250
+ results.push(...(await runBaseChecks(targetDir, lock, {
251
+ hooks: jsGitHooksProfile(pkg),
252
+ badges: { audience: jsBadgeAudience(pkg), fixTarget: 'badges' },
253
+ })));
254
+ return demoteDeclined(results, lock);
255
+ }
256
+ const STATUS_ICONS = {
257
+ ok: chalk.green('✅'),
258
+ drift: chalk.yellow('⚠️ '),
259
+ missing: chalk.red('❌'),
260
+ 'optional-missing': chalk.gray('➖'),
261
+ };
262
+ function statusLabel(status) {
263
+ switch (status) {
264
+ case 'ok':
265
+ return chalk.green('ok');
266
+ case 'drift':
267
+ return chalk.yellow('drift');
268
+ case 'missing':
269
+ return chalk.red('missing');
270
+ case 'optional-missing':
271
+ return chalk.gray('not configured');
272
+ }
273
+ }
274
+ const MAX_NEXT_STEP_SUGGESTIONS = 8;
275
+ export function nextStepSuggestions(results, language) {
276
+ const fixable = results.filter((r) => r.status === 'drift' || r.status === 'missing' || r.status === 'optional-missing');
277
+ const lines = [];
278
+ let overflow = 0;
279
+ for (const r of fixable) {
280
+ const target = getFixTargetForCheck(r.check, language);
281
+ if (!target)
282
+ continue;
283
+ if (lines.length >= MAX_NEXT_STEP_SUGGESTIONS) {
284
+ overflow++;
285
+ continue;
286
+ }
287
+ const verb = r.status === 'drift' ? 'align' : 'scaffold';
288
+ lines.push(`Run \`npx @rtorcato/repo-tooling fix ${target}\` to ${verb} ${r.check}`);
289
+ }
290
+ if (overflow > 0) {
291
+ lines.push(`...and ${overflow} more — run \`npx @rtorcato/repo-tooling fix\` to walk all findings`);
292
+ }
293
+ else if (lines.length > 0) {
294
+ lines.push('Run `npx @rtorcato/repo-tooling fix` to walk all findings interactively');
295
+ }
296
+ return lines;
297
+ }
298
+ export function summarize(results) {
299
+ return {
300
+ ok: results.filter((r) => r.status === 'ok').length,
301
+ drift: results.filter((r) => r.status === 'drift').length,
302
+ missing: results.filter((r) => r.status === 'missing').length,
303
+ optionalMissing: results.filter((r) => r.status === 'optional-missing').length,
304
+ };
305
+ }
306
+ export async function doctorCommand(options = {}) {
307
+ const dir = options.directory ?? process.cwd();
308
+ const results = await runDoctor(dir);
309
+ if (options.json) {
310
+ console.log(JSON.stringify({ directory: path.resolve(dir), results }, null, 2));
311
+ }
312
+ else {
313
+ console.log(chalk.cyan(`\n🩺 Diagnosing ${path.resolve(dir)} against ${PACKAGE} presets...\n`));
314
+ for (const r of results) {
315
+ console.log(` ${STATUS_ICONS[r.status]} ${chalk.bold(r.check)} — ${statusLabel(r.status)}`);
316
+ console.log(` ${chalk.gray(r.detail)}`);
317
+ if (r.hint && r.status !== 'ok') {
318
+ console.log(` ${chalk.dim('hint:')} ${chalk.dim(r.hint)}`);
319
+ }
320
+ }
321
+ const summary = summarize(results);
322
+ console.log();
323
+ console.log(` Summary: ${chalk.green(`${summary.ok} ok`)}, ${chalk.yellow(`${summary.drift} drift`)}, ${chalk.red(`${summary.missing} missing`)}, ${chalk.gray(`${summary.optionalMissing} not configured`)}\n`);
324
+ const suggestions = nextStepSuggestions(results, await detectLanguage(dir));
325
+ if (suggestions.length > 0) {
326
+ console.log(chalk.bold(' Next steps:'));
327
+ for (const s of suggestions) {
328
+ console.log(` ${chalk.gray('-')} ${s}`);
329
+ }
330
+ console.log();
331
+ }
332
+ }
333
+ const summary = summarize(results);
334
+ const exitCode = summary.drift > 0 || summary.missing > 0 ? 1 : 0;
335
+ process.exitCode = exitCode;
336
+ }