@rtorcato/repo-tooling 3.19.0 → 3.19.2

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
  /**
@@ -500,7 +500,9 @@ export async function checkClaudeSkills(skillsDir) {
500
500
  // A local fork is `ok` for the same reason a modified copied asset is (#448):
501
501
  // it is somebody's deliberate work, so it is named once and never nagged as
502
502
  // fixable — pointing at a `fix` that would refuse is worse than saying nothing.
503
- 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') {
504
506
  const why = status.contentState === 'modified'
505
507
  ? `has local changes since ${status.installedVersion}`
506
508
  : 'carries no content record, so a fork cannot be told from a stale copy';
@@ -508,7 +510,9 @@ export async function checkClaudeSkills(skillsDir) {
508
510
  check,
509
511
  status: 'ok',
510
512
  detail: `${SHIPPED_SKILL} skill at ${status.file} ${why}; this package ships ${status.shippedVersion} and will not overwrite it`,
511
- 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`,
512
516
  };
513
517
  }
514
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
  }
@@ -129,6 +129,32 @@ 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
+ * POSIX single-quote escaping — the only form correct for arbitrary bytes.
134
+ * Inside single quotes every character is literal, so `'` is the sole one
135
+ * needing care: end the quote, escape it, start a new one. Double quotes are
136
+ * not a substitute; `$(...)`, backticks and `\` all still expand inside them.
137
+ */
138
+ function shellQuote(value) {
139
+ return `'${value.replaceAll("'", "'\\''")}'`;
140
+ }
141
+ /**
142
+ * The command a human runs to see what their fork changed, before deciding
143
+ * whether to take the shipped copy. One builder, because `doctor` and `fix`
144
+ * both print it and two independently-built strings drift (#484). Always
145
+ * `realFile`: through a stow symlink the nominal path is the link, and the
146
+ * bytes worth reading are in the dotfiles checkout it points at.
147
+ *
148
+ * Both paths are user-influenced — `--skills-dir` is an argument (#490) and
149
+ * `realFile` is wherever a symlink happens to point — and this line exists to
150
+ * be pasted into a shell, so it is quoted rather than merely interpolated
151
+ * (#493). Quoting beats refusing to emit a command: single quotes make an odd
152
+ * path *more* visible, not less, and keep the hint usable in the odd case
153
+ * instead of only the ordinary one.
154
+ */
155
+ export function skillDiffCommand(paths) {
156
+ return `diff ${shellQuote(paths.realFile)} ${shellQuote(paths.shippedFile)}`;
157
+ }
132
158
  /** Whether `file` is itself a symlink, as opposed to merely resolving through one. */
133
159
  async function isSymlink(file) {
134
160
  return await fs
@@ -185,6 +211,8 @@ export async function claudeSkillStatus(name = SHIPPED_SKILL, explicit) {
185
211
  const shipped = await readShippedSkill(name);
186
212
  const { dir } = await resolveSkillsDir(explicit);
187
213
  const absent = {
214
+ realFile: null,
215
+ shippedFile: shipped.file,
188
216
  installed: false,
189
217
  installedVersion: null,
190
218
  shippedVersion: shipped.version,
@@ -202,6 +230,8 @@ export async function claudeSkillStatus(name = SHIPPED_SKILL, explicit) {
202
230
  const behind = installedVersion === null || isNewerVersion(shipped.version, installedVersion);
203
231
  return {
204
232
  file,
233
+ realFile: (await isSymlink(file)) ? await fs.realpath(file) : file,
234
+ shippedFile: shipped.file,
205
235
  installed: true,
206
236
  installedVersion,
207
237
  shippedVersion: shipped.version,
@@ -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.19.0",
3
+ "version": "3.19.2",
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",