@rtorcato/repo-tooling 3.18.0 → 3.19.1

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.
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import fs from 'fs-extra';
3
3
  import { BLOCK_START, hasStaleAgentBlock } from '../cli/generators/agent-rules.js';
4
4
  import { BADGE_START, hasPublicOnlyBadges } from '../cli/generators/badges.js';
5
- import { claudeSkillStatus, SHIPPED_SKILL } from '../cli/generators/claude-skills.js';
5
+ import { claudeSkillStatus, SHIPPED_SKILL, skillDiffCommand, } from '../cli/generators/claude-skills.js';
6
6
  import { DEPENDABOT_CONFIG_PATHS, dependabotIgnoreRules } from '../cli/generators/security.js';
7
7
  import { detectNestedLanguages } from '../cli/utils/detect-language.js';
8
8
  /**
@@ -469,11 +469,16 @@ export async function checkAiSetup(dir) {
469
469
  * is not a defect in this repo, and either of those statuses would fail its CI
470
470
  * over a file CI has never seen. `optional-missing` is the honest verdict — an
471
471
  * opt-in workflow tool that isn't configured — and it leaves the exit code alone.
472
+ *
473
+ * `skillsDir` is doctor's `--skills-dir`. Without it this reported against
474
+ * `~/.claude/skills` whatever directory the repo actually installs into, so a
475
+ * consumer who passes the flag to `fix` saw a permanent false `optional-missing`
476
+ * for a skill they have (#485).
472
477
  */
473
- export async function checkClaudeSkills() {
478
+ export async function checkClaudeSkills(skillsDir) {
474
479
  const check = 'Claude skills';
475
480
  const hint = `Run \`npx @rtorcato/repo-tooling fix claude-skills\` to install the ${SHIPPED_SKILL} skill (writes outside the repo; opt-in, so \`fix\` alone skips it)`;
476
- const status = await claudeSkillStatus();
481
+ const status = await claudeSkillStatus(SHIPPED_SKILL, skillsDir);
477
482
  if (!status.installed) {
478
483
  return {
479
484
  check,
@@ -495,7 +500,9 @@ export async function checkClaudeSkills() {
495
500
  // A local fork is `ok` for the same reason a modified copied asset is (#448):
496
501
  // it is somebody's deliberate work, so it is named once and never nagged as
497
502
  // fixable — pointing at a `fix` that would refuse is worse than saying nothing.
498
- if (status.contentState && status.contentState !== 'pristine') {
503
+ // `realFile` is set whenever `contentState` is — both mean something is
504
+ // installed. The extra test is TypeScript's, not a real condition.
505
+ if (status.realFile && status.contentState && status.contentState !== 'pristine') {
499
506
  const why = status.contentState === 'modified'
500
507
  ? `has local changes since ${status.installedVersion}`
501
508
  : 'carries no content record, so a fork cannot be told from a stale copy';
@@ -503,7 +510,9 @@ export async function checkClaudeSkills() {
503
510
  check,
504
511
  status: 'ok',
505
512
  detail: `${SHIPPED_SKILL} skill at ${status.file} ${why}; this package ships ${status.shippedVersion} and will not overwrite it`,
506
- hint: `Diff it against the shipped copy, then run \`npx @rtorcato/repo-tooling fix claude-skills --force-skills\` to take the shipped version`,
513
+ // Name both paths: "diff it against the shipped copy" left the reader
514
+ // with nothing to diff, in the one case they most want to look (#484).
515
+ hint: `Diff it against the shipped copy — \`${skillDiffCommand({ realFile: status.realFile, shippedFile: status.shippedFile })}\` — then run \`npx @rtorcato/repo-tooling fix claude-skills --force-skills\` to take the shipped version`,
507
516
  };
508
517
  }
509
518
  return {
@@ -16,7 +16,7 @@ import path from 'node:path';
16
16
  import chalk from 'chalk';
17
17
  import inquirer from 'inquirer';
18
18
  import { installAgentRules, installAiSetup } from '../cli/generators/agent-rules.js';
19
- import { installClaudeSkill, resolveSkillsDir, SHIPPED_SKILL, } from '../cli/generators/claude-skills.js';
19
+ import { installClaudeSkill, resolveSkillsDir, SHIPPED_SKILL, skillDiffCommand, } from '../cli/generators/claude-skills.js';
20
20
  import { generateBrand } from '../cli/generators/brand.js';
21
21
  import { generateCommunityHealth } from '../cli/generators/community-health.js';
22
22
  import { generateCommitlintConfig } from '../cli/generators/git.js';
@@ -89,7 +89,7 @@ function describeSkillFork(result) {
89
89
  return [
90
90
  `skipped — ${SHIPPED_SKILL} was not overwritten with ${result.shippedVersion}: ${why}`,
91
91
  ` ${target}`,
92
- ` compare: diff "${result.realFile}" "${result.shippedFile}"`,
92
+ ` compare: ${skillDiffCommand(result)}`,
93
93
  ' overwrite anyway: fix claude-skills --force-skills',
94
94
  ];
95
95
  }
@@ -179,12 +179,12 @@ async function runBaseChecks(dir, lock, opts) {
179
179
  results.push(await checkBrand(dir));
180
180
  results.push(await checkAiSetup(dir));
181
181
  // User-global, not repo state — see checkClaudeSkills on why it never returns drift.
182
- results.push(await checkClaudeSkills());
182
+ results.push(await checkClaudeSkills(opts.skillsDir));
183
183
  results.push(await checkReadmeBadges(dir, opts.badges.audience, opts.badges.fixTarget));
184
184
  results.push(await checkCoverageUpload(dir));
185
185
  return results;
186
186
  }
187
- export async function runDoctor(dir) {
187
+ export async function runDoctor(dir, skillsDir) {
188
188
  const targetDir = path.resolve(dir);
189
189
  const lock = await readLockfile(targetDir);
190
190
  // Per-module dispatch (#285): the base checks (repo hygiene, CI, security,
@@ -212,6 +212,7 @@ export async function runDoctor(dir) {
212
212
  presetWorkflow: null,
213
213
  language,
214
214
  codeqlLanguages: languageModule.codeqlLanguages,
215
+ skillsDir,
215
216
  })),
216
217
  ];
217
218
  return demoteDeclined(results, lock);
@@ -234,6 +235,7 @@ export async function runDoctor(dir) {
234
235
  presetWorkflow: renderSwiftWorkflow(await readSwiftPackage(targetDir)),
235
236
  language,
236
237
  codeqlLanguages: languageModule.codeqlLanguages,
238
+ skillsDir,
237
239
  })),
238
240
  ...(await runSwiftChecks(targetDir)),
239
241
  ];
@@ -257,6 +259,7 @@ export async function runDoctor(dir) {
257
259
  presetWorkflow: renderPythonWorkflow(await readPyproject(targetDir)),
258
260
  language,
259
261
  codeqlLanguages: languageModule.codeqlLanguages,
262
+ skillsDir,
260
263
  })),
261
264
  ...(await runPythonChecks(targetDir)),
262
265
  ];
@@ -282,6 +285,7 @@ export async function runDoctor(dir) {
282
285
  presetWorkflow: renderPerlWorkflow(await readPerlProject(targetDir)),
283
286
  language,
284
287
  codeqlLanguages: languageModule.codeqlLanguages,
288
+ skillsDir,
285
289
  })),
286
290
  ...(await runPerlChecks(targetDir)),
287
291
  ];
@@ -342,6 +346,7 @@ export async function runDoctor(dir) {
342
346
  presetWorkflow: renderGitHubWorkflow(githubJobs(inferProjectConfig(pkg), { scripts: scriptsOf(pkg) })),
343
347
  language,
344
348
  codeqlLanguages: languageModule.codeqlLanguages,
349
+ skillsDir,
345
350
  })));
346
351
  return demoteDeclined(results, lock);
347
352
  }
@@ -397,7 +402,7 @@ export function summarize(results) {
397
402
  }
398
403
  export async function doctorCommand(options = {}) {
399
404
  const dir = options.directory ?? process.cwd();
400
- const results = await runDoctor(dir);
405
+ const results = await runDoctor(dir, options.skillsDir);
401
406
  if (options.json) {
402
407
  console.log(JSON.stringify({ directory: path.resolve(dir), results }, null, 2));
403
408
  }
@@ -333,7 +333,9 @@ export async function fixCommand(target, options = {}) {
333
333
  const pkg = await readPackageJson(targetDir);
334
334
  const lock = await readLockfile(targetDir);
335
335
  const fixers = fixersForLanguage(await detectLanguage(targetDir));
336
- const results = await runDoctor(targetDir);
336
+ // Same --skills-dir the claude-skills fixer writes to, so the diagnosis fix
337
+ // acts on and the install it performs agree on one directory (#485).
338
+ const results = await runDoctor(targetDir, options.skillsDir);
337
339
  const actions = [];
338
340
  const noteLockConflict = (check) => {
339
341
  if (!lock)
@@ -129,6 +129,16 @@ export async function readShippedSkill(name = SHIPPED_SKILL) {
129
129
  const pkg = await fs.readJson(path.join(root, 'package.json'));
130
130
  return { content, version: String(pkg.version), file };
131
131
  }
132
+ /**
133
+ * The command a human runs to see what their fork changed, before deciding
134
+ * whether to take the shipped copy. One builder, because `doctor` and `fix`
135
+ * both print it and two independently-built strings drift (#484). Always
136
+ * `realFile`: through a stow symlink the nominal path is the link, and the
137
+ * bytes worth reading are in the dotfiles checkout it points at.
138
+ */
139
+ export function skillDiffCommand(paths) {
140
+ return `diff "${paths.realFile}" "${paths.shippedFile}"`;
141
+ }
132
142
  /** Whether `file` is itself a symlink, as opposed to merely resolving through one. */
133
143
  async function isSymlink(file) {
134
144
  return await fs
@@ -185,6 +195,8 @@ export async function claudeSkillStatus(name = SHIPPED_SKILL, explicit) {
185
195
  const shipped = await readShippedSkill(name);
186
196
  const { dir } = await resolveSkillsDir(explicit);
187
197
  const absent = {
198
+ realFile: null,
199
+ shippedFile: shipped.file,
188
200
  installed: false,
189
201
  installedVersion: null,
190
202
  shippedVersion: shipped.version,
@@ -202,6 +214,8 @@ export async function claudeSkillStatus(name = SHIPPED_SKILL, explicit) {
202
214
  const behind = installedVersion === null || isNewerVersion(shipped.version, installedVersion);
203
215
  return {
204
216
  file,
217
+ realFile: (await isSymlink(file)) ? await fs.realpath(file) : file,
218
+ shippedFile: shipped.file,
205
219
  installed: true,
206
220
  installedVersion,
207
221
  shippedVersion: shipped.version,
package/dist/cli/index.js CHANGED
@@ -317,6 +317,9 @@ program
317
317
  .description('🩺 Diagnose project alignment with @rtorcato/repo-tooling presets')
318
318
  .option('-d, --directory <path>', 'Target directory to diagnose', process.cwd())
319
319
  .option('--json', 'Emit machine-readable JSON output')
320
+ // The read side of `fix --skills-dir` (#485). No --yes/--json requirement
321
+ // here: doctor never writes, so an unresolved directory is just reported.
322
+ .option('--skills-dir <path>', 'Where `fix claude-skills` installs user-global agent skills (default: ~/.claude/skills). Pass the same path `fix` was given, or the skill reports as not installed')
320
323
  .action(doctorCommand);
321
324
  program
322
325
  .command('fix [target]')
@@ -4,7 +4,9 @@ import fs from 'fs-extra';
4
4
  import { recordAssetHash } from './lockfile.js';
5
5
  export const PRESETS = {
6
6
  biome: {
7
- source: 'tooling/biome/biome.json',
7
+ // Not named `biome.json`: Biome auto-discovers any file by that name, so
8
+ // shipping one inside this package made it a second root config here (#486).
9
+ source: 'tooling/biome/preset.json',
8
10
  target: 'biome.json',
9
11
  desc: 'Biome formatter and linter configuration',
10
12
  },
@@ -84,7 +84,7 @@ function matchesBiomeConfig(contents) {
84
84
  if (BIOME_PRESET_REF.test(JSON.stringify(config.extends ?? '')))
85
85
  return true;
86
86
  // The inlined preset: biome's own `$schema` plus the `linter.rules.preset`
87
- // key the shipped `tooling/biome/biome.json` sets.
87
+ // key the shipped `tooling/biome/preset.json` sets.
88
88
  const schema = config.$schema;
89
89
  return (typeof schema === 'string' &&
90
90
  schema.includes('biomejs.dev/schemas/') &&
@@ -1373,7 +1373,7 @@ function readSchemaUrl(contents) {
1373
1373
  * of which version the config targets, so comparing it against the declared
1374
1374
  * dependency floor is exact — no heuristics, no false positives.
1375
1375
  *
1376
- * This is the #330 defect precisely: `tooling/biome/biome.json` carries
1376
+ * This is the #330 defect precisely: `tooling/biome/preset.json` carries
1377
1377
  * `$schema` 2.5.0 and uses `linter.rules.preset`, a 2.5 key, while
1378
1378
  * `peerDependencies` advertised `@biomejs/biome: ^2.0.0`. Consumers on 2.0–2.4
1379
1379
  * got `Found an unknown key \`preset\`` with nothing pointing at the range.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.18.0",
3
+ "version": "3.19.1",
4
4
  "description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -90,7 +90,7 @@
90
90
  "tooling/docusaurus/docs-helpers.mjs",
91
91
  "tooling/docusaurus/theme-tokens.css",
92
92
  "tooling/docusaurus/theme.css",
93
- "tooling/biome/biome.json",
93
+ "tooling/biome/preset.json",
94
94
  "tooling/bun/bunfig.toml",
95
95
  "tooling/nx/nx.json",
96
96
  "tooling/changesets/config.json",
@@ -201,7 +201,7 @@
201
201
  "types": "./tooling/docusaurus/index.d.mts",
202
202
  "import": "./tooling/docusaurus/index.mjs"
203
203
  },
204
- "./biome": "./tooling/biome/biome.json",
204
+ "./biome": "./tooling/biome/preset.json",
205
205
  "./nx": "./tooling/nx/nx.json",
206
206
  "./changesets": "./tooling/changesets/config.json",
207
207
  "./release-please": "./tooling/release-please/release-please-config.json",
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "$schema": "https://biomejs.dev/schemas/latest/schema.json",
3
- "root": false,
4
3
  "vcs": {
5
4
  "enabled": true,
6
5
  "clientKind": "git",