skillrepo 4.9.2 → 4.10.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 +17 -3
- package/package.json +1 -1
- package/src/lib/artifact-registry.mjs +28 -10
- package/src/lib/constants.mjs +11 -0
- package/src/lib/file-write.mjs +32 -47
- package/src/lib/foreign-content.mjs +461 -0
- package/src/lib/gitignore-section.mjs +223 -0
- package/src/lib/mergers/gitignore.mjs +22 -65
- package/src/lib/paths.mjs +11 -0
- package/src/lib/removers/gitignore.mjs +38 -3
- package/src/lib/sync.mjs +117 -0
- package/src/test/commands/update.test.mjs +51 -0
- package/src/test/e2e/cli-commands.test.mjs +63 -0
- package/src/test/lib/file-write.test.mjs +1 -1
- package/src/test/lib/foreign-content.test.mjs +522 -0
- package/src/test/lib/gitignore-section.test.mjs +213 -0
- package/src/test/lib/sync.test.mjs +349 -0
- package/src/test/mergers/uninstall-gitignore.test.mjs +65 -0
package/README.md
CHANGED
|
@@ -169,6 +169,20 @@ code and the error message goes to stderr (distinct from
|
|
|
169
169
|
`--session-hook`, which is Claude-Code-specific and exits 0 on every
|
|
170
170
|
error so a sync failure can't block a session start).
|
|
171
171
|
|
|
172
|
+
Every sync also inventories the skill folders it manages. A directory
|
|
173
|
+
the CLI did not write — a hand-authored or copied-in skill — gets a
|
|
174
|
+
one-line warning naming the path, why it matters (agents load skills
|
|
175
|
+
from disk whether or not they came from your library), and what to do
|
|
176
|
+
about it (`skillrepo push <path>` to bring it into your library, or
|
|
177
|
+
remove the directory). Each finding is reported once: the warning
|
|
178
|
+
repeats only when something changes, and only interactive runs print
|
|
179
|
+
it (session auto-sync hooks stay quiet). Warnings are local only
|
|
180
|
+
(skill names never leave your machine), and nothing is ever deleted or
|
|
181
|
+
modified. `--json` output includes the total count as `unmanaged`.
|
|
182
|
+
Sync also warns — once per repo, same rules — when a `skillrepo.json`
|
|
183
|
+
at the repo root is gitignored: the skillset declaration only works as
|
|
184
|
+
a committed file, so remove it from `.gitignore` and commit it.
|
|
185
|
+
|
|
172
186
|
### `get` — fetch a single skill
|
|
173
187
|
|
|
174
188
|
```sh
|
|
@@ -543,9 +557,9 @@ Skills land at one of two project paths, depending on the agent:
|
|
|
543
557
|
| Cursor / Windsurf / Gemini CLI / Codex CLI / Cline / GitHub Copilot | `.agents/skills/<name>/` | `~/.agents/skills/<name>/` |
|
|
544
558
|
| Windsurf (personal-scope override) | — | `~/.codeium/windsurf/skills/<name>/` |
|
|
545
559
|
|
|
546
|
-
The CLI
|
|
547
|
-
|
|
548
|
-
committed content. The `--agent` flag overrides detection (e.g.
|
|
560
|
+
The CLI keeps `.claude/skills/` and `.agents/skills/` listed in the
|
|
561
|
+
SkillRepo section of your project `.gitignore`, re-checked on every
|
|
562
|
+
sync — skills are a per-developer cache, not committed content. The `--agent` flag overrides detection (e.g.
|
|
549
563
|
`--agent claude,agents` writes both paths). See
|
|
550
564
|
[`docs/vendor-paths.md`](docs/vendor-paths.md) for primary-source
|
|
551
565
|
citations on each agent's read paths.
|
package/package.json
CHANGED
|
@@ -58,22 +58,39 @@ import { AGENT_REGISTRY } from "./agent-registry.mjs";
|
|
|
58
58
|
* and are removed by the gitignore remover. User-authored lines outside
|
|
59
59
|
* the section are never touched — this is the attribution mechanism.
|
|
60
60
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* The section writer (`src/lib/gitignore-section.mjs`) imports this
|
|
62
|
+
* constant directly (#2361 consolidated the previously duplicated
|
|
63
|
+
* header definitions), so merger, sync pre-flight, and remover are in
|
|
64
|
+
* lockstep at the language level.
|
|
64
65
|
*/
|
|
65
66
|
export const GITIGNORE_SECTION_HEADER =
|
|
66
67
|
"# SkillRepo CLI (added by `skillrepo init`)";
|
|
67
68
|
|
|
68
69
|
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* header
|
|
72
|
-
*
|
|
70
|
+
* Header of the RETIRED sync-time gitignore section (#2361). Before
|
|
71
|
+
* 4.10.0 the sync pre-flight wrote `.agents/skills/` under this
|
|
72
|
+
* separate header, outside the marked section above — so the uninstall
|
|
73
|
+
* remover never cleaned it. The merger's heal migrates the shape into
|
|
74
|
+
* the marked section on every sync, and the remover strips a leftover
|
|
75
|
+
* legacy section on uninstall. Nothing writes this header anymore;
|
|
76
|
+
* the constant exists so heal and remover stay in lockstep on the
|
|
77
|
+
* exact historical text.
|
|
78
|
+
*/
|
|
79
|
+
export const LEGACY_GITIGNORE_SECTION_HEADER =
|
|
80
|
+
"# SkillRepo (CLI-managed library skills)";
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Entries the CLI may write under the gitignore section. Init writes
|
|
84
|
+
* all of them (subject to vendor targets); the sync-time pre-flight
|
|
85
|
+
* (`ensureManagedRootIgnores`) writes only the managed-root entries.
|
|
86
|
+
* The remover does not match individual entries — it removes every
|
|
87
|
+
* line between the header and the next blank line — but the expected
|
|
88
|
+
* set is exported so tests can verify the lists stay in sync.
|
|
73
89
|
*/
|
|
74
90
|
export const GITIGNORE_REQUIRED_ENTRIES = Object.freeze([
|
|
75
91
|
".env.local",
|
|
76
92
|
".claude/skills/",
|
|
93
|
+
".agents/skills/",
|
|
77
94
|
".claude/settings.local.json",
|
|
78
95
|
]);
|
|
79
96
|
|
|
@@ -346,9 +363,10 @@ export const ARTIFACT_REGISTRY = Object.freeze([
|
|
|
346
363
|
id: "global-config-dir",
|
|
347
364
|
scope: "global",
|
|
348
365
|
kind: "directory",
|
|
349
|
-
// `~/.claude/skillrepo/` — the parent dir of
|
|
350
|
-
// .last-sync.
|
|
351
|
-
//
|
|
366
|
+
// `~/.claude/skillrepo/` — the parent dir of config.json,
|
|
367
|
+
// .last-sync, .npm-version-check, and .governance-seen (#2361).
|
|
368
|
+
// Whole-directory removal is correct because every child is
|
|
369
|
+
// CLI-owned and there's no room for user content.
|
|
352
370
|
pathFn: () => join(homedir(), ".claude", "skillrepo"),
|
|
353
371
|
displayPath: "~/.claude/skillrepo/",
|
|
354
372
|
}),
|
package/src/lib/constants.mjs
CHANGED
|
@@ -37,6 +37,17 @@
|
|
|
37
37
|
* and authorized regardless. Spoofing the params just affects UX
|
|
38
38
|
* immediacy, not authorization.
|
|
39
39
|
*/
|
|
40
|
+
/**
|
|
41
|
+
* Repo-root skillset declaration file (#2361, design doc D6 amendment
|
|
42
|
+
* 2026-08-07). The committed, human-owned governance contract: the
|
|
43
|
+
* declaration lives at the repo root in `skillrepo.json`, nested under
|
|
44
|
+
* a `skillset` key. H4 ships only the gitignored-declaration warning
|
|
45
|
+
* (`foreign-content.mjs`); reading and resolving the declaration is
|
|
46
|
+
* H5 (#2362), which consumes the sub-schema recorded in the design
|
|
47
|
+
* doc's D6 note.
|
|
48
|
+
*/
|
|
49
|
+
export const SKILLSET_DECLARATION_FILE = "skillrepo.json";
|
|
50
|
+
|
|
40
51
|
const DEFAULT_CLI_AUTH_HOST = "https://skillrepo.dev";
|
|
41
52
|
const CLI_AUTH_PATH = "/cli/auth";
|
|
42
53
|
const CLI_AUTH_QUERY = "?source=cli&new=1";
|
package/src/lib/file-write.mjs
CHANGED
|
@@ -56,7 +56,6 @@ import {
|
|
|
56
56
|
} from "node:fs";
|
|
57
57
|
import { dirname, join, isAbsolute, relative } from "node:path";
|
|
58
58
|
|
|
59
|
-
import { readFileSafe, writeFileSafe } from "./fs-utils.mjs";
|
|
60
59
|
import {
|
|
61
60
|
claudeSkillsProject,
|
|
62
61
|
claudeSkillsGlobal,
|
|
@@ -71,6 +70,7 @@ import {
|
|
|
71
70
|
gitignorePath,
|
|
72
71
|
} from "./paths.mjs";
|
|
73
72
|
import { AGENT_REGISTRY, getAgentByKey } from "./agent-registry.mjs";
|
|
73
|
+
import { ensureManagedRootIgnores } from "./gitignore-section.mjs";
|
|
74
74
|
import { CliError, validationError, diskError } from "./errors.mjs";
|
|
75
75
|
import { platformConventions } from "./platform.mjs";
|
|
76
76
|
|
|
@@ -93,9 +93,6 @@ export const BLOCKED_EXTENSIONS = new Set([
|
|
|
93
93
|
".wasm",
|
|
94
94
|
]);
|
|
95
95
|
|
|
96
|
-
const GITIGNORE_AGENTS_SKILLS_LINE = ".agents/skills/";
|
|
97
|
-
const GITIGNORE_SKILLS_HEADER = "# SkillRepo (CLI-managed library skills)";
|
|
98
|
-
|
|
99
96
|
// ── Types (JSDoc only — this is plain JS) ───────────────────────────────
|
|
100
97
|
|
|
101
98
|
/**
|
|
@@ -148,10 +145,15 @@ const CONTROL_CHARS_GLOBAL = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
|
148
145
|
* \uXXXX-escaped, so the error string can never re-inject the characters
|
|
149
146
|
* it rejects into terminal output or sync logs.
|
|
150
147
|
*
|
|
148
|
+
* Exported (#2361 prod-readiness): the foreign-content warnings print
|
|
149
|
+
* on-disk directory names — filesystem-sourced, attacker-influenceable
|
|
150
|
+
* strings — to stderr, the exact injection class this helper exists
|
|
151
|
+
* for. Single source per the shared-logic rule.
|
|
152
|
+
*
|
|
151
153
|
* @param {string} value
|
|
152
154
|
* @returns {string}
|
|
153
155
|
*/
|
|
154
|
-
function escapeControlChars(value) {
|
|
156
|
+
export function escapeControlChars(value) {
|
|
155
157
|
return value.replace(
|
|
156
158
|
CONTROL_CHARS_GLOBAL,
|
|
157
159
|
(c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`,
|
|
@@ -417,16 +419,17 @@ export function writeSkillDir(skill, options = {}) {
|
|
|
417
419
|
|
|
418
420
|
const targets = placementTargetsFor(options);
|
|
419
421
|
|
|
420
|
-
// Pre-flight: if any target writes to
|
|
421
|
-
// .
|
|
422
|
-
// is
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
422
|
+
// Pre-flight: if any target writes to a project-scope managed root
|
|
423
|
+
// (.claude/skills/, .agents/skills/), ensure the root's blanket
|
|
424
|
+
// ignore is present in the CLI's marked .gitignore section before
|
|
425
|
+
// starting any writes (#2361 — heal on every sync, both roots; the
|
|
426
|
+
// pre-4.10.0 code covered only .agents/skills/ under a separate
|
|
427
|
+
// header). A failure here is recoverable (user fixes their
|
|
428
|
+
// .gitignore and re-runs) because nothing has hit disk yet. A
|
|
429
|
+
// failure here AFTER a successful write to an earlier target would
|
|
430
|
+
// leave the user with a half-applied state and an error message
|
|
431
|
+
// that doesn't reflect what's actually on disk.
|
|
432
|
+
ensureProjectGitignore(targets);
|
|
430
433
|
|
|
431
434
|
const written = [];
|
|
432
435
|
|
|
@@ -807,43 +810,25 @@ function writeSkillToDir(skill, targetDir) {
|
|
|
807
810
|
}
|
|
808
811
|
|
|
809
812
|
/**
|
|
810
|
-
* Ensure
|
|
811
|
-
*
|
|
813
|
+
* Ensure every project-scope managed root in the write set has its
|
|
814
|
+
* blanket ignore inside the CLI's marked .gitignore section (#2361).
|
|
815
|
+
* Idempotent; also heals the retired pre-4.10.0 sync-time section.
|
|
816
|
+
* No-op for global-only write sets.
|
|
817
|
+
*
|
|
818
|
+
* `ensureManagedRootIgnores` throws typed diskErrors on read/write
|
|
819
|
+
* failure so a read-only or symlinked .gitignore surfaces as a
|
|
820
|
+
* user-visible error instead of silent data loss; the belt-and-
|
|
821
|
+
* suspenders wrap here only converts a non-CliError escape.
|
|
812
822
|
*
|
|
813
|
-
*
|
|
814
|
-
* .gitignore surfaces as a user-visible error instead of silent data
|
|
815
|
-
* loss.
|
|
823
|
+
* @param {PlacementTarget[]} targets
|
|
816
824
|
*/
|
|
817
|
-
function
|
|
818
|
-
const filePath = gitignorePath();
|
|
819
|
-
let existing = null;
|
|
820
|
-
try {
|
|
821
|
-
existing = readFileSafe(filePath);
|
|
822
|
-
} catch (err) {
|
|
823
|
-
throw diskError(`Cannot read ${filePath}: ${err.message}`, {
|
|
824
|
-
cause: err,
|
|
825
|
-
hint:
|
|
826
|
-
`Update your .gitignore manually to add ${GITIGNORE_AGENTS_SKILLS_LINE} before re-running.`,
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (existing !== null && existing.includes(GITIGNORE_AGENTS_SKILLS_LINE)) {
|
|
831
|
-
return; // Already present — no-op
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
const newContent =
|
|
835
|
-
existing === null
|
|
836
|
-
? `${GITIGNORE_SKILLS_HEADER}\n${GITIGNORE_AGENTS_SKILLS_LINE}\n`
|
|
837
|
-
: `${existing}${existing.endsWith("\n") ? "" : "\n"}\n${GITIGNORE_SKILLS_HEADER}\n${GITIGNORE_AGENTS_SKILLS_LINE}\n`;
|
|
838
|
-
|
|
825
|
+
function ensureProjectGitignore(targets) {
|
|
839
826
|
try {
|
|
840
|
-
|
|
827
|
+
ensureManagedRootIgnores(targets);
|
|
841
828
|
} catch (err) {
|
|
842
|
-
|
|
829
|
+
if (err instanceof CliError) throw err;
|
|
830
|
+
throw diskError(`Cannot update ${gitignorePath()}: ${err.message}`, {
|
|
843
831
|
cause: err,
|
|
844
|
-
hint:
|
|
845
|
-
`The CLI needs to add ${GITIGNORE_AGENTS_SKILLS_LINE} to .gitignore so library skills don't get committed. ` +
|
|
846
|
-
"Update .gitignore manually and re-run, or remove the read-only/symlinked constraint.",
|
|
847
832
|
});
|
|
848
833
|
}
|
|
849
834
|
}
|