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 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 auto-adds `.claude/skills/` and `.agents/skills/` to your project
547
- `.gitignore` on first write skills are a per-developer cache, not
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillrepo",
3
- "version": "4.9.2",
3
+ "version": "4.10.0",
4
4
  "description": "Pull-based CLI for agent skills — init, sync, search, add, remove your library from any IDE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- * MUST stay in lockstep with `src/lib/mergers/gitignore.mjs`
62
- * SECTION_HEADER. The artifact-registry CI test asserts both modules
63
- * resolve the same header text.
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
- * Entries written under the gitignore section by init. The remover
70
- * does not match individual entries it removes every line between the
71
- * header and the next blank linebut the expected set is exported so
72
- * tests can verify the two lists stay in sync.
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 aboveso 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 both config.json and
350
- // .last-sync. Whole-directory removal is correct because both
351
- // children are CLI-owned and there's no room for user content.
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
  }),
@@ -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";
@@ -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 .agents/skills/, ensure
421
- // .gitignore is set up before starting any writes. A failure here
422
- // is recoverable (user fixes their .gitignore and re-runs) because
423
- // nothing has hit disk yet. A failure here AFTER a successful
424
- // claudeProject write would leave the user with a half-applied
425
- // state and an error message that doesn't reflect what's actually
426
- // on disk.
427
- if (targets.includes("agentsProject")) {
428
- ensureAgentsGitignore();
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 `.agents/skills/` is gitignored. Idempotent skips if the
811
- * entry is already present. Creates .gitignore if missing.
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
- * Throws diskError on any write failure so a read-only or symlinked
814
- * .gitignore surfaces as a user-visible error instead of silent data
815
- * loss.
823
+ * @param {PlacementTarget[]} targets
816
824
  */
817
- function ensureAgentsGitignore() {
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
- writeFileSafe(filePath, newContent);
827
+ ensureManagedRootIgnores(targets);
841
828
  } catch (err) {
842
- throw diskError(`Cannot update ${filePath}: ${err.message}`, {
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
  }