@rtorcato/repo-tooling 3.12.1 → 3.13.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.
- package/README.md +1 -1
- package/dist/cli/commands/doctor.js +5 -2
- package/dist/cli/generators/linting.js +19 -9
- package/dist/cli/utils/copy-preset.js +5 -0
- package/dist/languages/js/checks.js +62 -11
- package/package.json +2 -1
- package/skills/ai-issue-loop/SKILL.md +10 -0
- package/skills/npm-publish/SKILL.md +69 -21
- package/tooling/claude/sync-agents.mjs +47 -0
package/README.md
CHANGED
|
@@ -175,7 +175,7 @@ ln -sf ../../node_modules/@rtorcato/repo-tooling/tooling/claude/repo-tooling.md
|
|
|
175
175
|
|
|
176
176
|
This repo is also a self-hosted Claude Code marketplace. Install the plugin to
|
|
177
177
|
get three skills — `repo-tooling` (adopt/audit the presets via the CLI),
|
|
178
|
-
`npm-publish` (
|
|
178
|
+
`npm-publish` (never hand-cut a release) and `ai-issue-loop` (the label-driven
|
|
179
179
|
issue → PR pipeline) — in any session:
|
|
180
180
|
|
|
181
181
|
```
|
|
@@ -18,7 +18,7 @@ import { checkGitIdentity } from '../../base/git-identity.js';
|
|
|
18
18
|
import { LOCKFILE_VERSION, readLockfile } from '../utils/lockfile.js';
|
|
19
19
|
import { declinedInLock, getFixTargetForCheck } from './fix-targets.js';
|
|
20
20
|
import { checkAiSetup, checkBrand, checkCodeowners, checkClaudeSkills, checkCodeQL, checkCommunityHealth, checkCoverageUpload, checkDependabot, checkEditorConfig, checkFile, checkGitHooks, checkGitHubActions, checkGitLabCI, checkNestedLanguages, checkPrePushHook, checkReadmeBadges, COMMITLINT_FILE_CHECK, } from '../../base/checks.js';
|
|
21
|
-
import { allDeps, checkAreTheTypesWrong, checkClaudeWorktreeSettings, checkConfigSchemaVersions, checkDocsSite, checkEnginesNode, checkGitDependencies, checkKnip, checkLintStaged, checkNodeVersionConsistency, checkNodeVersionPin, checkPackageJson, checkPackageManager, checkPublint, checkSemanticRelease, checkSizeLimit, checkTailwind, checkBuildApprovals, checkPnpmWorkspace, checkTreeshakeSetup, checkTurborepo, checkTypedoc, checkVerifyScript, checkVscodeExtensions, evaluateNodeVersion, FILE_CHECKS, findDocsAppDir, jsBadgeAudience, jsGitHooksProfile, readPackageJson, } from '../../languages/js/checks.js';
|
|
21
|
+
import { allDeps, checkAreTheTypesWrong, checkBiome, checkClaudeWorktreeSettings, checkConfigSchemaVersions, checkDocsSite, checkEnginesNode, checkGitDependencies, checkKnip, checkLintStaged, checkNodeVersionConsistency, checkNodeVersionPin, checkPackageJson, checkPackageManager, checkPublint, checkSemanticRelease, checkSizeLimit, checkTailwind, checkBuildApprovals, checkPnpmWorkspace, checkTreeshakeSetup, checkTurborepo, checkTypedoc, checkVerifyScript, checkVscodeExtensions, evaluateNodeVersion, FILE_CHECKS, findDocsAppDir, jsBadgeAudience, jsGitHooksProfile, readPackageJson, } from '../../languages/js/checks.js';
|
|
22
22
|
export { evaluateNodeVersion };
|
|
23
23
|
const PACKAGE = '@rtorcato/repo-tooling';
|
|
24
24
|
// Detects the broken-release-on-protected-main footgun: a workflow that runs
|
|
@@ -295,7 +295,10 @@ export async function runDoctor(dir) {
|
|
|
295
295
|
results.push(await checkNodeVersionPin(targetDir));
|
|
296
296
|
results.push(await checkNodeVersionConsistency(targetDir, pkg));
|
|
297
297
|
for (const spec of FILE_CHECKS) {
|
|
298
|
-
|
|
298
|
+
// Biome is the one spec that needs the directory as well as the file
|
|
299
|
+
// contents — it compares the `$schema` version against the installed
|
|
300
|
+
// CLI (#424). Dispatched here so it keeps its place in the output.
|
|
301
|
+
results.push(spec.check === 'Biome' ? await checkBiome(targetDir) : await checkFile(targetDir, spec));
|
|
299
302
|
}
|
|
300
303
|
results.push(await checkLintStaged(targetDir, pkg));
|
|
301
304
|
results.push(await checkVerifyScript(targetDir, pkg));
|
|
@@ -48,6 +48,18 @@ async function readJsonOrNull(filepath) {
|
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* The `@biomejs/biome` actually installed under `targetDir`, or null when
|
|
53
|
+
* node_modules isn't populated. This is the binary that will parse the config
|
|
54
|
+
* and emit the schema-mismatch warning, so it is the only version worth
|
|
55
|
+
* comparing a written `$schema` against — a declared range like `^2.5.0` says
|
|
56
|
+
* nothing about which 2.x resolved (#424).
|
|
57
|
+
*/
|
|
58
|
+
export async function installedBiomeVersion(targetDir) {
|
|
59
|
+
const installed = await readJsonOrNull(path.join(targetDir, 'node_modules', '@biomejs', 'biome', 'package.json'));
|
|
60
|
+
const match = typeof installed?.version === 'string' ? /(\d+\.\d+\.\d+)/.exec(installed.version) : null;
|
|
61
|
+
return match?.[1] ?? null;
|
|
62
|
+
}
|
|
51
63
|
/**
|
|
52
64
|
* The Biome version the scaffolded `$schema` URL should name (#363).
|
|
53
65
|
*
|
|
@@ -59,20 +71,18 @@ async function readJsonOrNull(filepath) {
|
|
|
59
71
|
* best statement of intent when node_modules isn't populated yet.
|
|
60
72
|
*/
|
|
61
73
|
export async function resolveBiomeSchemaVersion(targetDir) {
|
|
62
|
-
const installed = await
|
|
74
|
+
const installed = await installedBiomeVersion(targetDir);
|
|
75
|
+
if (installed)
|
|
76
|
+
return installed;
|
|
63
77
|
const pkg = await readJsonOrNull(path.join(targetDir, 'package.json'));
|
|
64
78
|
const deps = {
|
|
65
79
|
...(pkg?.dependencies ?? {}),
|
|
66
80
|
...(pkg?.devDependencies ?? {}),
|
|
67
81
|
};
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (match?.[1])
|
|
73
|
-
return match[1];
|
|
74
|
-
}
|
|
75
|
-
return BIOME_SCHEMA_FALLBACK;
|
|
82
|
+
// A range ("^2.5.0") carries the x.y.z we need just as an exact version does.
|
|
83
|
+
const declared = deps['@biomejs/biome'];
|
|
84
|
+
const match = typeof declared === 'string' ? /(\d+\.\d+\.\d+)/.exec(declared) : null;
|
|
85
|
+
return match?.[1] ?? BIOME_SCHEMA_FALLBACK;
|
|
76
86
|
}
|
|
77
87
|
/**
|
|
78
88
|
* The thin pointer config — the same shape this repo dogfoods. `fix biome` used
|
|
@@ -82,6 +82,11 @@ export const PRESETS = {
|
|
|
82
82
|
desc: 'Claude Code skill for driving the repo-tooling CLI',
|
|
83
83
|
legacyTarget: '.claude/skills/js-tooling.md',
|
|
84
84
|
},
|
|
85
|
+
'claude-sync-agents': {
|
|
86
|
+
source: 'tooling/claude/sync-agents.mjs',
|
|
87
|
+
target: 'scripts/sync-agents.mjs',
|
|
88
|
+
desc: 'Canonical SKILL.md → AGENTS.md sync script (zero-config; --check mode for CI)',
|
|
89
|
+
},
|
|
85
90
|
'mcp-example': {
|
|
86
91
|
source: 'tooling/mcp/mcp.json.example',
|
|
87
92
|
target: '.mcp.json.example',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
|
-
import { hookHasUncommented } from '../../base/checks.js';
|
|
3
|
+
import { checkFile, hookHasUncommented } from '../../base/checks.js';
|
|
4
|
+
import { installedBiomeVersion } from '../../cli/generators/linting.js';
|
|
4
5
|
import { CLAUDE_SETTINGS_FILE, readClaudeSettings, workspaceSymlinkDirs, worktreeSymlinkDirs, } from '../../cli/generators/agent-rules.js';
|
|
5
6
|
import { WORKSPACE_FILE, dependsOnEsbuild, familyGlob, missingPnpmSettings, } from '../../cli/generators/pnpm-workspace.js';
|
|
6
7
|
const PACKAGE = '@rtorcato/repo-tooling';
|
|
@@ -152,6 +153,54 @@ function matchesTsConfig(contents) {
|
|
|
152
153
|
rootDir.includes('${configDir}') &&
|
|
153
154
|
TS_PRESET_STRICTNESS.every((key) => compilerOptions[key] === true));
|
|
154
155
|
}
|
|
156
|
+
export const BIOME_FILE_CHECK = {
|
|
157
|
+
check: 'Biome',
|
|
158
|
+
candidates: ['biome.json', 'biome.jsonc'],
|
|
159
|
+
expected: `extends "${PACKAGE}/biome" or inlines the preset, with the linter on`,
|
|
160
|
+
matcher: matchesBiomeConfig,
|
|
161
|
+
optional: true,
|
|
162
|
+
hint: 'Run `npx @rtorcato/repo-tooling fix biome` to scaffold',
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* The Biome check plus the one thing a contents-only matcher can't see: whether
|
|
166
|
+
* the `$schema` URL still names the Biome that will read it (#424).
|
|
167
|
+
*
|
|
168
|
+
* `fix biome` writes the URL from the version resolved at scaffold time, and
|
|
169
|
+
* nothing re-checked it afterwards — so once the repo's Biome moved on, every
|
|
170
|
+
* single invocation (the pre-commit hook included) printed "The configuration
|
|
171
|
+
* schema version does not match the CLI version", while doctor stayed clean.
|
|
172
|
+
*
|
|
173
|
+
* Compared against the *installed* package only, never the declared range: an
|
|
174
|
+
* exact match is what Biome itself demands, and `^2.5.0` doesn't say which 2.x
|
|
175
|
+
* resolved. With no node_modules there is nothing to be wrong about, so the
|
|
176
|
+
* check says nothing rather than guessing.
|
|
177
|
+
*/
|
|
178
|
+
export async function checkBiome(dir) {
|
|
179
|
+
const result = await checkFile(dir, BIOME_FILE_CHECK);
|
|
180
|
+
if (result.status !== 'ok')
|
|
181
|
+
return result;
|
|
182
|
+
const installed = await installedBiomeVersion(dir);
|
|
183
|
+
if (!installed)
|
|
184
|
+
return result;
|
|
185
|
+
// The same candidate checkFile settled on — first one that exists wins.
|
|
186
|
+
for (const candidate of BIOME_FILE_CHECK.candidates) {
|
|
187
|
+
const filepath = path.join(dir, candidate);
|
|
188
|
+
if (!(await fs.pathExists(filepath)))
|
|
189
|
+
continue;
|
|
190
|
+
const url = readSchemaUrl(await fs.readFile(filepath, 'utf8'));
|
|
191
|
+
const targeted = url?.includes('biomejs.dev') ? schemaUrlVersion(url)?.join('.') : null;
|
|
192
|
+
if (targeted && targeted !== installed) {
|
|
193
|
+
return {
|
|
194
|
+
check: BIOME_FILE_CHECK.check,
|
|
195
|
+
status: 'drift',
|
|
196
|
+
detail: `${candidate} targets Biome ${targeted} but @biomejs/biome ${installed} is installed`,
|
|
197
|
+
hint: 'Run `npx @rtorcato/repo-tooling fix biome` to rewrite the $schema URL',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
155
204
|
export const FILE_CHECKS = [
|
|
156
205
|
{
|
|
157
206
|
check: 'TypeScript',
|
|
@@ -160,14 +209,7 @@ export const FILE_CHECKS = [
|
|
|
160
209
|
matcher: matchesTsConfig,
|
|
161
210
|
hint: 'Run `npx @rtorcato/repo-tooling fix tsconfig` to scaffold',
|
|
162
211
|
},
|
|
163
|
-
|
|
164
|
-
check: 'Biome',
|
|
165
|
-
candidates: ['biome.json', 'biome.jsonc'],
|
|
166
|
-
expected: `extends "${PACKAGE}/biome" or inlines the preset, with the linter on`,
|
|
167
|
-
matcher: matchesBiomeConfig,
|
|
168
|
-
optional: true,
|
|
169
|
-
hint: 'Run `npx @rtorcato/repo-tooling fix biome` to scaffold',
|
|
170
|
-
},
|
|
212
|
+
BIOME_FILE_CHECK,
|
|
171
213
|
{
|
|
172
214
|
check: 'ESLint',
|
|
173
215
|
candidates: ['eslint.config.js', 'eslint.config.mjs', 'eslint.config.cjs'],
|
|
@@ -685,6 +727,10 @@ export async function checkSizeLimit(dir, pkg) {
|
|
|
685
727
|
hint: 'Add `size-limit` to enforce bundle-size budgets in CI for library projects',
|
|
686
728
|
};
|
|
687
729
|
}
|
|
730
|
+
/** Changesets is in use when its config file is present. */
|
|
731
|
+
export function usesChangesets(dir) {
|
|
732
|
+
return fs.pathExists(path.join(dir, '.changeset', 'config.json'));
|
|
733
|
+
}
|
|
688
734
|
const SEMANTIC_RELEASE_FILES = [
|
|
689
735
|
'.releaserc',
|
|
690
736
|
'.releaserc.json',
|
|
@@ -714,7 +760,7 @@ export async function checkSemanticRelease(dir, pkg) {
|
|
|
714
760
|
break;
|
|
715
761
|
}
|
|
716
762
|
}
|
|
717
|
-
const hasChangesets = await
|
|
763
|
+
const hasChangesets = await usesChangesets(dir);
|
|
718
764
|
const hasReleasePlease = await fs.pathExists(path.join(dir, 'release-please-config.json'));
|
|
719
765
|
const hasSemanticRelease = inPkg || !!configFile;
|
|
720
766
|
// Conflict: more than one of {semantic-release, Changesets, Release Please}.
|
|
@@ -1205,9 +1251,14 @@ export async function findDocsAppDir(dir) {
|
|
|
1205
1251
|
export async function checkDocsSite(dir, docsAppDir) {
|
|
1206
1252
|
const check = 'Docs site';
|
|
1207
1253
|
const deltas = [];
|
|
1254
|
+
// sync-changelog pumps a root CHANGELOG.md into the site — a semantic-release
|
|
1255
|
+
// shape. Changesets writes per-package changelogs and leaves the (private)
|
|
1256
|
+
// root package without one, so don't require the script there (#425). If a
|
|
1257
|
+
// Changesets repo keeps the script anyway, it still has to be wired.
|
|
1208
1258
|
const syncPath = path.join(dir, 'scripts', 'sync-changelog.mjs');
|
|
1209
1259
|
if (!(await fs.pathExists(syncPath))) {
|
|
1210
|
-
|
|
1260
|
+
if (!(await usesChangesets(dir)))
|
|
1261
|
+
deltas.push('scripts/sync-changelog.mjs missing');
|
|
1211
1262
|
}
|
|
1212
1263
|
else {
|
|
1213
1264
|
const pkgPath = path.join(dir, docsAppDir, 'package.json');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rtorcato/repo-tooling",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.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": [
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"tooling/semantic-release/*.mjs",
|
|
101
101
|
"tooling/semantic-release/*.d.mts",
|
|
102
102
|
"tooling/claude/*.md",
|
|
103
|
+
"tooling/claude/*.mjs",
|
|
103
104
|
"tooling/mcp/mcp.json.example",
|
|
104
105
|
"tooling/swift/*.yml",
|
|
105
106
|
"tooling/swift/*.json",
|
|
@@ -694,6 +694,16 @@ Then spawn a background implementer agent:
|
|
|
694
694
|
> 3. Read the repo's `CLAUDE.md` and obey it — especially any pre-commit build
|
|
695
695
|
> step or committed build output.
|
|
696
696
|
> 4. Do the work. Conventional Commits within the branch.
|
|
697
|
+
>
|
|
698
|
+
> **Do not run `pnpm install`.** This worktree's `node_modules` is a symlink to
|
|
699
|
+
> the main checkout, so pnpm sees a foreign directory it must purge first and
|
|
700
|
+
> aborts with `ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY`. Dependencies are
|
|
701
|
+
> already present — run tests, lint and build directly. If the work *is* a
|
|
702
|
+
> dependency change, `pnpm install --lockfile-only` updates `pnpm-lock.yaml`
|
|
703
|
+
> without touching `node_modules`. When that leaves a verification step you
|
|
704
|
+
> cannot run, say so in the PR body — name the command you could not run and
|
|
705
|
+
> why — so the reviewer knows CI is the only check on it rather than assuming
|
|
706
|
+
> you ran it.
|
|
697
707
|
> 5. Push and open the PR. The title must be a Conventional Commit — it becomes
|
|
698
708
|
> the squash subject on `main` and, in repos using semantic-release, decides
|
|
699
709
|
> whether a release goes out at all. Body must contain `Closes #<N>`.
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: npm-publish
|
|
3
|
-
description: Use before releasing or publishing any
|
|
3
|
+
description: Use before releasing or publishing any package in a repo set up by @rtorcato/repo-tooling. Triggers on "publish", "cut a release", "bump the version", "tag a release", "npm publish", "how do releases work here". The hard rule — releases are automated by the repo's release tool (semantic-release, Changesets, or Release Please); an agent must NEVER run `npm publish`, `npm version`, or `git tag`, or hand-edit the version in package.json. NOT for the day-to-day tooling audit/fix flow — that's the repo-tooling skill.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# npm-publish
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
A repo set up by `@rtorcato/repo-tooling` releases through **automation on merge to
|
|
9
|
+
`main`** — one of semantic-release, Changesets, or Release Please. Versioning, the
|
|
10
|
+
git tag, the GitHub release, the CHANGELOG, and the npm publish all come from that
|
|
11
|
+
pipeline. There is no manual release step under any of them.
|
|
11
12
|
|
|
12
13
|
## The rule
|
|
13
14
|
|
|
@@ -17,31 +18,78 @@ all derived from the commit history. There is no manual release step.
|
|
|
17
18
|
- `npm version` (or editing `"version"` in `package.json`)
|
|
18
19
|
- `git tag` / pushing tags
|
|
19
20
|
- hand-editing `CHANGELOG.md`
|
|
21
|
+
- merging the "Version Packages" PR (Changesets) or the release PR (Release Please) —
|
|
22
|
+
that merge *is* the publish, so it is the human's release decision, not yours
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
This holds for all three tools. Only the "what to do instead" differs, so first
|
|
25
|
+
work out which tool the repo uses.
|
|
23
26
|
|
|
24
|
-
##
|
|
27
|
+
## Which release tool is this repo on?
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
Check the repo root, in this order — these are the same signals the CLI's
|
|
30
|
+
`doctor` check uses (`usesChangesets` / `checkSemanticRelease` in
|
|
31
|
+
`src/languages/js/checks.ts`):
|
|
32
|
+
|
|
33
|
+
| Signal | Tool |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `.changeset/config.json` exists | **Changesets** |
|
|
36
|
+
| `release-please-config.json` exists | **Release Please** |
|
|
37
|
+
| `.releaserc*` / `release.config.*` exists, or a `"release"` key in `package.json` | **semantic-release** |
|
|
38
|
+
|
|
39
|
+
Or just ask the tooling: `npx @rtorcato/repo-tooling doctor --json` reports the
|
|
40
|
+
`semantic-release` check as ok with a detail naming the tool actually in use.
|
|
41
|
+
|
|
42
|
+
If more than one is configured that's drift, not a choice — stop and flag it.
|
|
43
|
+
|
|
44
|
+
## What to do instead, per tool
|
|
45
|
+
|
|
46
|
+
### semantic-release
|
|
47
|
+
|
|
48
|
+
The bump is derived from the commit history, so the commit type *is* the release
|
|
49
|
+
decision:
|
|
50
|
+
|
|
51
|
+
- `fix:` → patch
|
|
52
|
+
- `feat:` → minor
|
|
53
|
+
- `feat!:` / `BREAKING CHANGE:` → major
|
|
54
|
+
- `chore:` / `docs:` / `refactor:` / `test:` → **no release**
|
|
55
|
+
|
|
56
|
+
To ship a change: give it the right commit type and merge to `main`.
|
|
57
|
+
|
|
58
|
+
### Changesets
|
|
59
|
+
|
|
60
|
+
The bump is declared in a changeset file, **not** inferred from commits. A PR with
|
|
61
|
+
no changeset publishes nothing, silently — so this is a required step, not an
|
|
62
|
+
optional one:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pnpm changeset # pick the packages + bump level, write the summary
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
That writes `.changeset/<name>.md`. Commit it with the change. On merge to `main`
|
|
69
|
+
the release action opens (or updates) a "Version Packages" PR; merging *that* is
|
|
70
|
+
what publishes — so never merge it yourself, leave it for a human. Hand-authoring
|
|
71
|
+
`.changeset/*.md` is fine — it is the one file in this flow you are meant to
|
|
72
|
+
write. `CHANGELOG.md` is still generated; don't touch it.
|
|
73
|
+
|
|
74
|
+
### Release Please
|
|
75
|
+
|
|
76
|
+
Also commit-driven, same Conventional Commit → bump mapping as semantic-release.
|
|
77
|
+
On merge to `main` it opens a release PR that carries the version bump and the
|
|
78
|
+
CHANGELOG entry; merging that PR tags and publishes, so leave that merge to a
|
|
79
|
+
human — never do it yourself. Don't edit `.release-please-manifest.json` by hand.
|
|
34
80
|
|
|
35
81
|
## What an agent should do
|
|
36
82
|
|
|
37
|
-
- To ship a change:
|
|
83
|
+
- To ship a change: right commit type, plus a changeset if the repo is on Changesets.
|
|
38
84
|
- To check the last release: `git tag --sort=-creatordate | head -1`, or the GitHub
|
|
39
|
-
Releases page — don't infer it from `package.json` (that's
|
|
40
|
-
|
|
41
|
-
- If a release seems stuck, inspect the release workflow run
|
|
85
|
+
Releases page — don't infer it from `package.json` (under semantic-release that's
|
|
86
|
+
`0.0.0-development` / a placeholder until CI stamps it).
|
|
87
|
+
- If a release seems stuck, inspect the release workflow run, and check for an open
|
|
88
|
+
release / "Version Packages" PR waiting to be merged — report it, do **not** merge
|
|
89
|
+
it yourself and do **not** publish by hand.
|
|
42
90
|
|
|
43
91
|
## Why
|
|
44
92
|
|
|
45
93
|
Manual publishes skip provenance, the CHANGELOG, and the tag/version invariant the
|
|
46
|
-
|
|
47
|
-
|
|
94
|
+
pipeline maintains, and a stray `npm version` commit derails the next automated bump.
|
|
95
|
+
One automated path per repo keeps releases reproducible and reviewable.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Canonical sync-agents for @rtorcato/* repos (shipped by @rtorcato/repo-tooling
|
|
3
|
+
// — copy via `repo-tooling copy claude-sync-agents`).
|
|
4
|
+
//
|
|
5
|
+
// Regenerates AGENTS.md from the repo's SKILL.md body so the two can never
|
|
6
|
+
// drift. SKILL.md (with its YAML frontmatter) is the single source of truth;
|
|
7
|
+
// AGENTS.md is its frontmatter-stripped mirror for non-Claude AI tools.
|
|
8
|
+
//
|
|
9
|
+
// node scripts/sync-agents.mjs # write AGENTS.md
|
|
10
|
+
// node scripts/sync-agents.mjs --check # exit 1 if AGENTS.md is stale (used by CI)
|
|
11
|
+
//
|
|
12
|
+
// Zero-config: the skill directory is derived from the root package.json `name`
|
|
13
|
+
// with any npm scope stripped — `@rtorcato/js-common` → `skills/js-common/`.
|
|
14
|
+
// That is the convention across the family, so this file is copied unmodified.
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
17
|
+
import { dirname, join } from 'node:path'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
|
|
20
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
21
|
+
const { name } = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
22
|
+
const skillRelative = `skills/${name.replace(/^@[^/]+\//, '')}/SKILL.md`
|
|
23
|
+
const SKILL = join(root, skillRelative)
|
|
24
|
+
const AGENTS = join(root, 'AGENTS.md')
|
|
25
|
+
|
|
26
|
+
const HEADER = `<!-- Generated from ${skillRelative} by scripts/sync-agents.mjs — do not edit by hand; run \`pnpm sync:agents\`. -->\n\n`
|
|
27
|
+
|
|
28
|
+
// Strip the leading YAML frontmatter block, keep the markdown body.
|
|
29
|
+
const body = readFileSync(SKILL, 'utf8').replace(/^---\n[\s\S]*?\n---\n+/, '')
|
|
30
|
+
const expected = HEADER + body
|
|
31
|
+
|
|
32
|
+
if (process.argv.includes('--check')) {
|
|
33
|
+
let current = ''
|
|
34
|
+
try {
|
|
35
|
+
current = readFileSync(AGENTS, 'utf8')
|
|
36
|
+
} catch {
|
|
37
|
+
// AGENTS.md missing → treat as stale below.
|
|
38
|
+
}
|
|
39
|
+
if (current !== expected) {
|
|
40
|
+
console.error(`AGENTS.md is out of sync with ${skillRelative} — run \`pnpm sync:agents\`.`)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
console.log(`AGENTS.md is in sync with ${skillRelative}.`)
|
|
44
|
+
} else {
|
|
45
|
+
writeFileSync(AGENTS, expected)
|
|
46
|
+
console.log(`Wrote AGENTS.md from ${skillRelative}`)
|
|
47
|
+
}
|