@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.
- package/AGENTS.md +113 -0
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/cli/commands/doctor.js +336 -0
- package/dist/cli/commands/fix-targets.js +192 -0
- package/dist/cli/commands/fix.js +440 -0
- package/dist/cli/commands/setup-presets.js +281 -0
- package/dist/cli/commands/setup.js +501 -0
- package/dist/cli/generators/agent-rules.js +103 -0
- package/dist/cli/generators/badges.js +88 -0
- package/dist/cli/generators/build.js +216 -0
- package/dist/cli/generators/bun.js +25 -0
- package/dist/cli/generators/community-health.js +145 -0
- package/dist/cli/generators/docs-site.js +436 -0
- package/dist/cli/generators/git.js +164 -0
- package/dist/cli/generators/github-actions.js +35 -0
- package/dist/cli/generators/github-workflows.js +24 -0
- package/dist/cli/generators/gitlab-ci.js +10 -0
- package/dist/cli/generators/index.js +123 -0
- package/dist/cli/generators/linting.js +54 -0
- package/dist/cli/generators/misc.js +298 -0
- package/dist/cli/generators/nx.js +16 -0
- package/dist/cli/generators/package-json.js +317 -0
- package/dist/cli/generators/pnpm-workspace.js +126 -0
- package/dist/cli/generators/postcss.js +24 -0
- package/dist/cli/generators/readme.js +268 -0
- package/dist/cli/generators/security.js +156 -0
- package/dist/cli/generators/skills-install.js +70 -0
- package/dist/cli/generators/tailwind.js +34 -0
- package/dist/cli/generators/testing.js +88 -0
- package/dist/cli/generators/treeshake.js +148 -0
- package/dist/cli/generators/tsconfig.js +27 -0
- package/dist/cli/generators/turborepo.js +35 -0
- package/dist/cli/generators/typedoc.js +40 -0
- package/dist/cli/index.js +332 -0
- package/dist/cli/utils/copy-preset.js +98 -0
- package/dist/cli/utils/detect-language.js +22 -0
- package/dist/cli/utils/format.js +32 -0
- package/dist/cli/utils/install.js +28 -0
- package/dist/cli/utils/lockfile.js +84 -0
- package/dist/languages/js/checks.js +949 -0
- package/dist/languages/js/ci.js +251 -0
- package/dist/languages/js/fixers.js +735 -0
- package/dist/languages/registry.js +37 -0
- package/dist/languages/swift/checks.js +127 -0
- package/dist/languages/swift/ci.js +165 -0
- package/dist/languages/swift/fixers.js +102 -0
- package/dist/languages/swift/git-hooks.js +63 -0
- package/dist/languages/swift/gitignore.js +43 -0
- package/dist/languages/swift/scaffold.js +244 -0
- package/package.json +461 -0
- package/tooling/biome/README.md +90 -0
- package/tooling/biome/biome.json +63 -0
- package/tooling/bun/bunfig.toml +14 -0
- package/tooling/changesets/README.md +35 -0
- package/tooling/changesets/config.json +11 -0
- package/tooling/claude/repo-tooling.md +87 -0
- package/tooling/commitlint/commitlint.d.mts +4 -0
- package/tooling/commitlint/commitlint.mjs +40 -0
- package/tooling/cypress/cypress.config.d.mts +4 -0
- package/tooling/cypress/cypress.config.mjs +11 -0
- package/tooling/docusaurus/index.d.mts +17 -0
- package/tooling/docusaurus/index.mjs +38 -0
- package/tooling/docusaurus/sync-changelog.mjs +42 -0
- package/tooling/docusaurus/theme-tokens.css +79 -0
- package/tooling/docusaurus/theme.css +378 -0
- package/tooling/esbuild/index.d.mts +6 -0
- package/tooling/esbuild/index.mjs +102 -0
- package/tooling/eslint/base.d.mts +6 -0
- package/tooling/eslint/base.mjs +122 -0
- package/tooling/eslint/nextjs.d.mts +4 -0
- package/tooling/eslint/nextjs.mjs +22 -0
- package/tooling/eslint/types.d.ts +58 -0
- package/tooling/github-actions/workflows/cloudflare-pages.yml +42 -0
- package/tooling/github-actions/workflows/docker-publish.yml +44 -0
- package/tooling/github-actions/workflows/preview-deployments.yml +54 -0
- package/tooling/github-actions/workflows/vercel-deploy.yml +40 -0
- package/tooling/jest-presets/browser/jest-preset.d.mts +4 -0
- package/tooling/jest-presets/browser/jest-preset.mjs +14 -0
- package/tooling/jest-presets/node/jest-preset.d.mts +4 -0
- package/tooling/jest-presets/node/jest-preset.mjs +13 -0
- package/tooling/mcp/mcp.json.example +27 -0
- package/tooling/nx/nx.json +24 -0
- package/tooling/oxlint/README.md +25 -0
- package/tooling/oxlint/oxlintrc.json +28 -0
- package/tooling/playwright/playwright.config.d.mts +4 -0
- package/tooling/playwright/playwright.config.mjs +19 -0
- package/tooling/prettier/index.d.mts +4 -0
- package/tooling/prettier/index.mjs +36 -0
- package/tooling/release-please/.release-please-manifest.json +3 -0
- package/tooling/release-please/release-please-config.json +9 -0
- package/tooling/rolldown/rolldown.config.d.mts +18 -0
- package/tooling/rolldown/rolldown.config.mjs +59 -0
- package/tooling/rollup/rollup.config.d.mts +20 -0
- package/tooling/rollup/rollup.config.mjs +70 -0
- package/tooling/semantic-release/docker.d.mts +4 -0
- package/tooling/semantic-release/docker.mjs +59 -0
- package/tooling/semantic-release/github.d.mts +4 -0
- package/tooling/semantic-release/github.mjs +79 -0
- package/tooling/semantic-release/index.d.mts +4 -0
- package/tooling/semantic-release/index.mjs +80 -0
- package/tooling/swift/periphery.yml +4 -0
- package/tooling/swift/swiftlint.yml +23 -0
- package/tooling/tests/exports-resolution.d.mts +14 -0
- package/tooling/tests/exports-resolution.mjs +68 -0
- package/tooling/tests/ssr-safety.d.mts +14 -0
- package/tooling/tests/ssr-safety.mjs +53 -0
- package/tooling/tsup/index.d.mts +8 -0
- package/tooling/tsup/index.mjs +33 -0
- package/tooling/typedoc/typedoc.json +7 -0
- package/tooling/typescript/README.md +49 -0
- package/tooling/typescript/reset.d.ts +9 -0
- package/tooling/typescript/tsconfig.base.json +84 -0
- package/tooling/typescript/tsconfig.build.json +11 -0
- package/tooling/typescript/tsconfig.bun.json +9 -0
- package/tooling/typescript/tsconfig.express.json +9 -0
- package/tooling/typescript/tsconfig.next.json +20 -0
- package/tooling/typescript/tsconfig.node.json +9 -0
- package/tooling/typescript/tsconfig.react.json +15 -0
- package/tooling/typescript/tsconfig.test.json +8 -0
- package/tooling/typescript/v1/tsconfig.base.json +81 -0
- package/tooling/typescript/v1/tsconfig.express.json +9 -0
- package/tooling/typescript/v1/tsconfig.next.json +19 -0
- package/tooling/typescript/v1/tsconfig.node.json +9 -0
- package/tooling/typescript/v1/tsconfig.react.json +14 -0
- package/tooling/typescript/v1/tsconfig.test.json +8 -0
- package/tooling/vite/vite.config.d.mts +4 -0
- package/tooling/vite/vite.config.mjs +18 -0
- package/tooling/vitest/jsdom-shims.d.mts +1 -0
- package/tooling/vitest/jsdom-shims.mjs +58 -0
- package/tooling/vitest/vitest.config.d.mts +4 -0
- package/tooling/vitest/vitest.config.mjs +25 -0
- package/tooling/vitest/vitest.config.react.d.mts +4 -0
- package/tooling/vitest/vitest.config.react.mjs +27 -0
- package/tooling/vitest/vitest.setup.d.mts +1 -0
- package/tooling/vitest/vitest.setup.mjs +3 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import { LANGUAGES } from '../../languages/registry.js';
|
|
6
|
+
import { generateConfigs } from '../generators/index.js';
|
|
7
|
+
import { detectLanguage } from '../utils/detect-language.js';
|
|
8
|
+
import { formatGeneratedFiles } from '../utils/format.js';
|
|
9
|
+
import { installDependencies } from '../utils/install.js';
|
|
10
|
+
import { LOCKFILE_NAME, writeLockfile } from '../utils/lockfile.js';
|
|
11
|
+
import { buildPresetConfig, computeFileList, CONFIG_SCHEMA, PRESET_NAMES, validateProjectConfig, } from './setup-presets.js';
|
|
12
|
+
/** `null` means the run was declined, not that it failed — see promptForConfig. */
|
|
13
|
+
async function resolveConfig(options) {
|
|
14
|
+
if (options.config && options.preset) {
|
|
15
|
+
console.warn(chalk.yellow('⚠️ Both --config and --preset given; --config wins.\n'));
|
|
16
|
+
}
|
|
17
|
+
if (options.config) {
|
|
18
|
+
const configPath = path.resolve(options.config);
|
|
19
|
+
if (!(await fs.pathExists(configPath))) {
|
|
20
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
21
|
+
}
|
|
22
|
+
const raw = await fs.readJson(configPath);
|
|
23
|
+
// Accept the `.repo-tooling.json` lockfile itself as the config source, so a
|
|
24
|
+
// repo needs only one file: the lockfile already embeds the full
|
|
25
|
+
// ProjectConfig under `config`. Unwrap it here rather than requiring a
|
|
26
|
+
// separate hand-authored config file (#271).
|
|
27
|
+
const candidate = typeof raw === 'object' && raw !== null && 'config' in raw && 'version' in raw
|
|
28
|
+
? raw.config
|
|
29
|
+
: raw;
|
|
30
|
+
const { valid, errors } = validateProjectConfig(candidate);
|
|
31
|
+
if (!valid) {
|
|
32
|
+
throw new Error(`Invalid config:\n - ${errors.join('\n - ')}`);
|
|
33
|
+
}
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
if (options.preset) {
|
|
37
|
+
if (!PRESET_NAMES.includes(options.preset)) {
|
|
38
|
+
throw new Error(`Unknown preset: ${options.preset}. Available: ${PRESET_NAMES.join(', ')}`);
|
|
39
|
+
}
|
|
40
|
+
const projectName = path.basename(path.resolve(options.directory));
|
|
41
|
+
return buildPresetConfig(options.preset, projectName);
|
|
42
|
+
}
|
|
43
|
+
return promptForConfig(path.resolve(options.directory));
|
|
44
|
+
}
|
|
45
|
+
export async function setupProject(options) {
|
|
46
|
+
if (options.configSchema) {
|
|
47
|
+
console.log(JSON.stringify(CONFIG_SCHEMA, null, 2));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const targetDir = path.resolve(options.directory);
|
|
51
|
+
const interactive = !options.config && !options.preset;
|
|
52
|
+
const dryRun = options.dryRun === true;
|
|
53
|
+
if (interactive && !dryRun) {
|
|
54
|
+
console.log(chalk.cyan('\n🛠️ Welcome to JS Tooling Setup!\n'));
|
|
55
|
+
console.log(chalk.gray(`Setting up tooling in: ${targetDir}\n`));
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
await fs.ensureDir(targetDir);
|
|
59
|
+
const config = await resolveConfig(options);
|
|
60
|
+
// The language picker bails out for a language whose module hasn't landed
|
|
61
|
+
// yet. It has already explained why — scaffolding a JS project into a Swift
|
|
62
|
+
// repo would be worse than doing nothing.
|
|
63
|
+
if (config === null)
|
|
64
|
+
return;
|
|
65
|
+
if (dryRun) {
|
|
66
|
+
const files = computeFileList(config);
|
|
67
|
+
console.log(JSON.stringify({ directory: targetDir, config, files }, null, 2));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!interactive) {
|
|
71
|
+
const shape = config.language === 'swift' ? `Swift ${config.projectType}` : config.projectType;
|
|
72
|
+
console.log(chalk.cyan(`\n🛠️ Scaffolding ${shape} in ${targetDir}\n`));
|
|
73
|
+
}
|
|
74
|
+
console.log(chalk.cyan('\n📝 Generating configuration files...\n'));
|
|
75
|
+
await generateConfigs(config, targetDir);
|
|
76
|
+
await writeLockfile(targetDir, config);
|
|
77
|
+
// Both steps shell out to pnpm. A Swift package has no dependencies to
|
|
78
|
+
// install and no Biome to format with — SwiftPM resolves on first build,
|
|
79
|
+
// and `swiftlint --fix` is the formatter (it needs a Swift toolchain we
|
|
80
|
+
// can't assume is present).
|
|
81
|
+
if (!options.skipInstall && config.language !== 'swift') {
|
|
82
|
+
console.log(chalk.cyan('\n📦 Installing dependencies...\n'));
|
|
83
|
+
await installDependencies(config, targetDir);
|
|
84
|
+
// Format now that the linter/formatter is installed, so the scaffold
|
|
85
|
+
// passes its own `pnpm check` out of the box (templates are written by
|
|
86
|
+
// hand and don't match biome/prettier whitespace).
|
|
87
|
+
await formatGeneratedFiles(config, targetDir);
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk.green('\n✅ Setup completed successfully!\n'));
|
|
90
|
+
showNextSteps(config, targetDir);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error(chalk.red('\n❌ Setup failed:'), error);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Languages `setup` can scaffold. Deliberately separate from the registry's
|
|
99
|
+
* `supported` flag, which means "has doctor/fix checks" — a language can be
|
|
100
|
+
* auditable before it's scaffoldable (Swift was, between #286 and #288). Both
|
|
101
|
+
* the picker label and the gate below read from here, so the two can't drift.
|
|
102
|
+
*/
|
|
103
|
+
const SCAFFOLDABLE = ['js', 'swift'];
|
|
104
|
+
/**
|
|
105
|
+
* Ask which language this repo is, defaulting to whatever the target dir looks
|
|
106
|
+
* like. Languages setup can't scaffold yet are still offered — hiding them reads
|
|
107
|
+
* as "repo-tooling is JS-only" when the honest answer is "not yet" (#139).
|
|
108
|
+
*/
|
|
109
|
+
async function promptForLanguage(targetDir) {
|
|
110
|
+
const detected = await detectLanguage(targetDir);
|
|
111
|
+
const { language } = await inquirer.prompt([
|
|
112
|
+
{
|
|
113
|
+
type: 'list',
|
|
114
|
+
name: 'language',
|
|
115
|
+
message: '🗣️ What is the primary language of this repo?',
|
|
116
|
+
choices: Object.values(LANGUAGES).map((module) => ({
|
|
117
|
+
name: SCAFFOLDABLE.includes(module.id)
|
|
118
|
+
? module.label
|
|
119
|
+
: `${module.label} (${module.supported ? 'doctor/fix only — no setup preset yet' : 'setup lands with its module'})`,
|
|
120
|
+
value: module.id,
|
|
121
|
+
})),
|
|
122
|
+
// 'unknown' is a bare dir mid-setup — JS is the historical default.
|
|
123
|
+
default: detected === 'unknown' ? 'js' : detected,
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
return LANGUAGES[language];
|
|
127
|
+
}
|
|
128
|
+
function explainNotScaffoldable(module) {
|
|
129
|
+
console.log(chalk.yellow(`\n⚠️ ${module.label} scaffolding isn't available yet.\n`));
|
|
130
|
+
console.log(chalk.gray(module.supported
|
|
131
|
+
? ` doctor and fix already audit ${module.label} repos — both the language-agnostic\n` +
|
|
132
|
+
` checks (CI, CodeQL, Dependabot, GitHub repo settings) and the ${module.label}-specific\n` +
|
|
133
|
+
' ones:\n'
|
|
134
|
+
: ` doctor and fix already cover ${module.label} repos with the language-agnostic\n` +
|
|
135
|
+
' checks (CI, CodeQL, Dependabot, GitHub repo settings):\n'));
|
|
136
|
+
console.log(chalk.gray(' npx @rtorcato/repo-tooling doctor\n'));
|
|
137
|
+
console.log(chalk.gray(` ${module.label} setup presets are tracked at\n` +
|
|
138
|
+
' https://github.com/rtorcato/repo-tooling/issues/139\n'));
|
|
139
|
+
}
|
|
140
|
+
/** `null` when the chosen language has no scaffolding yet; nothing is written. */
|
|
141
|
+
async function promptForConfig(targetDir) {
|
|
142
|
+
const language = await promptForLanguage(targetDir);
|
|
143
|
+
// Gate on SCAFFOLDABLE, not `module.supported` — Swift is supported (#286)
|
|
144
|
+
// but has no preset yet (#288), and falling through here would write a
|
|
145
|
+
// package.json into a Swift repo.
|
|
146
|
+
if (!SCAFFOLDABLE.includes(language.id)) {
|
|
147
|
+
explainNotScaffoldable(language);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
// Swift has exactly one shape to scaffold, so there is nothing to ask beyond
|
|
151
|
+
// the name: SwiftLint, Periphery and `swift test` are the standard, not
|
|
152
|
+
// options (see the `swift-library` preset). A wizard whose every question has
|
|
153
|
+
// one answer is worse than no wizard.
|
|
154
|
+
if (language.id === 'swift') {
|
|
155
|
+
const { projectName } = await inquirer.prompt([
|
|
156
|
+
{
|
|
157
|
+
type: 'input',
|
|
158
|
+
name: 'projectName',
|
|
159
|
+
message: '📦 What is your package name?',
|
|
160
|
+
default: path.basename(targetDir),
|
|
161
|
+
validate: (input) => input.trim().length > 0 || 'Package name is required',
|
|
162
|
+
},
|
|
163
|
+
]);
|
|
164
|
+
return buildPresetConfig('swift-library', projectName);
|
|
165
|
+
}
|
|
166
|
+
// Narrowing for the question list below, which is JS-shaped throughout.
|
|
167
|
+
if (language.id !== 'js')
|
|
168
|
+
return null;
|
|
169
|
+
// Turborepo only makes sense in a pnpm-workspace monorepo, so the prompt is
|
|
170
|
+
// only offered when one is already present in the target dir.
|
|
171
|
+
const hasWorkspace = await fs.pathExists(path.join(targetDir, 'pnpm-workspace.yaml'));
|
|
172
|
+
const answers = await inquirer.prompt([
|
|
173
|
+
{
|
|
174
|
+
type: 'input',
|
|
175
|
+
name: 'projectName',
|
|
176
|
+
message: '📦 What is your project name?',
|
|
177
|
+
default: path.basename(process.cwd()),
|
|
178
|
+
validate: (input) => input.trim().length > 0 || 'Project name is required',
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: 'list',
|
|
182
|
+
name: 'projectType',
|
|
183
|
+
message: '🏗️ What type of project are you building?',
|
|
184
|
+
choices: [
|
|
185
|
+
{ name: '📚 Library/Package', value: 'library' },
|
|
186
|
+
{ name: '🌐 Web Application', value: 'web-app' },
|
|
187
|
+
{ name: '🚀 Node.js API', value: 'node-api' },
|
|
188
|
+
{ name: '⚡ Next.js App', value: 'nextjs-app' },
|
|
189
|
+
{ name: '⚛️ React App', value: 'react-app' },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
type: 'confirm',
|
|
194
|
+
name: 'useTypeScript',
|
|
195
|
+
message: '📘 Do you want to use TypeScript?',
|
|
196
|
+
default: true,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
type: 'list',
|
|
200
|
+
name: 'tsConfig',
|
|
201
|
+
message: '⚙️ Which TypeScript configuration?',
|
|
202
|
+
choices: (answers) => {
|
|
203
|
+
const baseChoices = [
|
|
204
|
+
{ name: '🔧 Base (General purpose)', value: 'base' },
|
|
205
|
+
{ name: '🖥️ Node.js', value: 'node' },
|
|
206
|
+
];
|
|
207
|
+
if (answers.projectType === 'nextjs-app') {
|
|
208
|
+
return [{ name: '⚡ Next.js', value: 'next' }, ...baseChoices];
|
|
209
|
+
}
|
|
210
|
+
if (answers.projectType === 'react-app' || answers.projectType === 'web-app') {
|
|
211
|
+
return [{ name: '⚛️ React', value: 'react' }, ...baseChoices];
|
|
212
|
+
}
|
|
213
|
+
if (answers.projectType === 'node-api') {
|
|
214
|
+
return [
|
|
215
|
+
{ name: '🚀 Express/API', value: 'express' },
|
|
216
|
+
{ name: '🖥️ Node.js', value: 'node' },
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
return baseChoices;
|
|
220
|
+
},
|
|
221
|
+
when: (answers) => answers.useTypeScript,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
type: 'list',
|
|
225
|
+
name: 'lintingTool',
|
|
226
|
+
message: '🔍 Which linting/formatting tool?',
|
|
227
|
+
choices: [
|
|
228
|
+
{ name: '⚡ Biome (Fast, all-in-one)', value: 'biome' },
|
|
229
|
+
{ name: '🔧 ESLint (Configurable)', value: 'eslint' },
|
|
230
|
+
{ name: '🔥 Both Biome + ESLint', value: 'both' },
|
|
231
|
+
{ name: '❌ None', value: 'none' },
|
|
232
|
+
],
|
|
233
|
+
default: 'biome',
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
type: 'list',
|
|
237
|
+
name: 'eslintConfig',
|
|
238
|
+
message: '🔧 Which ESLint configuration?',
|
|
239
|
+
choices: (answers) => {
|
|
240
|
+
const choices = [{ name: '🔧 Base configuration', value: 'base' }];
|
|
241
|
+
if (answers.projectType === 'nextjs-app') {
|
|
242
|
+
choices.unshift({ name: '⚡ Next.js configuration', value: 'nextjs' });
|
|
243
|
+
}
|
|
244
|
+
return choices;
|
|
245
|
+
},
|
|
246
|
+
when: (answers) => answers.lintingTool === 'eslint' || answers.lintingTool === 'both',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
type: 'confirm',
|
|
250
|
+
name: 'oxlint',
|
|
251
|
+
message: '🦀 Also run Oxlint alongside (50–100× faster than ESLint)?',
|
|
252
|
+
default: false,
|
|
253
|
+
when: (answers) => answers.lintingTool !== 'none',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
type: 'list',
|
|
257
|
+
name: 'testingFramework',
|
|
258
|
+
message: '🧪 Which testing framework?',
|
|
259
|
+
choices: [
|
|
260
|
+
{ name: '⚡ Vitest (Fast, Vite-powered)', value: 'vitest' },
|
|
261
|
+
{ name: '🃏 Jest (Traditional)', value: 'jest' },
|
|
262
|
+
{ name: '🎭 Playwright (E2E)', value: 'playwright' },
|
|
263
|
+
{ name: '🌲 Cypress (E2E)', value: 'cypress' },
|
|
264
|
+
{ name: '❌ None', value: 'none' },
|
|
265
|
+
],
|
|
266
|
+
default: 'vitest',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
type: 'list',
|
|
270
|
+
name: 'testEnvironment',
|
|
271
|
+
message: '🌍 Test environment?',
|
|
272
|
+
choices: [
|
|
273
|
+
{ name: '🖥️ Node.js', value: 'node' },
|
|
274
|
+
{ name: '🌐 Browser (DOM)', value: 'browser' },
|
|
275
|
+
{ name: '🔄 Both', value: 'both' },
|
|
276
|
+
],
|
|
277
|
+
when: (answers) => answers.testingFramework === 'jest',
|
|
278
|
+
default: 'node',
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
type: 'confirm',
|
|
282
|
+
name: 'gitHooks',
|
|
283
|
+
message: '🪝 Set up Git hooks (Husky + lint-staged)?',
|
|
284
|
+
default: true,
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
type: 'confirm',
|
|
288
|
+
name: 'commitLint',
|
|
289
|
+
message: '📝 Set up conventional commit linting?',
|
|
290
|
+
default: true,
|
|
291
|
+
when: (answers) => answers.gitHooks,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
type: 'list',
|
|
295
|
+
name: 'releaseTool',
|
|
296
|
+
message: '🚀 Automated release tool?',
|
|
297
|
+
choices: [
|
|
298
|
+
{ name: '📦 semantic-release (commit-message-driven)', value: 'semantic-release' },
|
|
299
|
+
{ name: '📝 Changesets (changeset-file-driven, monorepo-friendly)', value: 'changesets' },
|
|
300
|
+
{ name: '🙏 Release Please (Google, release-PR-driven)', value: 'release-please' },
|
|
301
|
+
{ name: '❌ None', value: 'none' },
|
|
302
|
+
],
|
|
303
|
+
default: 'semantic-release',
|
|
304
|
+
when: (answers) => answers.projectType === 'library',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
type: 'confirm',
|
|
308
|
+
name: 'treeshakeCheck',
|
|
309
|
+
message: '🌳 Add a tree-shake verification check (apps/treeshake-check)?',
|
|
310
|
+
default: false,
|
|
311
|
+
when: (answers) => answers.projectType === 'library',
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
type: 'confirm',
|
|
315
|
+
name: 'publint',
|
|
316
|
+
message: '📦 Add publint to lint your package before publishing?',
|
|
317
|
+
default: true,
|
|
318
|
+
when: (answers) => answers.projectType === 'library',
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
type: 'confirm',
|
|
322
|
+
name: 'badges',
|
|
323
|
+
message: '🔖 Add status badges (CI, npm, coverage, license) to the README?',
|
|
324
|
+
default: true,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
type: 'confirm',
|
|
328
|
+
name: 'securityAutomation',
|
|
329
|
+
message: '🛡️ Include security automation (Dependabot + CodeQL)?',
|
|
330
|
+
default: true,
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
type: 'confirm',
|
|
334
|
+
name: 'aiSetup',
|
|
335
|
+
message: '🤖 Add AI agent rules (AGENTS.md, CLAUDE.md, Cursor, Copilot, Claude skill)?',
|
|
336
|
+
default: true,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
type: 'list',
|
|
340
|
+
name: 'orchestrator',
|
|
341
|
+
message: '🚀 Monorepo task orchestrator?',
|
|
342
|
+
choices: [
|
|
343
|
+
{ name: '⚡ Turborepo (turbo.json)', value: 'turbo' },
|
|
344
|
+
{ name: '🔷 Nx (nx.json)', value: 'nx' },
|
|
345
|
+
{ name: '❌ None', value: 'none' },
|
|
346
|
+
],
|
|
347
|
+
default: 'turbo',
|
|
348
|
+
when: () => hasWorkspace,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
type: 'confirm',
|
|
352
|
+
name: 'tailwind',
|
|
353
|
+
message: '🎨 Add Tailwind CSS v4 (PostCSS plugin + globals.css)?',
|
|
354
|
+
default: true,
|
|
355
|
+
// Only meaningful for frontend project types.
|
|
356
|
+
when: (answers) => answers.projectType === 'web-app' ||
|
|
357
|
+
answers.projectType === 'react-app' ||
|
|
358
|
+
answers.projectType === 'nextjs-app',
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
type: 'list',
|
|
362
|
+
name: 'bundler',
|
|
363
|
+
message: '📦 Which bundler/build tool?',
|
|
364
|
+
choices: (answers) => {
|
|
365
|
+
const choices = [
|
|
366
|
+
{ name: '📦 tsup (TypeScript packages)', value: 'tsup' },
|
|
367
|
+
{ name: '⚡ esbuild (Fast bundling)', value: 'esbuild' },
|
|
368
|
+
{ name: '🍣 Rollup (library bundler)', value: 'rollup' },
|
|
369
|
+
{ name: '🦀 Rolldown (Rust, Rollup-compatible)', value: 'rolldown' },
|
|
370
|
+
{ name: '❌ None', value: 'none' },
|
|
371
|
+
];
|
|
372
|
+
if (answers.projectType === 'web-app' || answers.projectType === 'react-app') {
|
|
373
|
+
choices.unshift({ name: '⚡ Vite (Modern web apps)', value: 'vite' });
|
|
374
|
+
}
|
|
375
|
+
return choices;
|
|
376
|
+
},
|
|
377
|
+
when: (answers) => answers.projectType !== 'nextjs-app', // Next.js has its own bundler
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
type: 'confirm',
|
|
381
|
+
name: 'bun',
|
|
382
|
+
message: '🥟 Target the Bun runtime (bunfig.toml + Bun-typed tsconfig)?',
|
|
383
|
+
default: false,
|
|
384
|
+
// A runtime flag, not a project type — offer it for Node-compatible
|
|
385
|
+
// library/API targets, not the browser-framework types.
|
|
386
|
+
when: (answers) => answers.projectType === 'library' || answers.projectType === 'node-api',
|
|
387
|
+
},
|
|
388
|
+
]);
|
|
389
|
+
return {
|
|
390
|
+
projectName: answers.projectName,
|
|
391
|
+
// The real answer from the language question above, not a hardcoded 'js'
|
|
392
|
+
// (#284) — the lockfile is what doctor/fix gate on later.
|
|
393
|
+
language: language.id,
|
|
394
|
+
projectType: answers.projectType,
|
|
395
|
+
typescript: {
|
|
396
|
+
enabled: answers.useTypeScript || false,
|
|
397
|
+
config: answers.tsConfig || 'base',
|
|
398
|
+
},
|
|
399
|
+
linting: {
|
|
400
|
+
tool: answers.lintingTool || 'none',
|
|
401
|
+
eslintConfig: answers.eslintConfig,
|
|
402
|
+
},
|
|
403
|
+
formatting: {
|
|
404
|
+
tool: answers.lintingTool === 'biome' || answers.lintingTool === 'both'
|
|
405
|
+
? 'biome'
|
|
406
|
+
: answers.lintingTool === 'eslint'
|
|
407
|
+
? 'prettier'
|
|
408
|
+
: 'none',
|
|
409
|
+
},
|
|
410
|
+
testing: {
|
|
411
|
+
framework: answers.testingFramework || 'none',
|
|
412
|
+
environment: answers.testEnvironment,
|
|
413
|
+
},
|
|
414
|
+
gitHooks: answers.gitHooks || false,
|
|
415
|
+
commitLint: answers.commitLint || false,
|
|
416
|
+
semanticRelease: answers.releaseTool === 'semantic-release',
|
|
417
|
+
changesets: answers.releaseTool === 'changesets',
|
|
418
|
+
releasePlease: answers.releaseTool === 'release-please',
|
|
419
|
+
oxlint: answers.oxlint || false,
|
|
420
|
+
securityAutomation: answers.securityAutomation ?? false,
|
|
421
|
+
bundler: answers.bundler || 'none',
|
|
422
|
+
treeshakeCheck: answers.treeshakeCheck || false,
|
|
423
|
+
publint: answers.publint ?? false,
|
|
424
|
+
badges: answers.badges ?? false,
|
|
425
|
+
aiSetup: answers.aiSetup ?? false,
|
|
426
|
+
turborepo: answers.orchestrator === 'turbo',
|
|
427
|
+
nx: answers.orchestrator === 'nx',
|
|
428
|
+
tailwind: answers.tailwind ?? false,
|
|
429
|
+
bun: answers.bun ?? false,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function showNextSteps(config, _targetDir) {
|
|
433
|
+
console.log(chalk.bold('\n📋 Next Steps:\n'));
|
|
434
|
+
const steps = [];
|
|
435
|
+
// Every step in the else-branch names a pnpm script. Swift's equivalents are
|
|
436
|
+
// SwiftPM and SwiftLint commands — and neither tool ships with the scaffold,
|
|
437
|
+
// so the last line is how to get them.
|
|
438
|
+
if (config.language === 'swift') {
|
|
439
|
+
steps.push('🏗️ Build with: swift build', '🧪 Run tests with: swift test', '🔍 Lint and format with: swiftlint --fix (CI runs swiftlint lint --strict)', '🧹 Find dead code with: periphery scan', '📦 Install both tools with: brew install swiftlint periphery');
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
if (config.typescript.enabled) {
|
|
443
|
+
steps.push('🔧 Customize your tsconfig.json as needed');
|
|
444
|
+
}
|
|
445
|
+
if (config.linting.tool !== 'none') {
|
|
446
|
+
steps.push(`🔍 Run linting with: ${config.linting.tool === 'biome' ? 'pnpm biome check .' : 'pnpm eslint .'}`);
|
|
447
|
+
}
|
|
448
|
+
if (config.testing.framework !== 'none') {
|
|
449
|
+
steps.push(`🧪 Run tests with: pnpm ${config.testing.framework}`);
|
|
450
|
+
}
|
|
451
|
+
if (config.gitHooks) {
|
|
452
|
+
steps.push('🪝 Commit your changes to test the git hooks');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
steps.push(`🔒 ${LOCKFILE_NAME} records your setup choices — doctor uses it to suppress intentional opt-outs`);
|
|
456
|
+
steps.push('📖 Check the generated README.md for more details');
|
|
457
|
+
steps.forEach((step, index) => {
|
|
458
|
+
console.log(` ${index + 1}. ${step}`);
|
|
459
|
+
});
|
|
460
|
+
const skipped = collectSkippedFixSuggestions(config);
|
|
461
|
+
if (skipped.length > 0) {
|
|
462
|
+
console.log(chalk.bold('\n💡 Want to add something you skipped?\n'));
|
|
463
|
+
for (const s of skipped) {
|
|
464
|
+
console.log(` ${chalk.gray('-')} ${s}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
console.log(chalk.dim('\n📁 All configuration files have been generated in your project directory.'));
|
|
468
|
+
console.log(chalk.dim(' You can modify them to suit your specific needs.\n'));
|
|
469
|
+
}
|
|
470
|
+
function collectSkippedFixSuggestions(config) {
|
|
471
|
+
const suggestions = [];
|
|
472
|
+
// Every suggestion below is a JS fix target (husky, biome, vitest…). On a
|
|
473
|
+
// Swift repo they'd all be wrong — `fix` doesn't even offer them there — so
|
|
474
|
+
// the Swift scaffold gets only the doctor line at the end.
|
|
475
|
+
if (config.language === 'swift') {
|
|
476
|
+
return ['Run `npx @rtorcato/repo-tooling doctor` any time to audit drift'];
|
|
477
|
+
}
|
|
478
|
+
if (!config.gitHooks) {
|
|
479
|
+
suggestions.push('Run `npx @rtorcato/repo-tooling fix husky` to add git hooks later');
|
|
480
|
+
}
|
|
481
|
+
if (!config.commitLint) {
|
|
482
|
+
suggestions.push('Run `npx @rtorcato/repo-tooling fix commitlint` to add conventional-commit linting');
|
|
483
|
+
}
|
|
484
|
+
if (!config.semanticRelease && config.projectType === 'library') {
|
|
485
|
+
suggestions.push('Run `npx @rtorcato/repo-tooling fix semantic-release` to add automated releases');
|
|
486
|
+
}
|
|
487
|
+
if (!config.securityAutomation) {
|
|
488
|
+
suggestions.push('Run `npx @rtorcato/repo-tooling fix dependabot` and `fix codeql` for security automation');
|
|
489
|
+
}
|
|
490
|
+
if (config.linting.tool === 'none') {
|
|
491
|
+
suggestions.push('Run `npx @rtorcato/repo-tooling fix biome` or `fix eslint` to add linting');
|
|
492
|
+
}
|
|
493
|
+
if (config.testing.framework === 'none') {
|
|
494
|
+
suggestions.push('Run `npx @rtorcato/repo-tooling fix vitest` to add a test runner');
|
|
495
|
+
}
|
|
496
|
+
if (config.projectType === 'library' && !config.treeshakeCheck) {
|
|
497
|
+
suggestions.push('Run `npx @rtorcato/repo-tooling fix treeshake-check` to add an esbuild-based tree-shake assertion');
|
|
498
|
+
}
|
|
499
|
+
suggestions.push('Run `npx @rtorcato/repo-tooling doctor` any time to audit drift');
|
|
500
|
+
return suggestions;
|
|
501
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { copyPreset, getPackageRoot } from '../utils/copy-preset.js';
|
|
4
|
+
import { installSkillsInstallDocs } from './skills-install.js';
|
|
5
|
+
/**
|
|
6
|
+
* All agent rule files are generated from one source of truth — the shipped
|
|
7
|
+
* Claude skill — so the guidance never drifts between agents. Only the
|
|
8
|
+
* location and the frontmatter differ per agent.
|
|
9
|
+
*/
|
|
10
|
+
const SOURCE = 'tooling/claude/repo-tooling.md';
|
|
11
|
+
const BLOCK_START = '<!-- js-tooling:start -->';
|
|
12
|
+
const BLOCK_END = '<!-- js-tooling:end -->';
|
|
13
|
+
/** Read the shipped skill and split its frontmatter from the markdown body. */
|
|
14
|
+
async function readSkill() {
|
|
15
|
+
const raw = await fs.readFile(path.join(getPackageRoot(), SOURCE), 'utf8');
|
|
16
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
17
|
+
if (!match)
|
|
18
|
+
return { description: '', body: raw.trim() };
|
|
19
|
+
const description = ((match[1] ?? '').match(/^description:\s*(.+)$/m)?.[1] ?? '').trim();
|
|
20
|
+
return { description, body: (match[2] ?? '').trim() };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Upsert a delimited js-tooling block into a file that may already hold the
|
|
24
|
+
* consumer's own content (AGENTS.md, copilot-instructions). Replaces an
|
|
25
|
+
* existing block, appends if the file exists without one, creates otherwise.
|
|
26
|
+
* Never clobbers surrounding content.
|
|
27
|
+
*/
|
|
28
|
+
export async function upsertBlock(filePath, body, markers = { start: BLOCK_START, end: BLOCK_END }) {
|
|
29
|
+
const { start: startMarker, end: endMarker } = markers;
|
|
30
|
+
const block = `${startMarker}\n${body}\n${endMarker}`;
|
|
31
|
+
if (await fs.pathExists(filePath)) {
|
|
32
|
+
const existing = await fs.readFile(filePath, 'utf8');
|
|
33
|
+
const start = existing.indexOf(startMarker);
|
|
34
|
+
const end = existing.indexOf(endMarker);
|
|
35
|
+
if (start !== -1 && end !== -1) {
|
|
36
|
+
const next = existing.slice(0, start) + block + existing.slice(end + endMarker.length);
|
|
37
|
+
await fs.writeFile(filePath, next);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
await fs.writeFile(filePath, `${existing.trimEnd()}\n\n${block}\n`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
44
|
+
await fs.writeFile(filePath, `${block}\n`);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Write CLAUDE.md as a thin pointer to AGENTS.md rather than duplicating the
|
|
48
|
+
* guidance — Claude Code reads both, and `@AGENTS.md` keeps a single source of
|
|
49
|
+
* truth. Merge-safe: only the delimited block is touched.
|
|
50
|
+
*/
|
|
51
|
+
export async function installClaudeMd(targetDir) {
|
|
52
|
+
const rel = 'CLAUDE.md';
|
|
53
|
+
const body = 'See @AGENTS.md for project and agent guidance (kept in sync by `repo-tooling fix ai`).';
|
|
54
|
+
await upsertBlock(path.join(targetDir, rel), body);
|
|
55
|
+
return rel;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Umbrella: install every AI agent file in one pass — AGENTS.md, the CLAUDE.md
|
|
59
|
+
* pointer, Cursor + Copilot rules, the Claude skill, and the commented MCP
|
|
60
|
+
* template. All are merge-safe or `.example`, so re-running is idempotent.
|
|
61
|
+
* Returns the written paths (relative to targetDir).
|
|
62
|
+
*/
|
|
63
|
+
export async function installAiSetup(targetDir) {
|
|
64
|
+
const written = [];
|
|
65
|
+
written.push(await installAgentRules(targetDir, 'agents-md'));
|
|
66
|
+
written.push(await installClaudeMd(targetDir));
|
|
67
|
+
written.push(await installAgentRules(targetDir, 'cursor'));
|
|
68
|
+
written.push(await installAgentRules(targetDir, 'copilot'));
|
|
69
|
+
written.push((await copyPreset('claude-skill', targetDir)).target);
|
|
70
|
+
written.push((await copyPreset('mcp-example', targetDir)).target);
|
|
71
|
+
// If this repo ships its own skills (skills/<name>/SKILL.md), document their
|
|
72
|
+
// one-command `npx skills add` install in README.md. No-op for repos without.
|
|
73
|
+
const skillsDoc = await installSkillsInstallDocs(targetDir);
|
|
74
|
+
if (skillsDoc)
|
|
75
|
+
written.push(skillsDoc);
|
|
76
|
+
return written;
|
|
77
|
+
}
|
|
78
|
+
/** Install the repo-tooling rules for one agent. Returns the written path (relative). */
|
|
79
|
+
export async function installAgentRules(targetDir, agent) {
|
|
80
|
+
const { description, body } = await readSkill();
|
|
81
|
+
switch (agent) {
|
|
82
|
+
case 'cursor': {
|
|
83
|
+
const rel = path.join('.cursor', 'rules', 'repo-tooling.mdc');
|
|
84
|
+
const file = path.join(targetDir, rel);
|
|
85
|
+
const frontmatter = `---\ndescription: ${description}\nglobs:\nalwaysApply: false\n---\n\n`;
|
|
86
|
+
await fs.ensureDir(path.dirname(file));
|
|
87
|
+
await fs.writeFile(file, `${frontmatter}${body}\n`);
|
|
88
|
+
// Remove the pre-rename rule so a re-run migrates it instead of leaving both.
|
|
89
|
+
await fs.remove(path.join(targetDir, '.cursor', 'rules', 'js-tooling.mdc'));
|
|
90
|
+
return rel;
|
|
91
|
+
}
|
|
92
|
+
case 'copilot': {
|
|
93
|
+
const rel = path.join('.github', 'copilot-instructions.md');
|
|
94
|
+
await upsertBlock(path.join(targetDir, rel), body);
|
|
95
|
+
return rel;
|
|
96
|
+
}
|
|
97
|
+
case 'agents-md': {
|
|
98
|
+
const rel = 'AGENTS.md';
|
|
99
|
+
await upsertBlock(path.join(targetDir, rel), body);
|
|
100
|
+
return rel;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|