@rtorcato/js-tooling 2.18.0 → 2.19.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
@@ -17,12 +17,65 @@ Most tooling libraries give you one piece — just TypeScript configs, or just a
17
17
 
18
18
  **[Full documentation →](https://rtorcato.github.io/js-tooling/)**
19
19
 
20
- ## Quick start
20
+ ## Start a new project
21
+
22
+ Interactive wizard — answers every prompt, scaffolds the whole project:
21
23
 
22
24
  ```bash
23
25
  npx @rtorcato/js-tooling setup
24
26
  ```
25
27
 
28
+ Non-interactive — scaffold from a named preset in one shot (CI-friendly):
29
+
30
+ ```bash
31
+ npx @rtorcato/js-tooling setup --preset library -d ./my-lib --skip-install
32
+ # presets: library | web-app | node-api | nextjs-app | react-app
33
+ ```
34
+
35
+ Just one config file? Use `copy`:
36
+
37
+ ```bash
38
+ npx @rtorcato/js-tooling copy biome # → biome.json
39
+ npx @rtorcato/js-tooling copy tsconfig # → tsconfig.json
40
+ npx @rtorcato/js-tooling copy changesets # → .changeset/config.json
41
+ npx @rtorcato/js-tooling copy oxlint # → .oxlintrc.json
42
+ npx @rtorcato/js-tooling copy claude-skill # → .claude/skills/js-tooling.md
43
+ ```
44
+
45
+ **Already have a project?** Don't rerun `setup` — use `doctor` + `fix`:
46
+
47
+ ```bash
48
+ npx @rtorcato/js-tooling doctor # find what's missing or drifted
49
+ npx @rtorcato/js-tooling fix # apply scaffolders, prompting per item
50
+ ```
51
+
52
+ See the [Getting Started guide](https://rtorcato.github.io/js-tooling/guides/getting-started/) for the full walkthrough.
53
+
54
+ ## AI agent rules
55
+
56
+ The package ships rules that teach AI coding agents to drive the CLI
57
+ (`doctor` / `fix` / `setup`) non-interactively. Install for your agent — all
58
+ generated from one source, so guidance never drifts between them:
59
+
60
+ ```bash
61
+ npx @rtorcato/js-tooling fix claude-skill --yes # → .claude/skills/js-tooling.md
62
+ npx @rtorcato/js-tooling fix cursor-rules --yes # → .cursor/rules/js-tooling.mdc
63
+ npx @rtorcato/js-tooling fix copilot-instructions --yes # → .github/copilot-instructions.md
64
+ npx @rtorcato/js-tooling fix agents-md --yes # → AGENTS.md
65
+ ```
66
+
67
+ `copilot-instructions` and `agents-md` upsert a delimited block, so your own
68
+ content in those shared files is never clobbered. Re-running updates the block
69
+ in place on upgrade.
70
+
71
+ Prefer a symlink that auto-syncs the Claude skill on every upgrade?
72
+
73
+ ```bash
74
+ mkdir -p .claude/skills
75
+ ln -sf ../../node_modules/@rtorcato/js-tooling/tooling/claude/js-tooling.md \
76
+ .claude/skills/js-tooling.md
77
+ ```
78
+
26
79
  ## What's new
27
80
 
28
81
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
@@ -716,28 +716,28 @@ async function checkAreTheTypesWrong(_dir, pkg) {
716
716
  ...(pkg?.devDependencies ?? {}),
717
717
  };
718
718
  const scripts = pkg?.scripts ?? {};
719
- const hasDep = !!deps['are-the-types-wrong'];
720
- const hasScript = Object.values(scripts).some((s) => /\battw\b|are-the-types-wrong/.test(s));
719
+ const hasDep = !!deps['@arethetypeswrong/cli'];
720
+ const hasScript = Object.values(scripts).some((s) => /\battw\b/.test(s));
721
721
  if (hasDep && hasScript) {
722
722
  return {
723
723
  check: 'are-the-types-wrong',
724
724
  status: 'ok',
725
- detail: 'are-the-types-wrong installed and wired into a script',
725
+ detail: '@arethetypeswrong/cli installed and wired into a script',
726
726
  };
727
727
  }
728
728
  if (hasDep) {
729
729
  return {
730
730
  check: 'are-the-types-wrong',
731
731
  status: 'drift',
732
- detail: 'are-the-types-wrong installed but no script runs it',
733
- hint: 'Add `"attw": "attw --pack"` to package.json scripts and call it from your verify/CI chain',
732
+ detail: '@arethetypeswrong/cli installed but no script runs it',
733
+ hint: 'Run `npx @rtorcato/js-tooling fix attw` to add an `attw` script and wire it into verify',
734
734
  };
735
735
  }
736
736
  return {
737
737
  check: 'are-the-types-wrong',
738
738
  status: 'optional-missing',
739
- detail: 'are-the-types-wrong not configured',
740
- hint: 'Run `pnpm add -D are-the-types-wrong && attw --pack` to validate TypeScript exports before publishing',
739
+ detail: '@arethetypeswrong/cli not configured',
740
+ hint: 'Run `npx @rtorcato/js-tooling fix attw` to validate TypeScript exports before publishing',
741
741
  };
742
742
  }
743
743
  async function checkTreeshakeSetup(dir, pkg) {
@@ -4,6 +4,7 @@ import chalk from 'chalk';
4
4
  import { createPatch } from 'diff';
5
5
  import fs from 'fs-extra';
6
6
  import inquirer from 'inquirer';
7
+ import { installAgentRules } from '../generators/agent-rules.js';
7
8
  import { generateSemanticReleaseConfig } from '../generators/build.js';
8
9
  import { generateCommitlintConfig, generateHuskyConfig, generatePrePushHook, } from '../generators/git.js';
9
10
  import { generateGitHubActions } from '../generators/github-actions.js';
@@ -59,6 +60,24 @@ async function readPackageJson(dir) {
59
60
  return null;
60
61
  }
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
+ }
62
81
  const FIXERS = [
63
82
  {
64
83
  target: 'biome',
@@ -408,6 +427,86 @@ const FIXERS = [
408
427
  return { filesWritten };
409
428
  },
410
429
  },
430
+ {
431
+ target: 'attw',
432
+ description: 'Install @arethetypeswrong/cli + add an `attw` script (esm-only profile when applicable) and wire it into verify',
433
+ appliesTo: ['are-the-types-wrong'],
434
+ outputs: ['package.json (devDependencies + scripts.attw)'],
435
+ riskLevel: 'safe-merge',
436
+ canFixDrift: true,
437
+ async run({ targetDir, pkg }) {
438
+ const pkgPath = path.join(targetDir, 'package.json');
439
+ if (!pkg) {
440
+ console.log(chalk.yellow(' no package.json found — skipping'));
441
+ return { filesWritten: [] };
442
+ }
443
+ const updated = { ...pkg };
444
+ const devDeps = {
445
+ ...(updated.devDependencies ?? {}),
446
+ };
447
+ if (!devDeps['@arethetypeswrong/cli'])
448
+ devDeps['@arethetypeswrong/cli'] = '^0.18.2';
449
+ updated.devDependencies = devDeps;
450
+ const scripts = { ...(updated.scripts ?? {}) };
451
+ scripts.attw = isEsmOnly(pkg) ? 'attw --pack --profile esm-only' : 'attw --pack';
452
+ if (scripts.verify && !/\battw\b/.test(scripts.verify)) {
453
+ scripts.verify = `${scripts.verify} && pnpm attw`;
454
+ }
455
+ updated.scripts = scripts;
456
+ await fs.writeJson(pkgPath, updated, { spaces: 2 });
457
+ return { filesWritten: ['package.json'] };
458
+ },
459
+ },
460
+ {
461
+ target: 'claude-skill',
462
+ description: 'Install the js-tooling Claude Code skill into .claude/skills/',
463
+ appliesTo: ['Claude skill'],
464
+ outputs: ['.claude/skills/js-tooling.md'],
465
+ riskLevel: 'safe-add',
466
+ canFixDrift: true,
467
+ async run({ targetDir }) {
468
+ const result = await copyPreset('claude-skill', targetDir);
469
+ return { filesWritten: [result.target] };
470
+ },
471
+ },
472
+ {
473
+ target: 'cursor-rules',
474
+ description: 'Install the js-tooling rules for Cursor (.cursor/rules/js-tooling.mdc)',
475
+ appliesTo: ['Cursor rules'],
476
+ outputs: ['.cursor/rules/js-tooling.mdc'],
477
+ riskLevel: 'safe-add',
478
+ canFixDrift: true,
479
+ async run({ targetDir }) {
480
+ const written = await installAgentRules(targetDir, 'cursor');
481
+ return { filesWritten: [written] };
482
+ },
483
+ },
484
+ {
485
+ target: 'copilot-instructions',
486
+ description: 'Install the js-tooling rules for GitHub Copilot (.github/copilot-instructions.md)',
487
+ appliesTo: ['Copilot instructions'],
488
+ outputs: ['.github/copilot-instructions.md'],
489
+ // Upserts a delimited block — never clobbers the consumer's own instructions.
490
+ riskLevel: 'safe-merge',
491
+ canFixDrift: true,
492
+ async run({ targetDir }) {
493
+ const written = await installAgentRules(targetDir, 'copilot');
494
+ return { filesWritten: [written] };
495
+ },
496
+ },
497
+ {
498
+ target: 'agents-md',
499
+ description: 'Install the js-tooling rules into AGENTS.md (universal agent instructions)',
500
+ appliesTo: ['AGENTS.md rules'],
501
+ outputs: ['AGENTS.md'],
502
+ // Upserts a delimited block — never clobbers existing AGENTS.md content.
503
+ riskLevel: 'safe-merge',
504
+ canFixDrift: true,
505
+ async run({ targetDir }) {
506
+ const written = await installAgentRules(targetDir, 'agents-md');
507
+ return { filesWritten: [written] };
508
+ },
509
+ },
411
510
  {
412
511
  target: 'package-json',
413
512
  description: 'Add @rtorcato/js-tooling to devDependencies',
@@ -0,0 +1,67 @@
1
+ import path from 'node:path';
2
+ import fs from 'fs-extra';
3
+ import { getPackageRoot } from '../utils/copy-preset.js';
4
+ /**
5
+ * All agent rule files are generated from one source of truth — the shipped
6
+ * Claude skill — so the guidance never drifts between agents. Only the
7
+ * location and the frontmatter differ per agent.
8
+ */
9
+ const SOURCE = 'tooling/claude/js-tooling.md';
10
+ const BLOCK_START = '<!-- js-tooling:start -->';
11
+ const BLOCK_END = '<!-- js-tooling:end -->';
12
+ /** Read the shipped skill and split its frontmatter from the markdown body. */
13
+ async function readSkill() {
14
+ const raw = await fs.readFile(path.join(getPackageRoot(), SOURCE), 'utf8');
15
+ const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
16
+ if (!match)
17
+ return { description: '', body: raw.trim() };
18
+ const description = (match[1].match(/^description:\s*(.+)$/m)?.[1] ?? '').trim();
19
+ return { description, body: match[2].trim() };
20
+ }
21
+ /**
22
+ * Upsert a delimited js-tooling block into a file that may already hold the
23
+ * consumer's own content (AGENTS.md, copilot-instructions). Replaces an
24
+ * existing block, appends if the file exists without one, creates otherwise.
25
+ * Never clobbers surrounding content.
26
+ */
27
+ async function upsertBlock(filePath, body) {
28
+ const block = `${BLOCK_START}\n${body}\n${BLOCK_END}`;
29
+ if (await fs.pathExists(filePath)) {
30
+ const existing = await fs.readFile(filePath, 'utf8');
31
+ const start = existing.indexOf(BLOCK_START);
32
+ const end = existing.indexOf(BLOCK_END);
33
+ if (start !== -1 && end !== -1) {
34
+ const next = existing.slice(0, start) + block + existing.slice(end + BLOCK_END.length);
35
+ await fs.writeFile(filePath, next);
36
+ return;
37
+ }
38
+ await fs.writeFile(filePath, `${existing.trimEnd()}\n\n${block}\n`);
39
+ return;
40
+ }
41
+ await fs.ensureDir(path.dirname(filePath));
42
+ await fs.writeFile(filePath, `${block}\n`);
43
+ }
44
+ /** Install the js-tooling rules for one agent. Returns the written path (relative). */
45
+ export async function installAgentRules(targetDir, agent) {
46
+ const { description, body } = await readSkill();
47
+ switch (agent) {
48
+ case 'cursor': {
49
+ const rel = path.join('.cursor', 'rules', 'js-tooling.mdc');
50
+ const file = path.join(targetDir, rel);
51
+ const frontmatter = `---\ndescription: ${description}\nglobs:\nalwaysApply: false\n---\n\n`;
52
+ await fs.ensureDir(path.dirname(file));
53
+ await fs.writeFile(file, `${frontmatter}${body}\n`);
54
+ return rel;
55
+ }
56
+ case 'copilot': {
57
+ const rel = path.join('.github', 'copilot-instructions.md');
58
+ await upsertBlock(path.join(targetDir, rel), body);
59
+ return rel;
60
+ }
61
+ case 'agents-md': {
62
+ const rel = 'AGENTS.md';
63
+ await upsertBlock(path.join(targetDir, rel), body);
64
+ return rel;
65
+ }
66
+ }
67
+ }
@@ -21,6 +21,11 @@ export const PRESETS = {
21
21
  target: 'tsconfig.json',
22
22
  desc: 'TypeScript base configuration',
23
23
  },
24
+ 'claude-skill': {
25
+ source: 'tooling/claude/js-tooling.md',
26
+ target: '.claude/skills/js-tooling.md',
27
+ desc: 'Claude Code skill for driving the js-tooling CLI',
28
+ },
24
29
  };
25
30
  export function getPackageRoot() {
26
31
  const cliFile = new URL(import.meta.url).pathname;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/js-tooling",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
4
4
  "description": "JavaScript and TypeScript tooling for Node.js, React, Next.js, and Vitest.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -78,6 +78,7 @@
78
78
  "tooling/typedoc/typedoc.json",
79
79
  "tooling/semantic-release/*.mjs",
80
80
  "tooling/semantic-release/*.d.mts",
81
+ "tooling/claude/*.md",
81
82
  "README.md"
82
83
  ],
83
84
  "exports": {
@@ -199,8 +200,8 @@
199
200
  "@total-typescript/ts-reset": "0.6.1",
200
201
  "@types/fs-extra": "^11.0.4",
201
202
  "@types/node": "^25.9.2",
202
- "@typescript-eslint/eslint-plugin": "^8.46.2",
203
- "@typescript-eslint/parser": "^8.46.2",
203
+ "@typescript-eslint/eslint-plugin": "^8.61.1",
204
+ "@typescript-eslint/parser": "^8.61.1",
204
205
  "@vitejs/plugin-react": "^5.1.0",
205
206
  "@vitest/coverage-v8": "^4.0.3",
206
207
  "commitizen": "^4.3.1",
@@ -208,9 +209,9 @@
208
209
  "cz-conventional-changelog": "^3.3.0",
209
210
  "esbuild": "^0.28.0",
210
211
  "esbuild-node-externals": "^1.22.0",
211
- "eslint": "9.38.0",
212
+ "eslint": "10.5.0",
212
213
  "eslint-plugin-import": "^2.32.0",
213
- "eslint-plugin-jest": "29.0.1",
214
+ "eslint-plugin-jest": "29.15.2",
214
215
  "husky": "^9.1.7",
215
216
  "is-ci": "^4.1.0",
216
217
  "jest": "^29.7.0",
@@ -223,7 +224,7 @@
223
224
  "ts-jest": "^29.4.11",
224
225
  "tsup": "8.5.1",
225
226
  "typescript": "^5.9.3",
226
- "typescript-eslint": "^8.60.0",
227
+ "typescript-eslint": "^8.61.1",
227
228
  "vitest": "^4.1.8"
228
229
  },
229
230
  "peerDependencies": {
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: js-tooling
3
+ description: Use when auditing or fixing TypeScript/JavaScript project tooling in a repo that depends on @rtorcato/js-tooling, or scaffolding a new project with it. Triggers on "audit my tooling", "fix tooling drift", "is my tsconfig/biome/vitest config right", "set up CI/semantic-release/dependabot", "scaffold a TS library/web-app/node-api", "run doctor", "run fix", or "/js-tooling". Drives the `@rtorcato/js-tooling` CLI non-interactively (--json --yes). NOT for hand-editing configs the CLI owns — let the CLI scaffold them so they stay in sync with the presets.
4
+ ---
5
+
6
+ # js-tooling
7
+
8
+ `@rtorcato/js-tooling` is a single-package TS/JS tooling distribution: every preset
9
+ (TypeScript, Biome, ESLint, Prettier, Vitest/Jest, Commitlint, semantic-release,
10
+ tsup/esbuild/Vite/Playwright) plus a CLI to scaffold and audit. Prefer the CLI over
11
+ hand-editing the configs it owns — a manual edit drifts from the preset and `doctor`
12
+ will flag it.
13
+
14
+ Every command takes `--json` and a non-interactive mode; pair with `--yes` for
15
+ autonomous use. `--json` implies `--yes` (a prompt would corrupt the JSON).
16
+
17
+ Run via `npx @rtorcato/js-tooling <cmd>` (or the local `js-tooling` bin if installed).
18
+ `-d <dir>` targets a directory other than cwd.
19
+
20
+ ## The two workflows you'll use most
21
+
22
+ ### Audit → fix → confirm (existing repo)
23
+
24
+ ```bash
25
+ npx @rtorcato/js-tooling doctor --json # findings
26
+ npx @rtorcato/js-tooling fix --yes --json # apply every fixable finding
27
+ npx @rtorcato/js-tooling doctor --json # confirm clean
28
+ ```
29
+
30
+ `doctor` returns `{ directory, results: [{ check, status, detail, hint? }] }`.
31
+ Status is one of:
32
+ - `ok` — configured correctly, nothing to do.
33
+ - `drift` — file exists but doesn't extend our preset. `fix` defaults the overwrite
34
+ prompt to **No**; `--yes` is required to overwrite.
35
+ - `missing` — required and absent → fix it.
36
+ - `optional-missing` — opt-in tool not configured. Only fix if the user wants that tool.
37
+
38
+ `fix` returns `FixActionRecord[]` with `status: applied | dry-run | skipped | already-ok | unsupported`.
39
+
40
+ ### Targeted fix (one concern)
41
+
42
+ ```bash
43
+ npx @rtorcato/js-tooling list --json # enumerate targets
44
+ npx @rtorcato/js-tooling fix <target> --yes --json # e.g. biome, vitest, dependabot, attw
45
+ npx @rtorcato/js-tooling fix <target> --dry-run --json # preview writes
46
+ npx @rtorcato/js-tooling fix <target> --diff # unified diff before confirming
47
+ ```
48
+
49
+ `list --json` is the source of truth for valid targets — read it, don't guess.
50
+
51
+ ## Scaffolding a new project
52
+
53
+ ```bash
54
+ # Quick: from a named preset (library | web-app | node-api | nextjs-app | react-app)
55
+ npx @rtorcato/js-tooling setup --preset library -d ./my-lib --skip-install
56
+
57
+ # Full control: validate a config against the schema, preview, then write
58
+ npx @rtorcato/js-tooling setup --config-schema > project-config.schema.json
59
+ npx @rtorcato/js-tooling setup --config project.json --dry-run # preview file list
60
+ npx @rtorcato/js-tooling setup --config project.json -d ./my-lib --skip-install
61
+ ```
62
+
63
+ ## Drift policy (don't surprise the user)
64
+
65
+ - Safe-merge fixers (`engines`, `husky`, `package-json`) never overwrite — they add/merge.
66
+ - Drift on a config file (`biome`, `tsconfig`, …) is only overwritten with `--yes`.
67
+ Before overwriting drift the user wrote by hand, show `fix <target> --diff` first.
68
+ - `optional-missing` ≠ broken. Don't install opt-in tools (typedoc, size-limit,
69
+ treeshake-check, attw, codeql) unless the user asked for that capability.
70
+
71
+ ## Rules
72
+
73
+ - Let the CLI own its configs. If `doctor` says `drift`, fix via the CLI, don't hand-patch.
74
+ - Use `--json` whenever you'll parse the result; use `--dry-run`/`--diff` before any
75
+ destructive overwrite.
76
+ - After a `fix`, re-run `doctor` to confirm the finding cleared.
77
+
78
+ Full docs: https://rtorcato.github.io/js-tooling/guides/cli/