@rtorcato/js-tooling 2.11.0 → 2.12.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.
@@ -630,6 +630,23 @@ function checkLockfile(lock) {
630
630
  detail: `.js-tooling.json v${lock.version} (written by ${lock.writtenBy})`,
631
631
  };
632
632
  }
633
+ async function checkCodeowners(dir) {
634
+ for (const candidate of ['CODEOWNERS', '.github/CODEOWNERS', 'docs/CODEOWNERS']) {
635
+ if (await fs.pathExists(path.join(dir, candidate))) {
636
+ return {
637
+ check: 'CODEOWNERS',
638
+ status: 'ok',
639
+ detail: `${candidate} found`,
640
+ };
641
+ }
642
+ }
643
+ return {
644
+ check: 'CODEOWNERS',
645
+ status: 'optional-missing',
646
+ detail: 'no CODEOWNERS file',
647
+ hint: 'Run `npx @rtorcato/js-tooling fix codeowners` to scaffold .github/CODEOWNERS',
648
+ };
649
+ }
633
650
  async function checkGitLabCI(dir) {
634
651
  for (const candidate of ['.gitlab-ci.yml', '.gitlab-ci.yaml']) {
635
652
  if (await fs.pathExists(path.join(dir, candidate))) {
@@ -672,6 +689,7 @@ export async function runDoctor(dir) {
672
689
  results.push(await checkDependabot(targetDir));
673
690
  results.push(await checkCodeQL(targetDir));
674
691
  results.push(await checkGitLabCI(targetDir));
692
+ results.push(await checkCodeowners(targetDir));
675
693
  results.push(await checkTreeshakeSetup(targetDir, pkg));
676
694
  // Lockfile-driven demotion: if the lock records an intentional opt-out for a
677
695
  // check that's currently optional-missing, demote it to ok with a clear detail.
@@ -20,6 +20,7 @@ export const FIX_TARGETS = {
20
20
  'GitHub Actions': 'github-actions',
21
21
  Dependabot: 'dependabot',
22
22
  CodeQL: 'codeql',
23
+ CODEOWNERS: 'codeowners',
23
24
  lockfile: 'lockfile',
24
25
  '.js-tooling.json': 'lockfile',
25
26
  };
@@ -6,7 +6,7 @@ import { generateSemanticReleaseConfig } from '../generators/build.js';
6
6
  import { generateCommitlintConfig, generateHuskyConfig, generatePrePushHook, } from '../generators/git.js';
7
7
  import { generateGitHubActions } from '../generators/github-actions.js';
8
8
  import { generateESLintConfig, generatePrettierConfig } from '../generators/linting.js';
9
- import { ensureEnginesNode, generateEditorConfig, generateKnipConfig, generateNvmrc, generateSizeLimitConfig, } from '../generators/misc.js';
9
+ import { ensureEnginesNode, generateCodeowners, generateEditorConfig, generateKnipConfig, generateNvmrc, generateSizeLimitConfig, } from '../generators/misc.js';
10
10
  import { composeVerifyScriptFromPkg } from '../generators/package-json.js';
11
11
  import { generateCodeQLWorkflow, generateDependabotConfig } from '../generators/security.js';
12
12
  import { generateVitestConfig } from '../generators/testing.js';
@@ -229,6 +229,18 @@ const FIXERS = [
229
229
  return { filesWritten: ['.github/workflows/codeql.yml'] };
230
230
  },
231
231
  },
232
+ {
233
+ target: 'codeowners',
234
+ description: 'Scaffold .github/CODEOWNERS with commented examples',
235
+ appliesTo: ['CODEOWNERS'],
236
+ outputs: ['.github/CODEOWNERS'],
237
+ riskLevel: 'safe-add',
238
+ canFixDrift: false,
239
+ async run({ targetDir }) {
240
+ const written = await generateCodeowners(targetDir);
241
+ return { filesWritten: [written] };
242
+ },
243
+ },
232
244
  {
233
245
  target: 'editorconfig',
234
246
  description: 'Scaffold .editorconfig (UTF-8, LF, tab indent)',
@@ -380,6 +392,16 @@ function logTargets() {
380
392
  console.log(` ${chalk.green('ā—')} ${chalk.bold(f.target)}: ${chalk.gray(f.description)}`);
381
393
  }
382
394
  }
395
+ export function listFixers() {
396
+ return FIXERS.map((f) => ({
397
+ target: f.target,
398
+ description: f.description,
399
+ appliesTo: f.appliesTo,
400
+ outputs: f.outputs,
401
+ riskLevel: f.riskLevel ?? 'destructive',
402
+ canFixDrift: f.canFixDrift ?? false,
403
+ }));
404
+ }
383
405
  async function applyFixer(fixer, result, targetDir, pkg, lock, dryRun, silent) {
384
406
  if (dryRun) {
385
407
  if (!silent) {
@@ -442,6 +464,21 @@ export async function fixCommand(target, options = {}) {
442
464
  // JSON mode implies --yes so prompts don't corrupt the output stream.
443
465
  const assumeYes = options.yes === true || json;
444
466
  const silent = json;
467
+ if (options.list) {
468
+ const summary = listFixers();
469
+ if (json) {
470
+ console.log(JSON.stringify({ targets: summary }, null, 2));
471
+ return;
472
+ }
473
+ console.log(chalk.cyan('\nšŸ”§ Registered fix targets:\n'));
474
+ for (const f of summary) {
475
+ console.log(` ${chalk.green('ā—')} ${chalk.bold(f.target)}`);
476
+ console.log(` ${chalk.gray(f.description)}`);
477
+ console.log(` ${chalk.dim(`risk=${f.riskLevel}, drift=${f.canFixDrift ? 'yes' : 'no'}, outputs=${f.outputs.join(', ')}`)}`);
478
+ }
479
+ console.log();
480
+ return;
481
+ }
445
482
  const pkg = await readPackageJson(targetDir);
446
483
  const lock = await readLockfile(targetDir);
447
484
  const results = await runDoctor(targetDir);
@@ -30,6 +30,20 @@ const SIZE_LIMIT_CONFIG = [
30
30
  limit: '10 kB',
31
31
  },
32
32
  ];
33
+ const CODEOWNERS_CONTENT = `# .github/CODEOWNERS
34
+ # Each line is a file pattern followed by one or more owners.
35
+ # Owners can be GitHub usernames (@user) or team names (@org/team).
36
+ # Order matters — the last matching pattern wins.
37
+ # See https://docs.github.com/articles/about-code-owners/
38
+ #
39
+ # Examples:
40
+ # * @your-username
41
+ # /src/api/ @backend-team
42
+ # /docs/ @docs-team @your-username
43
+ # *.md @docs-team
44
+
45
+ * @TODO-owner
46
+ `;
33
47
  export async function generateEditorConfig(targetDir) {
34
48
  await fs.writeFile(path.join(targetDir, '.editorconfig'), EDITORCONFIG_CONTENT);
35
49
  }
@@ -54,6 +68,12 @@ export async function generateKnipConfig(targetDir) {
54
68
  export async function generateSizeLimitConfig(targetDir) {
55
69
  await fs.writeJson(path.join(targetDir, '.size-limit.json'), SIZE_LIMIT_CONFIG, { spaces: 2 });
56
70
  }
71
+ export async function generateCodeowners(targetDir) {
72
+ const filepath = path.join(targetDir, '.github', 'CODEOWNERS');
73
+ await fs.ensureDir(path.dirname(filepath));
74
+ await fs.writeFile(filepath, CODEOWNERS_CONTENT);
75
+ return '.github/CODEOWNERS';
76
+ }
57
77
  export async function generateMiscBaseline(targetDir) {
58
78
  await generateEditorConfig(targetDir);
59
79
  await generateNvmrc(targetDir);
package/dist/cli/index.js CHANGED
@@ -221,15 +221,20 @@ program
221
221
  .option('--yes', 'Assume yes to all prompts (including drift overwrites)')
222
222
  .option('--dry-run', 'Print what would change without writing files')
223
223
  .option('--json', 'Emit machine-readable JSON output (implies --yes)')
224
+ .option('--list', 'List all registered fix targets and exit')
224
225
  .action((target, options) => fixCommand(target, {
225
226
  directory: options.directory,
226
227
  yes: options.yes,
227
228
  dryRun: options.dryRun,
228
229
  json: options.json,
230
+ list: options.list,
229
231
  }));
230
232
  program.hook('preAction', async (_, actionCommand) => {
231
233
  const name = actionCommand.name();
232
234
  if (name === 'setup' || name === 'doctor' || name === 'fix') {
235
+ // `fix --list` is read-only and safe to run anywhere, including this repo.
236
+ if (name === 'fix' && actionCommand.opts().list)
237
+ return;
233
238
  const dir = actionCommand.opts().directory ?? process.cwd();
234
239
  if (await isSelfRepo(dir)) {
235
240
  console.log(chalk.yellow('\nāš ļø This command cannot be run inside the @rtorcato/js-tooling repo itself.\n'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/js-tooling",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "JavaScript and TypeScript tooling for Node.js, React, Next.js, and Vitest.",
5
5
  "type": "module",
6
6
  "keywords": [