@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
@@ -0,0 +1,735 @@
1
+ import path from 'node:path';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs-extra';
4
+ import { buildBadgeBlock, parseRepository, upsertBadges } from '../../cli/generators/badges.js';
5
+ import { generateReleasePleaseConfig, generateRolldownConfig, generateRollupConfig, generateSemanticReleaseConfig, } from '../../cli/generators/build.js';
6
+ import { generateHuskyConfig, generatePrePushHook } from '../../cli/generators/git.js';
7
+ import { generateGitHubActions } from '../../cli/generators/github-actions.js';
8
+ import { generateGitLabCI } from '../../cli/generators/gitlab-ci.js';
9
+ import { GH_WORKFLOWS, generateGhWorkflow } from '../../cli/generators/github-workflows.js';
10
+ import { generateESLintConfig, generatePrettierConfig } from '../../cli/generators/linting.js';
11
+ import { alignNodeVersion, ensureEnginesNode, generateKnipConfig, generateNvmrc, generateSizeLimitConfig, generateVscodeExtensions, } from '../../cli/generators/misc.js';
12
+ import { composeVerifyScriptFromPkg } from '../../cli/generators/package-json.js';
13
+ import { WORKSPACE_FILE, dependsOnEsbuild, ensurePnpmSettings, } from '../../cli/generators/pnpm-workspace.js';
14
+ import { generatePostcss } from '../../cli/generators/postcss.js';
15
+ import { generateCypressConfig, generateVitestConfig } from '../../cli/generators/testing.js';
16
+ import { generateTreeshakeCheck, inferSubpathsFromExports } from '../../cli/generators/treeshake.js';
17
+ import { generateTailwind } from '../../cli/generators/tailwind.js';
18
+ import { generateTurborepo } from '../../cli/generators/turborepo.js';
19
+ import { generateNx } from '../../cli/generators/nx.js';
20
+ import { generateBun } from '../../cli/generators/bun.js';
21
+ import { generateDocsSite } from '../../cli/generators/docs-site.js';
22
+ import { generateTypedocConfig, generateTypedocWorkflow } from '../../cli/generators/typedoc.js';
23
+ import { copyPreset } from '../../cli/utils/copy-preset.js';
24
+ import { LOCKFILE_NAME, writeLockfile } from '../../cli/utils/lockfile.js';
25
+ function inferProjectConfig(pkg) {
26
+ const deps = {
27
+ ...(pkg?.dependencies ?? {}),
28
+ ...(pkg?.devDependencies ?? {}),
29
+ };
30
+ let projectType = 'library';
31
+ if (deps.next)
32
+ projectType = 'nextjs-app';
33
+ else if (deps['react-dom'])
34
+ projectType = 'react-app';
35
+ return {
36
+ projectName: pkg?.name ?? 'project',
37
+ projectType,
38
+ typescript: { enabled: true, config: projectType === 'nextjs-app' ? 'next' : 'base' },
39
+ linting: {
40
+ tool: 'biome',
41
+ eslintConfig: projectType === 'nextjs-app' ? 'nextjs' : 'base',
42
+ },
43
+ formatting: { tool: 'biome' },
44
+ testing: { framework: 'vitest', environment: 'node' },
45
+ gitHooks: true,
46
+ commitLint: true,
47
+ semanticRelease: pkg?.private !== true,
48
+ securityAutomation: true,
49
+ bundler: 'tsup',
50
+ };
51
+ }
52
+ export async function readPackageJson(dir) {
53
+ const filepath = path.join(dir, 'package.json');
54
+ if (!(await fs.pathExists(filepath)))
55
+ return null;
56
+ try {
57
+ return (await fs.readJson(filepath));
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ /** True when any (possibly nested) exports condition declares a `require`/CJS entry. */
64
+ function hasRequireCondition(exports) {
65
+ if (!exports || typeof exports !== 'object')
66
+ return false;
67
+ for (const value of Object.values(exports)) {
68
+ if (value && typeof value === 'object') {
69
+ if ('require' in value)
70
+ return true;
71
+ if (hasRequireCondition(value))
72
+ return true;
73
+ }
74
+ }
75
+ return false;
76
+ }
77
+ /** ESM-only = `"type": "module"` and no CJS/`require` resolution in exports. */
78
+ function isEsmOnly(pkg) {
79
+ return pkg.type === 'module' && !hasRequireCondition(pkg.exports);
80
+ }
81
+ // Optional deploy workflows (issue #66). One flat fix target each, no doctor
82
+ // check and no setup prompt — they're opt-in and too deploy-specific to nag.
83
+ const GH_WORKFLOW_META = {
84
+ 'docker-publish': {
85
+ check: 'Docker publish workflow',
86
+ desc: 'Scaffold .github/workflows/docker-publish.yml (build + push image to GHCR on tag)',
87
+ },
88
+ 'vercel-deploy': {
89
+ check: 'Vercel deploy workflow',
90
+ desc: 'Scaffold .github/workflows/vercel-deploy.yml (production deploy to Vercel on main)',
91
+ },
92
+ 'cloudflare-pages': {
93
+ check: 'Cloudflare Pages workflow',
94
+ desc: 'Scaffold .github/workflows/cloudflare-pages.yml (static-site deploy to Cloudflare Pages)',
95
+ },
96
+ 'preview-deployments': {
97
+ check: 'Preview deployments workflow',
98
+ desc: 'Scaffold .github/workflows/preview-deployments.yml (per-PR preview deploy)',
99
+ },
100
+ };
101
+ const GH_WORKFLOW_FIXERS = GH_WORKFLOWS.map((name) => ({
102
+ target: name,
103
+ description: GH_WORKFLOW_META[name].desc,
104
+ appliesTo: [GH_WORKFLOW_META[name].check],
105
+ outputs: [`.github/workflows/${name}.yml`],
106
+ // safe-add: never clobber an existing workflow of the same name.
107
+ riskLevel: 'safe-add',
108
+ async run({ targetDir }) {
109
+ const written = await generateGhWorkflow(name, targetDir);
110
+ return { filesWritten: [written] };
111
+ },
112
+ }));
113
+ export const FIXERS = [
114
+ {
115
+ target: 'biome',
116
+ description: 'Scaffold biome.json extending the @rtorcato/repo-tooling preset',
117
+ appliesTo: ['Biome'],
118
+ outputs: ['biome.json'],
119
+ canFixDrift: true,
120
+ async run({ targetDir }) {
121
+ const result = await copyPreset('biome', targetDir);
122
+ return { filesWritten: [result.target] };
123
+ },
124
+ },
125
+ {
126
+ target: 'tsconfig',
127
+ description: 'Scaffold tsconfig.json extending the @rtorcato/repo-tooling preset',
128
+ appliesTo: ['TypeScript'],
129
+ outputs: ['tsconfig.json'],
130
+ canFixDrift: true,
131
+ async run({ targetDir }) {
132
+ const result = await copyPreset('tsconfig', targetDir);
133
+ return { filesWritten: [result.target] };
134
+ },
135
+ },
136
+ {
137
+ target: 'eslint',
138
+ description: 'Scaffold eslint.config.mjs importing the @rtorcato/repo-tooling preset',
139
+ appliesTo: ['ESLint'],
140
+ outputs: ['eslint.config.mjs'],
141
+ canFixDrift: true,
142
+ async run({ targetDir, pkg }) {
143
+ await generateESLintConfig(inferProjectConfig(pkg), targetDir);
144
+ return { filesWritten: ['eslint.config.mjs'] };
145
+ },
146
+ },
147
+ {
148
+ target: 'prettier',
149
+ description: 'Scaffold prettier.config.mjs re-exporting the preset',
150
+ appliesTo: ['Prettier'],
151
+ outputs: ['prettier.config.mjs'],
152
+ canFixDrift: true,
153
+ async run({ targetDir }) {
154
+ await generatePrettierConfig(targetDir);
155
+ return { filesWritten: ['prettier.config.mjs'] };
156
+ },
157
+ },
158
+ {
159
+ target: 'vitest',
160
+ description: 'Scaffold vitest.config.ts (preserves vitest.setup.ts if present)',
161
+ appliesTo: ['Vitest'],
162
+ outputs: ['vitest.config.ts'],
163
+ canFixDrift: true,
164
+ async run({ targetDir, pkg }) {
165
+ const setupPath = path.join(targetDir, 'vitest.setup.ts');
166
+ const hadSetup = await fs.pathExists(setupPath);
167
+ const savedSetup = hadSetup ? await fs.readFile(setupPath, 'utf-8') : null;
168
+ await generateVitestConfig(inferProjectConfig(pkg), targetDir);
169
+ if (hadSetup && savedSetup !== null) {
170
+ await fs.writeFile(setupPath, savedSetup);
171
+ }
172
+ return { filesWritten: ['vitest.config.ts'] };
173
+ },
174
+ },
175
+ {
176
+ target: 'cypress',
177
+ description: 'Scaffold cypress.config.ts (re-exports the preset) + tests/e2e and cypress/support boilerplate',
178
+ // No doctor check — E2E is opt-in, so this runs only when explicitly targeted.
179
+ appliesTo: [],
180
+ outputs: [
181
+ 'cypress.config.ts',
182
+ 'cypress/support/e2e.ts',
183
+ 'cypress/support/commands.ts',
184
+ 'tests/e2e/example.cy.ts',
185
+ ],
186
+ // safe-add: regenerates the config re-export but never clobbers existing specs/support.
187
+ riskLevel: 'safe-add',
188
+ async run({ targetDir }) {
189
+ const filesWritten = await generateCypressConfig(targetDir);
190
+ return { filesWritten };
191
+ },
192
+ },
193
+ {
194
+ target: 'husky',
195
+ description: 'Set up Husky + lint-staged (and a `pnpm verify` pre-push hook)',
196
+ appliesTo: ['Git hooks', 'lint-staged', 'Pre-push hook'],
197
+ outputs: ['.husky/pre-commit', '.husky/pre-push', 'package.json (lint-staged field)'],
198
+ riskLevel: 'safe-merge',
199
+ canFixDrift: true,
200
+ async run({ targetDir, pkg }) {
201
+ const pkgPath = path.join(targetDir, 'package.json');
202
+ const existingLintStaged = pkg?.['lint-staged'] ?? {};
203
+ await generateHuskyConfig(inferProjectConfig(pkg), targetDir);
204
+ const updated = (await fs.readJson(pkgPath));
205
+ const generated = updated['lint-staged'] ?? {};
206
+ updated['lint-staged'] = { ...generated, ...existingLintStaged };
207
+ await fs.writeJson(pkgPath, updated, { spaces: 2 });
208
+ const filesWritten = ['.husky/pre-commit', 'package.json'];
209
+ const scripts = updated.scripts ?? {};
210
+ if (scripts.verify) {
211
+ await generatePrePushHook(targetDir);
212
+ filesWritten.push('.husky/pre-push');
213
+ }
214
+ return { filesWritten };
215
+ },
216
+ },
217
+ {
218
+ target: 'verify',
219
+ description: 'Add a unified `verify` script (typecheck && lint && tests) to package.json',
220
+ appliesTo: ['verify script'],
221
+ outputs: ['package.json (scripts.verify)'],
222
+ riskLevel: 'safe-merge',
223
+ canFixDrift: true,
224
+ async run({ targetDir, pkg }) {
225
+ const pkgPath = path.join(targetDir, 'package.json');
226
+ if (!pkg) {
227
+ console.log(chalk.yellow(' no package.json found — skipping'));
228
+ return { filesWritten: [] };
229
+ }
230
+ const includeTreeshake = await fs.pathExists(path.join(targetDir, 'apps', 'treeshake-check', 'check.mjs'));
231
+ const verify = composeVerifyScriptFromPkg(pkg, { includeTreeshake });
232
+ if (!verify) {
233
+ console.log(chalk.gray(' not enough tools enabled to compose a verify chain — skipping (need 2+ of typecheck/lint/tests)'));
234
+ return { filesWritten: [] };
235
+ }
236
+ const updated = { ...pkg };
237
+ const scripts = { ...(updated.scripts ?? {}) };
238
+ scripts.verify = verify;
239
+ if (includeTreeshake && !scripts.treeshake) {
240
+ scripts.treeshake = 'pnpm --filter=*treeshake-check run check';
241
+ if (!scripts.pretreeshake) {
242
+ scripts.pretreeshake = scripts.build ? 'pnpm build' : 'echo "no build step"';
243
+ }
244
+ }
245
+ updated.scripts = scripts;
246
+ await fs.writeJson(pkgPath, updated, { spaces: 2 });
247
+ return { filesWritten: ['package.json'] };
248
+ },
249
+ },
250
+ {
251
+ target: 'semantic-release',
252
+ description: 'Scaffold release.config.mjs + install preset plugins (skipped on private packages)',
253
+ appliesTo: ['semantic-release'],
254
+ outputs: ['release.config.mjs', 'package.json'],
255
+ canFixDrift: true,
256
+ async run({ targetDir, pkg }) {
257
+ if (pkg?.private === true) {
258
+ console.log(chalk.gray(' skipping — package is private'));
259
+ return { filesWritten: [] };
260
+ }
261
+ const filesWritten = await generateSemanticReleaseConfig(targetDir);
262
+ return { filesWritten };
263
+ },
264
+ },
265
+ {
266
+ target: 'changesets',
267
+ description: 'Scaffold .changeset/config.json (alternative to semantic-release)',
268
+ appliesTo: ['Changesets'],
269
+ outputs: ['.changeset/config.json'],
270
+ canFixDrift: true,
271
+ async run({ targetDir }) {
272
+ const result = await copyPreset('changesets', targetDir);
273
+ return { filesWritten: [result.target] };
274
+ },
275
+ },
276
+ {
277
+ target: 'release-please',
278
+ description: 'Scaffold release-please-config.json + manifest + release-please.yml workflow (alternative to semantic-release)',
279
+ appliesTo: ['Release Please'],
280
+ outputs: [
281
+ 'release-please-config.json',
282
+ '.release-please-manifest.json',
283
+ '.github/workflows/release-please.yml',
284
+ ],
285
+ canFixDrift: true,
286
+ async run({ targetDir }) {
287
+ const filesWritten = await generateReleasePleaseConfig(targetDir);
288
+ return { filesWritten };
289
+ },
290
+ },
291
+ {
292
+ target: 'oxlint',
293
+ description: 'Scaffold .oxlintrc.json (additive to Biome/ESLint)',
294
+ appliesTo: ['Oxlint'],
295
+ outputs: ['.oxlintrc.json'],
296
+ canFixDrift: true,
297
+ async run({ targetDir }) {
298
+ const result = await copyPreset('oxlint', targetDir);
299
+ return { filesWritten: [result.target] };
300
+ },
301
+ },
302
+ {
303
+ target: 'github-actions',
304
+ description: 'Scaffold .github/workflows/ci.yml (+ codecov.yml when tests run)',
305
+ appliesTo: ['GitHub Actions', 'Coverage upload', 'npm OIDC publish'],
306
+ outputs: ['.github/workflows/ci.yml', 'codecov.yml'],
307
+ canFixDrift: true,
308
+ async run({ targetDir, pkg }) {
309
+ await generateGitHubActions(inferProjectConfig(pkg), targetDir);
310
+ const filesWritten = ['.github/workflows/ci.yml'];
311
+ if (await fs.pathExists(path.join(targetDir, 'codecov.yml')))
312
+ filesWritten.push('codecov.yml');
313
+ return { filesWritten };
314
+ },
315
+ },
316
+ {
317
+ target: 'gitlab-ci',
318
+ description: 'Scaffold .gitlab-ci.yml (lint/typecheck/test/build mirrored from GitHub Actions)',
319
+ appliesTo: ['GitLab CI'],
320
+ outputs: ['.gitlab-ci.yml'],
321
+ canFixDrift: true,
322
+ async run({ targetDir, pkg }) {
323
+ const written = await generateGitLabCI(inferProjectConfig(pkg), targetDir);
324
+ return { filesWritten: [written] };
325
+ },
326
+ },
327
+ ...GH_WORKFLOW_FIXERS,
328
+ {
329
+ target: 'turborepo',
330
+ description: 'Scaffold turbo.json task pipeline (pnpm-workspace monorepos)',
331
+ appliesTo: ['Turborepo'],
332
+ outputs: ['turbo.json'],
333
+ // safe-add: never clobber a hand-tuned turbo.json.
334
+ riskLevel: 'safe-add',
335
+ async run({ targetDir }) {
336
+ const written = await generateTurborepo(targetDir);
337
+ return { filesWritten: [written] };
338
+ },
339
+ },
340
+ {
341
+ target: 'pnpm-workspace',
342
+ description: 'Merge the managed pnpm settings into pnpm-workspace.yaml',
343
+ appliesTo: ['pnpm settings'],
344
+ outputs: [WORKSPACE_FILE],
345
+ // safe-merge: the file also carries the repo's own packages: globs and
346
+ // hand-vetted allowBuilds entries, so only missing keys are added.
347
+ riskLevel: 'safe-merge',
348
+ canFixDrift: true,
349
+ async run({ targetDir, pkg }) {
350
+ const deps = {
351
+ ...(pkg?.dependencies ?? {}),
352
+ ...(pkg?.devDependencies ?? {}),
353
+ };
354
+ const written = await ensurePnpmSettings(targetDir, dependsOnEsbuild(deps));
355
+ return { filesWritten: written ? [written] : [] };
356
+ },
357
+ },
358
+ {
359
+ target: 'nx',
360
+ description: 'Scaffold nx.json task orchestrator (alternative to Turborepo)',
361
+ // No doctor check — orchestrator is opt-in and Turborepo is the default;
362
+ // runs only when explicitly targeted. generateNx never clobbers nx.json.
363
+ appliesTo: [],
364
+ outputs: ['nx.json'],
365
+ riskLevel: 'safe-add',
366
+ async run({ targetDir }) {
367
+ const filesWritten = await generateNx(targetDir);
368
+ return { filesWritten };
369
+ },
370
+ },
371
+ {
372
+ target: 'tailwind',
373
+ description: 'Scaffold Tailwind CSS v4 (postcss.config.mjs + src/styles/globals.css)',
374
+ appliesTo: ['Tailwind'],
375
+ outputs: ['postcss.config.mjs', 'src/styles/globals.css'],
376
+ // safe-add: never clobber an existing PostCSS config or CSS entry.
377
+ riskLevel: 'safe-add',
378
+ async run({ targetDir }) {
379
+ const written = await generateTailwind(targetDir);
380
+ return { filesWritten: written };
381
+ },
382
+ },
383
+ {
384
+ target: 'postcss',
385
+ description: 'Scaffold postcss.config.mjs with autoprefixer (non-Tailwind CSS pipelines)',
386
+ appliesTo: ['PostCSS'],
387
+ outputs: ['postcss.config.mjs'],
388
+ // safe-add: never clobber an existing config (incl. one written by `fix tailwind`).
389
+ riskLevel: 'safe-add',
390
+ async run({ targetDir }) {
391
+ const written = await generatePostcss(targetDir);
392
+ return { filesWritten: written };
393
+ },
394
+ },
395
+ {
396
+ target: 'vscode-extensions',
397
+ description: 'Recommend the VS Code extensions matching the enabled tools (.vscode/extensions.json)',
398
+ appliesTo: ['VS Code extensions'],
399
+ outputs: ['.vscode/extensions.json'],
400
+ // Preserves any recommendations already present; only appends ours.
401
+ riskLevel: 'safe-merge',
402
+ canFixDrift: true,
403
+ async run({ targetDir, pkg }) {
404
+ const written = await generateVscodeExtensions(inferProjectConfig(pkg), targetDir);
405
+ return { filesWritten: written ? [written] : [] };
406
+ },
407
+ },
408
+ {
409
+ target: 'nvmrc',
410
+ description: 'Scaffold .nvmrc pinned to Node 22',
411
+ appliesTo: ['Node version pin'],
412
+ outputs: ['.nvmrc'],
413
+ canFixDrift: true,
414
+ async run({ targetDir }) {
415
+ await generateNvmrc(targetDir);
416
+ return { filesWritten: ['.nvmrc'] };
417
+ },
418
+ },
419
+ {
420
+ target: 'node-version',
421
+ description: 'Point CI workflows at `node-version-file: .nvmrc` (one Node source of truth)',
422
+ appliesTo: ['Node version consistency'],
423
+ outputs: ['.nvmrc', '.github/workflows/*.yml (node-version-file)'],
424
+ riskLevel: 'safe-merge',
425
+ canFixDrift: true,
426
+ async run({ targetDir }) {
427
+ const filesWritten = await alignNodeVersion(targetDir);
428
+ return { filesWritten };
429
+ },
430
+ },
431
+ {
432
+ target: 'engines',
433
+ description: 'Add engines.node to package.json',
434
+ appliesTo: ['engines.node'],
435
+ outputs: ['package.json (engines.node field)'],
436
+ riskLevel: 'safe-merge',
437
+ canFixDrift: true,
438
+ async run({ targetDir }) {
439
+ const result = await ensureEnginesNode(targetDir);
440
+ return { filesWritten: result === 'added' ? ['package.json'] : [] };
441
+ },
442
+ },
443
+ {
444
+ target: 'knip',
445
+ description: 'Scaffold knip.json (entry globs matched to the package.json build model)',
446
+ appliesTo: ['knip'],
447
+ outputs: ['knip.json'],
448
+ canFixDrift: true,
449
+ async run({ targetDir }) {
450
+ await generateKnipConfig(targetDir);
451
+ return { filesWritten: ['knip.json'] };
452
+ },
453
+ },
454
+ {
455
+ target: 'size-limit',
456
+ description: 'Scaffold a size-limit budget — exports-driven .size-limit.cjs for multi-subpath libraries, else a static .size-limit.json',
457
+ appliesTo: ['size-limit'],
458
+ outputs: ['.size-limit.cjs', '.size-limit.json'],
459
+ canFixDrift: true,
460
+ async run({ targetDir }) {
461
+ const written = await generateSizeLimitConfig(targetDir);
462
+ return { filesWritten: [written] };
463
+ },
464
+ },
465
+ {
466
+ target: 'rollup',
467
+ description: 'Scaffold rollup.config.mjs re-exporting the shared library preset',
468
+ appliesTo: [],
469
+ outputs: ['rollup.config.mjs'],
470
+ riskLevel: 'safe-add',
471
+ async run({ targetDir }) {
472
+ await generateRollupConfig(targetDir);
473
+ return { filesWritten: ['rollup.config.mjs'] };
474
+ },
475
+ },
476
+ {
477
+ target: 'rolldown',
478
+ description: 'Scaffold rolldown.config.mjs re-exporting the shared library preset',
479
+ appliesTo: [],
480
+ outputs: ['rolldown.config.mjs'],
481
+ riskLevel: 'safe-add',
482
+ async run({ targetDir }) {
483
+ await generateRolldownConfig(targetDir);
484
+ return { filesWritten: ['rolldown.config.mjs'] };
485
+ },
486
+ },
487
+ {
488
+ target: 'bun',
489
+ description: 'Scaffold bunfig.toml + a Bun-typed tsconfig.json (extends the bun preset) for Bun runtime/test users',
490
+ // Opt-in toolchain — no doctor check; runs only when explicitly targeted.
491
+ appliesTo: [],
492
+ outputs: ['bunfig.toml', 'tsconfig.json'],
493
+ // safe-add: never clobbers an existing bunfig.toml or tsconfig.json.
494
+ riskLevel: 'safe-add',
495
+ async run({ targetDir }) {
496
+ const filesWritten = await generateBun(targetDir);
497
+ return { filesWritten };
498
+ },
499
+ },
500
+ {
501
+ target: 'treeshake-check',
502
+ description: 'Scaffold apps/treeshake-check — esbuild + metafile assertion that one subpath bundles cleanly',
503
+ appliesTo: ['Tree-shake check'],
504
+ outputs: [
505
+ 'apps/treeshake-check/package.json',
506
+ 'apps/treeshake-check/check.mjs',
507
+ 'apps/treeshake-check/src/entry.ts',
508
+ 'pnpm-workspace.yaml',
509
+ ],
510
+ riskLevel: 'safe-add',
511
+ canFixDrift: false,
512
+ async run({ targetDir, pkg }) {
513
+ if (!pkg) {
514
+ console.log(chalk.yellow(' no package.json found — skipping'));
515
+ return { filesWritten: [] };
516
+ }
517
+ const workspaceName = pkg.name ?? null;
518
+ if (!workspaceName) {
519
+ console.log(chalk.yellow(' package.json has no `name` — skipping'));
520
+ return { filesWritten: [] };
521
+ }
522
+ const { allCandidates, defaultAllowed } = inferSubpathsFromExports(pkg);
523
+ if (allCandidates.length < 2 || !defaultAllowed) {
524
+ console.log(chalk.yellow(' package.json does not expose ≥2 subpath exports — tree-shake check needs multiple subpaths to be meaningful. Skipping.'));
525
+ return { filesWritten: [] };
526
+ }
527
+ const allowedSubpath = defaultAllowed;
528
+ const forbiddenSubpaths = allCandidates.filter((s) => s !== allowedSubpath);
529
+ const written = await generateTreeshakeCheck(targetDir, {
530
+ workspaceName,
531
+ allowedSubpath,
532
+ forbiddenSubpaths,
533
+ });
534
+ console.log(chalk.dim(` Wired '${allowedSubpath}' as allowed; forbidden = [${forbiddenSubpaths.join(', ')}]. Edit apps/treeshake-check/check.mjs to tune.`));
535
+ return { filesWritten: written };
536
+ },
537
+ },
538
+ {
539
+ target: 'typedoc',
540
+ description: 'Scaffold typedoc.json extending the preset + .github/workflows/docs.yml (GitHub Pages)',
541
+ appliesTo: ['TypeDoc'],
542
+ outputs: ['typedoc.json', '.github/workflows/docs.yml'],
543
+ riskLevel: 'safe-add',
544
+ canFixDrift: true,
545
+ async run({ targetDir, pkg }) {
546
+ await generateTypedocConfig(pkg, targetDir);
547
+ const workflow = await generateTypedocWorkflow(targetDir);
548
+ const pkgPath = path.join(targetDir, 'package.json');
549
+ const filesWritten = ['typedoc.json', workflow];
550
+ if (await fs.pathExists(pkgPath)) {
551
+ const pkgData = (await fs.readJson(pkgPath));
552
+ const scripts = pkgData.scripts ?? {};
553
+ if (!scripts.docs) {
554
+ pkgData.scripts = { ...scripts, docs: 'typedoc' };
555
+ await fs.writeJson(pkgPath, pkgData, { spaces: 2 });
556
+ filesWritten.push('package.json');
557
+ }
558
+ }
559
+ return { filesWritten };
560
+ },
561
+ },
562
+ {
563
+ target: 'docs-site',
564
+ description: 'Scaffold a Docusaurus docs site under apps/docs (config/sidebars/tokens + reusable Pages deploy), inferring name/org/repo from package.json',
565
+ // Manual/opt-in target — the "Docs site" doctor check is opt-in (only
566
+ // surfaces once a site exists), so this never nags a repo without one.
567
+ appliesTo: [],
568
+ outputs: [
569
+ 'apps/docs/**',
570
+ 'scripts/sync-changelog.mjs',
571
+ 'pnpm-workspace.yaml',
572
+ '.github/workflows/docs.yml',
573
+ ],
574
+ riskLevel: 'safe-add',
575
+ async run({ targetDir, pkg }) {
576
+ // Wire the TypeDoc API section (#229) when the repo already uses TypeDoc —
577
+ // a typedoc config on disk or the dep installed. No new CLI flag needed.
578
+ const deps = {
579
+ ...(pkg?.dependencies ?? {}),
580
+ ...(pkg?.devDependencies ?? {}),
581
+ };
582
+ const typedocConfigs = [
583
+ 'typedoc.json',
584
+ 'typedoc.config.js',
585
+ 'typedoc.config.mjs',
586
+ 'typedoc.config.cjs',
587
+ 'typedoc.config.ts',
588
+ ];
589
+ let hasTypedocConfig = false;
590
+ for (const c of typedocConfigs) {
591
+ if (await fs.pathExists(path.join(targetDir, c))) {
592
+ hasTypedocConfig = true;
593
+ break;
594
+ }
595
+ }
596
+ const typedoc = hasTypedocConfig || 'typedoc' in deps;
597
+ const filesWritten = await generateDocsSite(pkg, targetDir, { typedoc });
598
+ return { filesWritten };
599
+ },
600
+ },
601
+ {
602
+ target: 'attw',
603
+ description: 'Install @arethetypeswrong/cli + add an `attw` script (esm-only profile when applicable) and wire it into verify',
604
+ appliesTo: ['are-the-types-wrong'],
605
+ outputs: ['package.json (devDependencies + scripts.attw)'],
606
+ riskLevel: 'safe-merge',
607
+ canFixDrift: true,
608
+ async run({ targetDir, pkg }) {
609
+ const pkgPath = path.join(targetDir, 'package.json');
610
+ if (!pkg) {
611
+ console.log(chalk.yellow(' no package.json found — skipping'));
612
+ return { filesWritten: [] };
613
+ }
614
+ const updated = { ...pkg };
615
+ const devDeps = {
616
+ ...(updated.devDependencies ?? {}),
617
+ };
618
+ if (!devDeps['@arethetypeswrong/cli'])
619
+ devDeps['@arethetypeswrong/cli'] = '^0.18.2';
620
+ updated.devDependencies = devDeps;
621
+ const scripts = { ...(updated.scripts ?? {}) };
622
+ scripts.attw = isEsmOnly(pkg) ? 'attw --pack --profile esm-only' : 'attw --pack';
623
+ if (scripts.verify && !/\battw\b/.test(scripts.verify)) {
624
+ scripts.verify = `${scripts.verify} && pnpm attw`;
625
+ }
626
+ updated.scripts = scripts;
627
+ await fs.writeJson(pkgPath, updated, { spaces: 2 });
628
+ return { filesWritten: ['package.json'] };
629
+ },
630
+ },
631
+ {
632
+ target: 'publint',
633
+ description: 'Install publint + add a `publint --strict` script and wire it into verify',
634
+ appliesTo: ['publint'],
635
+ outputs: ['package.json (devDependencies + scripts.publint)'],
636
+ riskLevel: 'safe-merge',
637
+ canFixDrift: true,
638
+ async run({ targetDir, pkg }) {
639
+ const pkgPath = path.join(targetDir, 'package.json');
640
+ if (!pkg) {
641
+ console.log(chalk.yellow(' no package.json found — skipping'));
642
+ return { filesWritten: [] };
643
+ }
644
+ const updated = { ...pkg };
645
+ const devDeps = {
646
+ ...(updated.devDependencies ?? {}),
647
+ };
648
+ if (!devDeps['publint'])
649
+ devDeps['publint'] = '^0.3.0';
650
+ updated.devDependencies = devDeps;
651
+ const scripts = { ...(updated.scripts ?? {}) };
652
+ scripts.publint = 'publint --strict';
653
+ if (scripts.verify && !/\bpublint\b/.test(scripts.verify)) {
654
+ scripts.verify = `${scripts.verify} && pnpm publint`;
655
+ }
656
+ updated.scripts = scripts;
657
+ await fs.writeJson(pkgPath, updated, { spaces: 2 });
658
+ return { filesWritten: ['package.json'] };
659
+ },
660
+ },
661
+ {
662
+ target: 'badges',
663
+ description: 'Add or refresh the status-badge block in README.md (derived from package.json)',
664
+ appliesTo: ['README badges'],
665
+ outputs: ['README.md'],
666
+ riskLevel: 'safe-merge',
667
+ canFixDrift: true,
668
+ async run({ targetDir, pkg }) {
669
+ if (!pkg) {
670
+ console.log(chalk.yellow(' no package.json found — skipping'));
671
+ return { filesWritten: [] };
672
+ }
673
+ const p = pkg;
674
+ const name = typeof p.name === 'string' ? p.name : undefined;
675
+ const parsed = parseRepository(p.repository);
676
+ const block = buildBadgeBlock({
677
+ name,
678
+ owner: parsed?.owner,
679
+ repo: parsed?.repo,
680
+ isPrivate: p.private === true,
681
+ });
682
+ if (!block) {
683
+ console.log(chalk.yellow(' package.json has no name/repository to build badges from — skipping'));
684
+ return { filesWritten: [] };
685
+ }
686
+ const readmePath = path.join(targetDir, 'README.md');
687
+ const existing = (await fs.pathExists(readmePath))
688
+ ? await fs.readFile(readmePath, 'utf8')
689
+ : `# ${name ?? 'project'}\n`;
690
+ await fs.writeFile(readmePath, upsertBadges(existing, block));
691
+ return { filesWritten: ['README.md'] };
692
+ },
693
+ },
694
+ {
695
+ target: 'package-json',
696
+ description: 'Add @rtorcato/repo-tooling to devDependencies',
697
+ appliesTo: ['package.json'],
698
+ outputs: ['package.json (devDependencies)'],
699
+ riskLevel: 'safe-merge',
700
+ canFixDrift: true,
701
+ async run({ targetDir, pkg }) {
702
+ const pkgPath = path.join(targetDir, 'package.json');
703
+ if (!pkg) {
704
+ console.log(chalk.yellow(' no package.json found — skipping'));
705
+ return { filesWritten: [] };
706
+ }
707
+ const updated = { ...pkg };
708
+ const devDeps = {
709
+ ...(updated.devDependencies ?? {}),
710
+ };
711
+ devDeps['@rtorcato/repo-tooling'] = 'latest';
712
+ updated.devDependencies = devDeps;
713
+ await fs.writeJson(pkgPath, updated, { spaces: 2 });
714
+ console.log(chalk.dim(' reminder: run `pnpm install` to install the new dep'));
715
+ return { filesWritten: ['package.json'] };
716
+ },
717
+ },
718
+ {
719
+ target: 'lockfile',
720
+ description: `Scaffold ${LOCKFILE_NAME} recording current tool choices`,
721
+ appliesTo: ['lockfile'],
722
+ outputs: [LOCKFILE_NAME],
723
+ riskLevel: 'safe-add',
724
+ canFixDrift: false,
725
+ async run({ targetDir, pkg }) {
726
+ if (!pkg) {
727
+ console.log(chalk.yellow(' no package.json found — skipping'));
728
+ return { filesWritten: [] };
729
+ }
730
+ const config = inferProjectConfig(pkg);
731
+ await writeLockfile(targetDir, config);
732
+ return { filesWritten: [LOCKFILE_NAME] };
733
+ },
734
+ },
735
+ ];