skillrepo 4.9.2 → 4.11.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 +30 -3
- package/package.json +1 -1
- package/src/commands/init.mjs +18 -4
- package/src/lib/artifact-registry.mjs +28 -10
- package/src/lib/constants.mjs +11 -0
- package/src/lib/errors.mjs +23 -0
- package/src/lib/file-write.mjs +47 -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/skillset-declaration.mjs +477 -0
- package/src/lib/sync.mjs +179 -0
- package/src/test/commands/add.test.mjs +24 -0
- package/src/test/commands/get.test.mjs +24 -0
- package/src/test/commands/init.test.mjs +37 -0
- package/src/test/commands/update.test.mjs +194 -0
- package/src/test/e2e/cli-commands.test.mjs +63 -0
- package/src/test/lib/errors.test.mjs +28 -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/skillset-declaration.test.mjs +514 -0
- package/src/test/lib/sync.test.mjs +635 -0
- package/src/test/mergers/uninstall-gitignore.test.mjs +65 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Merger for .gitignore —
|
|
3
|
-
*
|
|
2
|
+
* Merger for .gitignore — the init-facing entry point over the CLI's
|
|
3
|
+
* single marked section (core writer: `../gitignore-section.mjs`).
|
|
4
4
|
*
|
|
5
|
-
* Always added
|
|
5
|
+
* Always added by `skillrepo init`:
|
|
6
6
|
* - `.env.local` — contains SKILLREPO_ACCESS_KEY, a live credential
|
|
7
7
|
* - `.claude/settings.local.json` — Claude Code per-user settings
|
|
8
8
|
*
|
|
@@ -10,16 +10,20 @@
|
|
|
10
10
|
* - `.claude/skills/` — when any vendor maps to `claudeProject`
|
|
11
11
|
* - `.agents/skills/` — when any vendor maps to `agentsProject`
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* The sync-time pre-flight (`file-write.mjs`) manages the managed-root
|
|
14
|
+
* entries through the same core, so every CLI-authored gitignore line
|
|
15
|
+
* lives in the one marked section the uninstall remover strips (#2361).
|
|
16
|
+
*
|
|
17
|
+
* Idempotent: entries already present are skipped; a legacy sync-time
|
|
18
|
+
* section is healed into the marked section as a side effect.
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
|
-
import { readFileSafe, writeFileSafe } from "../fs-utils.mjs";
|
|
19
|
-
import { gitignorePath } from "../paths.mjs";
|
|
20
21
|
import { placementTargetsFor } from "../file-write.mjs";
|
|
22
|
+
import {
|
|
23
|
+
ensureGitignoreEntries,
|
|
24
|
+
MANAGED_ROOT_ENTRIES,
|
|
25
|
+
} from "../gitignore-section.mjs";
|
|
21
26
|
|
|
22
|
-
const SECTION_HEADER = "# SkillRepo CLI (added by `skillrepo init`)";
|
|
23
27
|
const ALWAYS_ENTRIES = Object.freeze([
|
|
24
28
|
".env.local",
|
|
25
29
|
".claude/settings.local.json",
|
|
@@ -38,7 +42,7 @@ const ALWAYS_ENTRIES = Object.freeze([
|
|
|
38
42
|
function entriesFor(options) {
|
|
39
43
|
const entries = [...ALWAYS_ENTRIES];
|
|
40
44
|
if (!Array.isArray(options.vendors) || options.vendors.length === 0) {
|
|
41
|
-
entries.push(
|
|
45
|
+
entries.push(MANAGED_ROOT_ENTRIES.claudeProject);
|
|
42
46
|
return entries;
|
|
43
47
|
}
|
|
44
48
|
let targets;
|
|
@@ -50,72 +54,25 @@ function entriesFor(options) {
|
|
|
50
54
|
} catch {
|
|
51
55
|
// Caller passed an unknown vendor; fall back to the always-set
|
|
52
56
|
// entries plus `.claude/skills/` so we never under-protect.
|
|
53
|
-
entries.push(
|
|
57
|
+
entries.push(MANAGED_ROOT_ENTRIES.claudeProject);
|
|
54
58
|
return entries;
|
|
55
59
|
}
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
for (const [target, entry] of Object.entries(MANAGED_ROOT_ENTRIES)) {
|
|
61
|
+
if (targets.includes(target)) entries.push(entry);
|
|
62
|
+
}
|
|
58
63
|
return entries;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
/**
|
|
62
67
|
* Ensure the init-required paths are in .gitignore. Creates the file
|
|
63
68
|
* if it doesn't exist. Idempotent — returns `"skipped"` if every
|
|
64
|
-
* required entry is already present
|
|
65
|
-
*
|
|
69
|
+
* required entry is already present and no legacy section needed
|
|
70
|
+
* healing, `"created"` if the file didn't exist, `"updated"` if it did
|
|
71
|
+
* and at least one entry was missing (or a legacy section was healed).
|
|
66
72
|
*
|
|
67
73
|
* @param {{ vendors?: string[], global?: boolean }} [options]
|
|
68
74
|
* @returns {{ path: string; action: "created" | "updated" | "skipped"; added: string[] }}
|
|
69
75
|
*/
|
|
70
76
|
export function mergeGitignore(options = {}) {
|
|
71
|
-
|
|
72
|
-
const filePath = gitignorePath();
|
|
73
|
-
const existing = readFileSafe(filePath);
|
|
74
|
-
|
|
75
|
-
if (existing === null) {
|
|
76
|
-
// Fresh .gitignore — write all required entries as one section.
|
|
77
|
-
const content = renderSection(requiredEntries);
|
|
78
|
-
writeFileSafe(filePath, content);
|
|
79
|
-
return {
|
|
80
|
-
path: ".gitignore",
|
|
81
|
-
action: "created",
|
|
82
|
-
added: [...requiredEntries],
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Line-exact match check. `.env.local` as a line is distinct from
|
|
87
|
-
// `.env.local.backup` or a comment `# .env.local` — split on
|
|
88
|
-
// newlines and trim so we match the entry literally.
|
|
89
|
-
const lines = existing.split(/\r?\n/).map((l) => l.trim());
|
|
90
|
-
const missing = requiredEntries.filter((entry) => !lines.includes(entry));
|
|
91
|
-
|
|
92
|
-
if (missing.length === 0) {
|
|
93
|
-
return {
|
|
94
|
-
path: ".gitignore",
|
|
95
|
-
action: "skipped",
|
|
96
|
-
added: [],
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Append only the missing entries as a new section. Preserve the
|
|
101
|
-
// original line-ending style.
|
|
102
|
-
const lineEnding = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
103
|
-
const separator = existing.endsWith("\n") ? "" : lineEnding;
|
|
104
|
-
const block = renderSection(missing, lineEnding);
|
|
105
|
-
writeFileSafe(filePath, existing + separator + lineEnding + block);
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
path: ".gitignore",
|
|
109
|
-
action: "updated",
|
|
110
|
-
added: missing,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Render the SkillRepo section with the given entries. The leading
|
|
116
|
-
* blank line separates the section from whatever precedes it when
|
|
117
|
-
* appending, and the trailing newline keeps the file POSIX-clean.
|
|
118
|
-
*/
|
|
119
|
-
function renderSection(entries, lineEnding = "\n") {
|
|
120
|
-
return SECTION_HEADER + lineEnding + entries.join(lineEnding) + lineEnding;
|
|
77
|
+
return ensureGitignoreEntries(entriesFor(options));
|
|
121
78
|
}
|
package/src/lib/paths.mjs
CHANGED
|
@@ -38,6 +38,17 @@ export const globalLastSyncPath = () => join(homedir(), ".claude", "skillrepo",
|
|
|
38
38
|
export const globalNpmVersionCheckPath = () =>
|
|
39
39
|
join(homedir(), ".claude", "skillrepo", ".npm-version-check");
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Governance warn-on-new state (#2361 owner directive): which unmanaged
|
|
43
|
+
* dirs (per placement root) and gitignored declarations (per repo) the
|
|
44
|
+
* CLI has already warned about. Separate file from `.last-sync` —
|
|
45
|
+
* different concern (what the USER was told vs. library content state)
|
|
46
|
+
* and a schema bump in one must not gate the other. Same parent dir so
|
|
47
|
+
* uninstall's whole-directory removal covers it.
|
|
48
|
+
*/
|
|
49
|
+
export const globalGovernanceSeenPath = () =>
|
|
50
|
+
join(homedir(), ".claude", "skillrepo", ".governance-seen");
|
|
51
|
+
|
|
41
52
|
// ── Skill placement targets ────────────────────────────────────────────
|
|
42
53
|
//
|
|
43
54
|
// Per-vendor placement decisions live in `agent-registry.mjs`. This
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
|
|
23
23
|
import { existsSync, readFileSync } from "node:fs";
|
|
24
24
|
import { writeFileAtomic } from "../fs-utils.mjs";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
GITIGNORE_SECTION_HEADER,
|
|
27
|
+
LEGACY_GITIGNORE_SECTION_HEADER,
|
|
28
|
+
} from "../artifact-registry.mjs";
|
|
26
29
|
import { gitignorePath } from "../paths.mjs";
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -50,10 +53,41 @@ export function removeGitignore({ dryRun = false } = {}) {
|
|
|
50
53
|
// stays CRLF. `split(/\r?\n/)` discards the line-ending bytes, so
|
|
51
54
|
// we detect them explicitly and rebuild on that basis.
|
|
52
55
|
const lineEnding = original.includes("\r\n") ? "\r\n" : "\n";
|
|
53
|
-
|
|
56
|
+
let lines = original.split(/\r?\n/);
|
|
57
|
+
|
|
58
|
+
// Legacy sync-time section (#2361): pre-4.10.0 syncs wrote
|
|
59
|
+
// `.agents/skills/` under its own header, outside the marked
|
|
60
|
+
// section. If the merger's heal never ran (user uninstalls without
|
|
61
|
+
// syncing on 4.10.0+), strip the attributable shape here: the
|
|
62
|
+
// header line plus a directly-following `.agents/skills/` line.
|
|
63
|
+
// Any other line under the legacy header is user content and
|
|
64
|
+
// survives.
|
|
65
|
+
let legacyRemoved = 0;
|
|
66
|
+
const legacyIdx = lines.findIndex(
|
|
67
|
+
(l) => l === LEGACY_GITIGNORE_SECTION_HEADER,
|
|
68
|
+
);
|
|
69
|
+
if (legacyIdx !== -1) {
|
|
70
|
+
let legacyEnd = legacyIdx + 1;
|
|
71
|
+
if (legacyEnd < lines.length && lines[legacyEnd] === ".agents/skills/") {
|
|
72
|
+
legacyEnd += 1;
|
|
73
|
+
}
|
|
74
|
+
legacyRemoved = legacyEnd - legacyIdx;
|
|
75
|
+
lines = [...lines.slice(0, legacyIdx), ...lines.slice(legacyEnd)];
|
|
76
|
+
}
|
|
54
77
|
|
|
55
78
|
const headerIdx = lines.findIndex((l) => l === GITIGNORE_SECTION_HEADER);
|
|
56
79
|
if (headerIdx === -1) {
|
|
80
|
+
if (legacyRemoved > 0) {
|
|
81
|
+
if (dryRun) {
|
|
82
|
+
return {
|
|
83
|
+
path: ".gitignore",
|
|
84
|
+
action: "would-remove",
|
|
85
|
+
removed: legacyRemoved,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
writeFileAtomic(filePath, lines.join(lineEnding));
|
|
89
|
+
return { path: ".gitignore", action: "removed", removed: legacyRemoved };
|
|
90
|
+
}
|
|
57
91
|
return { path: ".gitignore", action: "skipped", removed: 0 };
|
|
58
92
|
}
|
|
59
93
|
|
|
@@ -89,7 +123,8 @@ export function removeGitignore({ dryRun = false } = {}) {
|
|
|
89
123
|
// ? 1 : 0)` gives the SkillRepo-line count.
|
|
90
124
|
const consumedTrailingBlank =
|
|
91
125
|
end > headerIdx + 1 && lines[end - 1] === "";
|
|
92
|
-
const reportedRemoved =
|
|
126
|
+
const reportedRemoved =
|
|
127
|
+
removedLines - (consumedTrailingBlank ? 1 : 0) + legacyRemoved;
|
|
93
128
|
|
|
94
129
|
if (dryRun) {
|
|
95
130
|
return {
|