@rtorcato/js-tooling 2.4.0 → 2.5.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/README.md CHANGED
@@ -9,7 +9,7 @@ JavaScript and TypeScript tooling for Node.js, React, Next.js, and Vitest.
9
9
  [![Coverage](https://codecov.io/gh/rtorcato/js-tooling/branch/main/graph/badge.svg)](https://codecov.io/gh/rtorcato/js-tooling)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
11
 
12
- Most tooling libraries give you one piece — just TypeScript configs, or just an ESLint preset. **js-tooling** covers the entire lifecycle: TypeScript, Biome/ESLint, Vitest/Jest, Commitlint, Husky, Semantic Release, and GitHub Actions CI — all wired together. The interactive `setup` wizard scaffolds everything in one shot; `doctor` checks an existing project for drift.
12
+ Most tooling libraries give you one piece — just TypeScript configs, or just an ESLint preset. **js-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.
13
13
 
14
14
  **[Full documentation →](https://rtorcato.github.io/js-tooling/)**
15
15
 
@@ -23,6 +23,8 @@ npx @rtorcato/js-tooling setup
23
23
 
24
24
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
25
25
 
26
+ **v2.4.0** — New `fix` command applies scaffolders for items `doctor` flags, with `--yes` and `--dry-run` flags. Drift never auto-overwrites — every existing file you'd lose is confirmed first. Doctor grew checks for `engines.node`, `.editorconfig`, `.nvmrc`, Husky, `lint-staged`, semantic-release, knip, GitHub Actions, GitLab CI, Dependabot, and CodeQL — plus a `Next steps:` footer that names the exact `fix` command to run for each finding. Setup wizard adds a "Include security automation?" prompt for Dependabot + CodeQL.
27
+
26
28
  **v2.0.0** — All 39 tool packages moved from `dependencies` to `peerDependencies`. Add them to your own `devDependencies`. Also ships: `doctor` subcommand, generator unit tests, Dependabot, CI matrix (Node 22 + 24).
27
29
 
28
30
  **v1.1.0** — Stricter commitlint limits, fix for CLI path resolution when copying configs.
@@ -269,16 +269,18 @@ function logTargets() {
269
269
  console.log(` ${chalk.green('●')} ${chalk.bold(f.target)}: ${chalk.gray(f.description)}`);
270
270
  }
271
271
  }
272
- async function applyFixer(fixer, result, targetDir, pkg, dryRun) {
272
+ async function applyFixer(fixer, result, targetDir, pkg, dryRun, silent) {
273
273
  if (dryRun) {
274
- console.log(chalk.cyan(` [dry-run] would write: ${fixer.outputs.join(', ')}`));
275
- return { applied: true, written: [] };
274
+ if (!silent) {
275
+ console.log(chalk.cyan(` [dry-run] would write: ${fixer.outputs.join(', ')}`));
276
+ }
277
+ return { filesWritten: [], dryRun: true };
276
278
  }
277
279
  const { filesWritten } = await fixer.run({ targetDir, pkg, result });
278
- if (filesWritten.length > 0) {
280
+ if (!silent && filesWritten.length > 0) {
279
281
  console.log(chalk.green(` ✅ wrote ${filesWritten.join(', ')}`));
280
282
  }
281
- return { applied: true, written: filesWritten };
283
+ return { filesWritten, dryRun: false };
282
284
  }
283
285
  async function confirmApply(fixer, result, assumeYes) {
284
286
  if (assumeYes)
@@ -292,15 +294,35 @@ async function confirmApply(fixer, result, assumeYes) {
292
294
  ]);
293
295
  return confirm === true;
294
296
  }
297
+ function recordFor(target, check, doctorStatus, status, filesWritten) {
298
+ return { target, check, status, doctorStatus, filesWritten };
299
+ }
295
300
  export async function fixCommand(target, options = {}) {
296
301
  const targetDir = path.resolve(options.directory ?? process.cwd());
297
- const assumeYes = options.yes === true;
298
302
  const dryRun = options.dryRun === true;
303
+ const json = options.json === true;
304
+ // JSON mode implies --yes so prompts don't corrupt the output stream.
305
+ const assumeYes = options.yes === true || json;
306
+ const silent = json;
299
307
  const pkg = await readPackageJson(targetDir);
300
308
  const results = await runDoctor(targetDir);
309
+ const actions = [];
310
+ const emitJson = (resolvedTarget) => {
311
+ const payload = { directory: targetDir, target: resolvedTarget, actions };
312
+ console.log(JSON.stringify(payload, null, 2));
313
+ };
301
314
  if (target) {
302
315
  const fixer = findFixer(target);
303
316
  if (!fixer) {
317
+ if (json) {
318
+ console.log(JSON.stringify({
319
+ directory: targetDir,
320
+ error: 'unknown-target',
321
+ target,
322
+ available: FIXERS.map((f) => f.target),
323
+ }, null, 2));
324
+ process.exit(1);
325
+ }
304
326
  console.error(chalk.red(`\n❌ Unknown fix target: ${target}\n`));
305
327
  logTargets();
306
328
  console.log();
@@ -309,46 +331,69 @@ export async function fixCommand(target, options = {}) {
309
331
  const result = results.find((r) => fixer.appliesTo.includes(r.check)) ??
310
332
  { check: fixer.appliesTo[0] ?? fixer.target, status: 'missing', detail: '' };
311
333
  if (result.status === 'ok') {
334
+ actions.push(recordFor(fixer.target, result.check, 'ok', 'already-ok', []));
335
+ if (json)
336
+ return emitJson(fixer.target);
312
337
  console.log(chalk.green(`\n✅ ${result.check} is already configured\n`));
313
338
  return;
314
339
  }
315
- console.log(chalk.cyan(`\n🔧 ${fixer.target} — ${chalk.bold(result.check)} is ${result.status}\n`));
340
+ if (!silent) {
341
+ console.log(chalk.cyan(`\n🔧 ${fixer.target} — ${chalk.bold(result.check)} is ${result.status}\n`));
342
+ }
316
343
  const ok = await confirmApply(fixer, result, assumeYes);
317
344
  if (!ok) {
345
+ actions.push(recordFor(fixer.target, result.check, result.status, 'skipped', []));
346
+ if (json)
347
+ return emitJson(fixer.target);
318
348
  console.log(chalk.gray(' skipped\n'));
319
349
  return;
320
350
  }
321
- await applyFixer(fixer, result, targetDir, pkg, dryRun);
351
+ const outcome = await applyFixer(fixer, result, targetDir, pkg, dryRun, silent);
352
+ actions.push(recordFor(fixer.target, result.check, result.status, outcome.dryRun ? 'dry-run' : 'applied', outcome.filesWritten));
353
+ if (json)
354
+ return emitJson(fixer.target);
322
355
  console.log();
323
356
  return;
324
357
  }
325
- // No target — walk all non-ok results
326
358
  const fixable = results.filter((r) => r.status !== 'ok');
327
359
  if (fixable.length === 0) {
360
+ if (json)
361
+ return emitJson(null);
328
362
  console.log(chalk.green('\n✅ All checks pass — nothing to fix\n'));
329
363
  return;
330
364
  }
331
- console.log(chalk.cyan(`\n🔧 ${fixable.length} item(s) to address\n`));
332
- let applied = 0;
333
- let skipped = 0;
334
- let unsupported = 0;
365
+ if (!silent) {
366
+ console.log(chalk.cyan(`\n🔧 ${fixable.length} item(s) to address\n`));
367
+ }
368
+ let appliedCount = 0;
369
+ let skippedCount = 0;
370
+ let unsupportedCount = 0;
335
371
  for (const result of fixable) {
336
372
  const fixer = findFixerForCheck(result.check);
337
373
  if (!fixer) {
338
- console.log(chalk.gray(` — ${result.check}: no fixer registered`));
339
- unsupported++;
374
+ actions.push(recordFor(null, result.check, result.status, 'unsupported', []));
375
+ if (!silent)
376
+ console.log(chalk.gray(` — ${result.check}: no fixer registered`));
377
+ unsupportedCount++;
340
378
  continue;
341
379
  }
342
- console.log(` ${chalk.bold(result.check)} (${result.status}) → ${fixer.target}`);
380
+ if (!silent) {
381
+ console.log(` ${chalk.bold(result.check)} (${result.status}) → ${fixer.target}`);
382
+ }
343
383
  const ok = await confirmApply(fixer, result, assumeYes);
344
384
  if (!ok) {
345
- console.log(chalk.gray(' skipped'));
346
- skipped++;
385
+ actions.push(recordFor(fixer.target, result.check, result.status, 'skipped', []));
386
+ if (!silent)
387
+ console.log(chalk.gray(' skipped'));
388
+ skippedCount++;
347
389
  continue;
348
390
  }
349
- await applyFixer(fixer, result, targetDir, pkg, dryRun);
350
- applied++;
391
+ const outcome = await applyFixer(fixer, result, targetDir, pkg, dryRun, silent);
392
+ actions.push(recordFor(fixer.target, result.check, result.status, outcome.dryRun ? 'dry-run' : 'applied', outcome.filesWritten));
393
+ appliedCount++;
351
394
  }
395
+ if (json)
396
+ return emitJson(null);
352
397
  console.log();
353
- console.log(` Summary: ${chalk.green(`${applied} applied`)}, ${chalk.gray(`${skipped} skipped`)}, ${chalk.yellow(`${unsupported} unsupported`)}\n`);
398
+ console.log(` Summary: ${chalk.green(`${appliedCount} applied`)}, ${chalk.gray(`${skippedCount} skipped`)}, ${chalk.yellow(`${unsupportedCount} unsupported`)}\n`);
354
399
  }
@@ -220,6 +220,36 @@ function showNextSteps(config, _targetDir) {
220
220
  steps.forEach((step, index) => {
221
221
  console.log(` ${index + 1}. ${step}`);
222
222
  });
223
- console.log(chalk.dim('\n💡 All configuration files have been generated in your project directory.'));
223
+ const skipped = collectSkippedFixSuggestions(config);
224
+ if (skipped.length > 0) {
225
+ console.log(chalk.bold('\n💡 Want to add something you skipped?\n'));
226
+ for (const s of skipped) {
227
+ console.log(` ${chalk.gray('-')} ${s}`);
228
+ }
229
+ }
230
+ console.log(chalk.dim('\n📁 All configuration files have been generated in your project directory.'));
224
231
  console.log(chalk.dim(' You can modify them to suit your specific needs.\n'));
225
232
  }
233
+ function collectSkippedFixSuggestions(config) {
234
+ const suggestions = [];
235
+ if (!config.gitHooks) {
236
+ suggestions.push('Run `npx @rtorcato/js-tooling fix husky` to add git hooks later');
237
+ }
238
+ if (!config.commitLint) {
239
+ suggestions.push('Run `npx @rtorcato/js-tooling fix commitlint` to add conventional-commit linting');
240
+ }
241
+ if (!config.semanticRelease && config.projectType === 'library') {
242
+ suggestions.push('Run `npx @rtorcato/js-tooling fix semantic-release` to add automated releases');
243
+ }
244
+ if (!config.securityAutomation) {
245
+ suggestions.push('Run `npx @rtorcato/js-tooling fix dependabot` and `fix codeql` for security automation');
246
+ }
247
+ if (config.linting.tool === 'none') {
248
+ suggestions.push('Run `npx @rtorcato/js-tooling fix biome` or `fix eslint` to add linting');
249
+ }
250
+ if (config.testing.framework === 'none') {
251
+ suggestions.push('Run `npx @rtorcato/js-tooling fix vitest` to add a test runner');
252
+ }
253
+ suggestions.push('Run `npx @rtorcato/js-tooling doctor` any time to audit drift');
254
+ return suggestions;
255
+ }
package/dist/cli/index.js CHANGED
@@ -74,14 +74,21 @@ program
74
74
  { name: 'Playwright', desc: 'End-to-end testing configuration' },
75
75
  { name: 'Commitlint', desc: 'Conventional commit linting' },
76
76
  { name: 'Husky', desc: 'Git hooks for pre-commit validation' },
77
+ { name: 'lint-staged', desc: 'Run linters on staged files (pairs with Husky)' },
77
78
  { name: 'Semantic Release', desc: 'Automated versioning and publishing' },
78
79
  { name: 'tsup', desc: 'TypeScript bundler configuration' },
79
80
  { name: 'esbuild', desc: 'Fast JavaScript bundler configuration' },
81
+ { name: 'EditorConfig', desc: 'Cross-editor formatting consistency (.editorconfig)' },
82
+ { name: '.nvmrc', desc: 'Pin Node version per repository' },
83
+ { name: 'knip', desc: 'Find unused files, exports, and dependencies' },
84
+ { name: 'Dependabot', desc: 'Weekly automated dependency updates' },
85
+ { name: 'CodeQL', desc: 'GitHub security scanning workflow' },
80
86
  ];
81
87
  configs.forEach(({ name, desc }) => {
82
88
  console.log(` ${chalk.green('●')} ${chalk.bold(name)}: ${chalk.gray(desc)}`);
83
89
  });
84
- console.log(chalk.dim('\n💡 Run "js-tooling setup" to configure your project\n'));
90
+ console.log(chalk.dim('\n💡 Run `js-tooling setup` for a new project'));
91
+ console.log(chalk.dim(' or `js-tooling fix` to apply missing pieces to an existing one\n'));
85
92
  });
86
93
  program
87
94
  .command('doctor')
@@ -95,10 +102,12 @@ program
95
102
  .option('-d, --directory <path>', 'Target directory', process.cwd())
96
103
  .option('--yes', 'Assume yes to all prompts (including drift overwrites)')
97
104
  .option('--dry-run', 'Print what would change without writing files')
105
+ .option('--json', 'Emit machine-readable JSON output (implies --yes)')
98
106
  .action((target, options) => fixCommand(target, {
99
107
  directory: options.directory,
100
108
  yes: options.yes,
101
109
  dryRun: options.dryRun,
110
+ json: options.json,
102
111
  }));
103
112
  program.hook('preAction', async (_, actionCommand) => {
104
113
  const name = actionCommand.name();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/js-tooling",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "JavaScript and TypeScript tooling for Node.js, React, Next.js, and Vitest.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -1,3 +0,0 @@
1
- # `@turbo/eslint-config`
2
-
3
- Collection of internal eslint configurations.