skillrepo 3.0.0 → 3.1.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 +74 -6
- package/bin/skillrepo.mjs +14 -0
- package/package.json +1 -1
- package/src/commands/init.mjs +184 -19
- package/src/commands/remove.mjs +8 -13
- package/src/commands/session-sync.mjs +152 -0
- package/src/commands/uninstall.mjs +484 -0
- package/src/commands/update.mjs +125 -8
- package/src/lib/artifact-registry.mjs +305 -0
- package/src/lib/cli-config.mjs +78 -0
- package/src/lib/config.mjs +6 -3
- package/src/lib/file-write.mjs +8 -3
- package/src/lib/fs-utils.mjs +90 -9
- package/src/lib/mergers/session-hook.mjs +378 -0
- package/src/lib/paths.mjs +21 -0
- package/src/lib/platform.mjs +124 -0
- package/src/lib/removers/claude-mcp.mjs +67 -0
- package/src/lib/removers/cursor-mcp.mjs +60 -0
- package/src/lib/removers/env-local.mjs +55 -0
- package/src/lib/removers/gitignore.mjs +108 -0
- package/src/lib/removers/settings.mjs +183 -0
- package/src/lib/removers/vscode-mcp.mjs +87 -0
- package/src/lib/removers/windsurf-mcp.mjs +65 -0
- package/src/lib/sync.mjs +26 -0
- package/src/test/commands/add.test.mjs +10 -4
- package/src/test/commands/get.test.mjs +10 -4
- package/src/test/commands/init.test.mjs +428 -4
- package/src/test/commands/list.test.mjs +10 -4
- package/src/test/commands/remove.test.mjs +10 -4
- package/src/test/commands/search.test.mjs +10 -4
- package/src/test/commands/session-sync.test.mjs +352 -0
- package/src/test/commands/uninstall.test.mjs +774 -0
- package/src/test/commands/update.test.mjs +168 -4
- package/src/test/helpers/sandbox-home.mjs +161 -0
- package/src/test/helpers/skillrepo-shim.mjs +133 -0
- package/src/test/integration/file-write.integration.test.mjs +10 -4
- package/src/test/lib/artifact-registry.test.mjs +268 -0
- package/src/test/lib/cli-config.test.mjs +126 -5
- package/src/test/lib/config.test.mjs +10 -4
- package/src/test/lib/file-write.test.mjs +24 -10
- package/src/test/lib/mcp-merge.test.mjs +10 -4
- package/src/test/lib/paths.test.mjs +10 -4
- package/src/test/lib/platform.test.mjs +135 -0
- package/src/test/lib/sync.test.mjs +20 -4
- package/src/test/mergers/session-hook.test.mjs +1175 -0
- package/src/test/mergers/uninstall-claude-mcp.test.mjs +145 -0
- package/src/test/mergers/uninstall-cursor-mcp.test.mjs +108 -0
- package/src/test/mergers/uninstall-env-local.test.mjs +144 -0
- package/src/test/mergers/uninstall-gitignore.test.mjs +209 -0
- package/src/test/mergers/uninstall-settings.test.mjs +296 -0
- package/src/test/mergers/uninstall-vscode-mcp.test.mjs +215 -0
- package/src/test/mergers/uninstall-windsurf-mcp.test.mjs +128 -0
package/README.md
CHANGED
|
@@ -41,22 +41,24 @@ Requires Node.js 18 or later.
|
|
|
41
41
|
|
|
42
42
|
### `init` — first-run setup
|
|
43
43
|
|
|
44
|
-
Validates your access key, detects installed IDEs, writes the MCP config,
|
|
45
|
-
and runs the first library sync.
|
|
46
|
-
|
|
47
44
|
```sh
|
|
48
|
-
skillrepo init [--key <key>] [--url <url>] [--yes] [--force] [--ide <list>] [--global] [--json]
|
|
45
|
+
skillrepo init [--key <key>] [--url <url>] [--yes] [--force] [--ide <list>] [--global] [--json] [--no-session-sync]
|
|
49
46
|
```
|
|
50
47
|
|
|
48
|
+
Validates your access key, detects installed IDEs, writes the MCP config,
|
|
49
|
+
installs the Claude Code SessionStart hook (opt-in — see `session-sync`
|
|
50
|
+
below), and runs the first library sync.
|
|
51
|
+
|
|
51
52
|
| Flag | Description |
|
|
52
53
|
|------|-------------|
|
|
53
54
|
| `--key, -k <key>` | Access key. Falls back to `SKILLREPO_ACCESS_KEY` env var, then interactive prompt. |
|
|
54
55
|
| `--url, -u <url>` | Server URL. Defaults to `https://skillrepo.dev`. Use for self-hosted. |
|
|
55
|
-
| `--yes, -y` | Non-interactive. Skip all confirmation prompts. Required for CI. |
|
|
56
|
+
| `--yes, -y` | Non-interactive. Skip all confirmation prompts. Required for CI. Installs the session-sync hook by default — pass `--no-session-sync` to opt out. |
|
|
56
57
|
| `--force` | Re-prompt for a new key even if `~/.claude/skillrepo/config.json` is valid. |
|
|
57
58
|
| `--ide <list>` | Comma-separated vendor override. One or more of `claude`, `cursor`, `windsurf`, `vscode`, or `all`. |
|
|
58
59
|
| `--global` | Write skills to `~/.claude/skills/` (personal) instead of `.claude/skills/` (project). |
|
|
59
|
-
| `--
|
|
60
|
+
| `--no-session-sync` | Skip step 6 (SessionStart hook install). Works in both interactive and `--yes` modes. Use for CI scripts that bootstrap a project without ever starting a Claude Code session. |
|
|
61
|
+
| `--json` | Emit a structured JSON summary on success. Includes a `sessionSync: { action, path }` block describing whether the hook was installed. |
|
|
60
62
|
|
|
61
63
|
`init` is idempotent: re-running with a valid existing config re-runs
|
|
62
64
|
detection + MCP merge + first sync without re-prompting for a key. If the
|
|
@@ -129,6 +131,72 @@ DELETEs from `/api/v1/library` and deletes the local directory. Requires
|
|
|
129
131
|
a write-scoped access key. The local delete is immediate and does not
|
|
130
132
|
wait for a follow-up sync.
|
|
131
133
|
|
|
134
|
+
### `session-sync` — auto-sync on Claude Code session start
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
skillrepo session-sync enable [--global] [--json]
|
|
138
|
+
skillrepo session-sync disable [--global] [--json]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Installs (or removes) a Claude Code [SessionStart hook](https://docs.claude.com/en/docs/claude-code/hooks) that calls `skillrepo update` every time you open a Claude Code session — keeping your library current without you remembering to sync manually. The hook lives in `.claude/settings.local.json` (per-developer, gitignored by default) and runs your existing globally-installed `skillrepo` binary.
|
|
142
|
+
|
|
143
|
+
By default `skillrepo init` prompts you to install this hook. If you said no (or passed `--no-session-sync`), run `session-sync enable` later to turn it on.
|
|
144
|
+
|
|
145
|
+
**Requires a stable global install.** The hook is skipped (with a clear warning) when `skillrepo init` is invoked via `npx`, because the npx cache path is transient and the baked-in hook command would break on the next cache eviction. Install globally with `npm install -g skillrepo` before running `session-sync enable`.
|
|
146
|
+
|
|
147
|
+
**The hook cannot block your session.** The command it runs is `<path-to-skillrepo> update --session-hook 2>&1 [|| true]`. The `--session-hook` flag makes `update` exit 0 on every failure — network outage, revoked key, disk error, anything — and print a single-line failure message to your session. On POSIX systems the `|| true` shell backstop is appended as belt-and-suspenders; on Windows it's omitted because cmd.exe doesn't know the `true` builtin (the `--session-hook` flag's exit-0 contract is the primary defense regardless of platform). Session starts are never blocked by sync failures.
|
|
148
|
+
|
|
149
|
+
**On 304 (nothing changed) the hook is silent.** You only see output when your library actually syncs or a failure happens. No "Syncing…" noise on every session.
|
|
150
|
+
|
|
151
|
+
Flags:
|
|
152
|
+
|
|
153
|
+
- `--global` — operates on `~/.claude/settings.local.json` so the hook fires in every Claude Code session across all projects on your machine.
|
|
154
|
+
- `--json` — emit structured JSON with `action`, `path`, and `command` fields for scripting.
|
|
155
|
+
|
|
156
|
+
### `uninstall` — remove SkillRepo from a project
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
skillrepo uninstall [--dry-run] [--yes] [--global] [--json]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Surgically removes every SkillRepo artifact from the current project:
|
|
163
|
+
|
|
164
|
+
- `mcpServers.skillrepo` from `.mcp.json`, `.cursor/mcp.json`,
|
|
165
|
+
and `.vscode/mcp.json` (plus the matching `inputs` prompt in the
|
|
166
|
+
VS Code config)
|
|
167
|
+
- `SKILLREPO_ACCESS_KEY=...` lines from `.env.local`
|
|
168
|
+
- The SkillRepo section of `.gitignore`
|
|
169
|
+
- The SkillRepo `SessionStart` hook from `.claude/settings.local.json`
|
|
170
|
+
(if present)
|
|
171
|
+
- The `.claude/skills/` directory
|
|
172
|
+
|
|
173
|
+
Non-SkillRepo entries in shared files are preserved. Runs offline — no
|
|
174
|
+
server call required, so a revoked or missing access key is not a
|
|
175
|
+
problem. Interactive by default: the command prints a full list of what
|
|
176
|
+
will be removed and prompts for confirmation before touching anything.
|
|
177
|
+
|
|
178
|
+
With `--global`, also removes:
|
|
179
|
+
|
|
180
|
+
- `mcpServers.skillrepo` from `~/.codeium/windsurf/mcp_config.json`
|
|
181
|
+
- The `~/.claude/skills/` global skill cache
|
|
182
|
+
- The `~/.claude/skillrepo/` directory (stored credentials + sync cache)
|
|
183
|
+
|
|
184
|
+
Flags:
|
|
185
|
+
|
|
186
|
+
- `--dry-run` / `-n` — print what would be removed and exit without
|
|
187
|
+
touching any file.
|
|
188
|
+
- `--yes` / `-y` — skip the confirmation prompt.
|
|
189
|
+
- `--global` — also remove user-global state. By default the command
|
|
190
|
+
leaves your credential and other projects' integrations untouched.
|
|
191
|
+
- `--json` — emit structured JSON instead of human output. The summary
|
|
192
|
+
includes `removed[]` and `errors[]` arrays suitable for scripting.
|
|
193
|
+
|
|
194
|
+
The command is idempotent — a second run with nothing left to remove
|
|
195
|
+
exits 0 and reports "Nothing to remove." If any artifact fails to
|
|
196
|
+
remove (e.g. a file is read-only), the command continues processing
|
|
197
|
+
the others, surfaces every error at the end, and exits with code 3
|
|
198
|
+
(disk error).
|
|
199
|
+
|
|
132
200
|
## Configuration
|
|
133
201
|
|
|
134
202
|
### Credentials
|
package/bin/skillrepo.mjs
CHANGED
|
@@ -28,6 +28,8 @@ import { runAdd } from "../src/commands/add.mjs";
|
|
|
28
28
|
import { runRemove } from "../src/commands/remove.mjs";
|
|
29
29
|
import { runList } from "../src/commands/list.mjs";
|
|
30
30
|
import { runSearch } from "../src/commands/search.mjs";
|
|
31
|
+
import { runUninstall } from "../src/commands/uninstall.mjs";
|
|
32
|
+
import { runSessionSync } from "../src/commands/session-sync.mjs";
|
|
31
33
|
import { CliError, EXIT_OK, EXIT_VALIDATION } from "../src/lib/errors.mjs";
|
|
32
34
|
|
|
33
35
|
// ── Command registry ────────────────────────────────────────────────────
|
|
@@ -75,6 +77,18 @@ const COMMANDS = {
|
|
|
75
77
|
usage: "skillrepo search <query> [--limit <n>] [--json] [--semantic]",
|
|
76
78
|
run: async (argv) => runSearch(argv),
|
|
77
79
|
},
|
|
80
|
+
uninstall: {
|
|
81
|
+
description:
|
|
82
|
+
"Remove SkillRepo from this project (and optionally global state with --global)",
|
|
83
|
+
usage: "skillrepo uninstall [--dry-run] [--yes] [--global] [--json]",
|
|
84
|
+
run: async (argv) => runUninstall(argv),
|
|
85
|
+
},
|
|
86
|
+
"session-sync": {
|
|
87
|
+
description:
|
|
88
|
+
"Enable or disable the Claude Code SessionStart hook that auto-syncs your library",
|
|
89
|
+
usage: "skillrepo session-sync <enable|disable> [--global] [--json]",
|
|
90
|
+
run: async (argv) => runSessionSync(argv),
|
|
91
|
+
},
|
|
78
92
|
};
|
|
79
93
|
|
|
80
94
|
const COMMAND_NAMES = Object.keys(COMMANDS);
|
package/package.json
CHANGED
package/src/commands/init.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `skillrepo init` (#673) — PR3b rewrite.
|
|
2
|
+
* `skillrepo init` (#673) — PR3b rewrite, v3.1.0 session-sync (#884).
|
|
3
3
|
*
|
|
4
4
|
* First-run command. Replaces the v2.0.0 init that consumed the
|
|
5
5
|
* deprecated `/api/v1/setup` endpoint and wrote hook-delivery
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* 3. Write ~/.claude/skillrepo/config.json via config.mjs
|
|
11
11
|
* 4. Detect installed IDEs (.claude/, .cursor/, .vscode/, ~/.codeium/windsurf/)
|
|
12
12
|
* 5. Run MCP auto-merge for detected IDEs (user-confirmed unless --yes)
|
|
13
|
-
* 6.
|
|
14
|
-
* 7.
|
|
13
|
+
* 6. Install Claude Code SessionStart hook (v3.1.0 #884)
|
|
14
|
+
* 7. Run the first library sync via sync.mjs
|
|
15
|
+
* 8. Print summary
|
|
15
16
|
*
|
|
16
17
|
* Key differences from v2.0.0:
|
|
17
18
|
*
|
|
@@ -42,11 +43,12 @@
|
|
|
42
43
|
import { validateAccessKey } from "../lib/http.mjs";
|
|
43
44
|
import { detectIdes, formatDetectedIdes } from "../lib/detect-ides.mjs";
|
|
44
45
|
import { readConfig, writeConfig } from "../lib/config.mjs";
|
|
45
|
-
import { resolveFlags, effectiveVendors } from "../lib/cli-config.mjs";
|
|
46
|
+
import { resolveFlags, effectiveVendors, isNpxInvocation } from "../lib/cli-config.mjs";
|
|
46
47
|
import { mergeMcpForVendors, printManualMcpInstructions } from "../lib/mcp-merge.mjs";
|
|
47
48
|
import { runSync } from "../lib/sync.mjs";
|
|
48
49
|
import { mergeEnvLocal } from "../lib/mergers/env-local.mjs";
|
|
49
50
|
import { mergeGitignore } from "../lib/mergers/gitignore.mjs";
|
|
51
|
+
import { mergeSessionHook } from "../lib/mergers/session-hook.mjs";
|
|
50
52
|
import { resolveKeyFromEnvFiles } from "../lib/resolve-key.mjs";
|
|
51
53
|
import {
|
|
52
54
|
promptSecret,
|
|
@@ -141,7 +143,7 @@ export async function runInit(argv, io = {}) {
|
|
|
141
143
|
const stdout = io.stdout ?? process.stdout;
|
|
142
144
|
const stderr = io.stderr ?? process.stderr;
|
|
143
145
|
|
|
144
|
-
const { flags, yes, force } = parseInitFlags(argv);
|
|
146
|
+
const { flags, yes, force, noSessionSync } = parseInitFlags(argv);
|
|
145
147
|
|
|
146
148
|
// In --json mode, suppress all step-progress output so stdout
|
|
147
149
|
// carries only the final JSON blob.
|
|
@@ -150,7 +152,7 @@ export async function runInit(argv, io = {}) {
|
|
|
150
152
|
p.header("SkillRepo Init");
|
|
151
153
|
|
|
152
154
|
// ── Step 1: Collect credentials ───────────────────────────────
|
|
153
|
-
p.step(1,
|
|
155
|
+
p.step(1, 7, "Credentials");
|
|
154
156
|
|
|
155
157
|
// Try sources in priority: --key flag > --url flag > global
|
|
156
158
|
// config > env vars > interactive prompt.
|
|
@@ -202,7 +204,7 @@ export async function runInit(argv, io = {}) {
|
|
|
202
204
|
p.blank();
|
|
203
205
|
|
|
204
206
|
// ── Step 2: Validate against the server ──────────────────────
|
|
205
|
-
p.step(2,
|
|
207
|
+
p.step(2, 7, "Validating key");
|
|
206
208
|
let accountCtx;
|
|
207
209
|
try {
|
|
208
210
|
accountCtx = await validateAccessKey(serverUrl, apiKey);
|
|
@@ -232,7 +234,7 @@ export async function runInit(argv, io = {}) {
|
|
|
232
234
|
p.blank();
|
|
233
235
|
|
|
234
236
|
// ── Step 3: Write global config ──────────────────────────────
|
|
235
|
-
p.step(3,
|
|
237
|
+
p.step(3, 7, "Writing config");
|
|
236
238
|
const configAction = writeConfig({
|
|
237
239
|
apiKey,
|
|
238
240
|
serverUrl,
|
|
@@ -290,7 +292,7 @@ export async function runInit(argv, io = {}) {
|
|
|
290
292
|
p.blank();
|
|
291
293
|
|
|
292
294
|
// ── Step 4: Detect IDEs ──────────────────────────────────────
|
|
293
|
-
p.step(4,
|
|
295
|
+
p.step(4, 7, "Detecting IDEs");
|
|
294
296
|
|
|
295
297
|
// The v2.0.0 CLI had a silent fallback to [claudeCode, cursor]
|
|
296
298
|
// when nothing was detected. The v3.0.0 CLI removes that — the
|
|
@@ -343,7 +345,7 @@ export async function runInit(argv, io = {}) {
|
|
|
343
345
|
p.blank();
|
|
344
346
|
|
|
345
347
|
// ── Step 5: MCP auto-merge ───────────────────────────────────
|
|
346
|
-
p.step(5,
|
|
348
|
+
p.step(5, 7, "Configuring MCP");
|
|
347
349
|
const mcpUrl = `${serverUrl}/api/mcp`;
|
|
348
350
|
// In --json mode, pass a black-hole stdout to mergeMcpForVendors
|
|
349
351
|
// so its per-vendor preview lines don't pollute the JSON output.
|
|
@@ -375,8 +377,103 @@ export async function runInit(argv, io = {}) {
|
|
|
375
377
|
}
|
|
376
378
|
p.blank();
|
|
377
379
|
|
|
378
|
-
// ── Step 6:
|
|
379
|
-
|
|
380
|
+
// ── Step 6: Session sync (#884) ──────────────────────────────
|
|
381
|
+
//
|
|
382
|
+
// Install the Claude Code SessionStart hook so the user's library
|
|
383
|
+
// auto-syncs on every session start. Per the architect design in
|
|
384
|
+
// issue #884:
|
|
385
|
+
// - Opt-in by default. Interactive mode prompts before installing.
|
|
386
|
+
// `--yes` skips the prompt and installs. `--no-session-sync`
|
|
387
|
+
// is the explicit opt-out for BOTH modes — CI bootstraps
|
|
388
|
+
// without session sync by passing it.
|
|
389
|
+
// - If `which skillrepo` fails (e.g. npx user without a global
|
|
390
|
+
// install), we SKIP with a warning. Init continues — do not
|
|
391
|
+
// abort for this.
|
|
392
|
+
// - A failure writing the settings file is non-fatal: the
|
|
393
|
+
// config, MCP, and first sync still run. Users can re-run
|
|
394
|
+
// `skillrepo session-sync enable` later.
|
|
395
|
+
// - **Skip entirely when Claude Code is not the target.** The
|
|
396
|
+
// SessionStart hook is Claude Code-specific: it lives at
|
|
397
|
+
// `.claude/settings.local.json` and is only read by Claude
|
|
398
|
+
// Code's session-start machinery. A Cursor-only or
|
|
399
|
+
// Windsurf-only user doesn't benefit from it and shouldn't
|
|
400
|
+
// get a prompt for it. Cross-PR review (v3.1.0) flagged this
|
|
401
|
+
// as a silent-useless-state bug: without the guard, the hook
|
|
402
|
+
// file was written even for non-Claude projects. The only
|
|
403
|
+
// "Claude Code is the target" signals are:
|
|
404
|
+
// • `vendors` includes "claudeCode" (either explicit
|
|
405
|
+
// `--ide claude` or detected `.claude/` directory), OR
|
|
406
|
+
// • `--global` is passed (writes to
|
|
407
|
+
// `~/.claude/settings.local.json`, which is explicitly
|
|
408
|
+
// Claude Code's user-wide path).
|
|
409
|
+
// Everything else → skip with a clear message.
|
|
410
|
+
//
|
|
411
|
+
// This step is INSERTED between MCP merge (step 5) and the first
|
|
412
|
+
// sync (step 7). Order matters — we need the config written
|
|
413
|
+
// (step 3) so the hook's `skillrepo update` calls find creds.
|
|
414
|
+
p.step(6, 7, "Session sync");
|
|
415
|
+
let sessionSyncAction = "skipped";
|
|
416
|
+
let sessionSyncPath = null;
|
|
417
|
+
const claudeTargeted =
|
|
418
|
+
Boolean(flags.global) ||
|
|
419
|
+
(Array.isArray(vendors) && vendors.includes("claudeCode"));
|
|
420
|
+
if (noSessionSync) {
|
|
421
|
+
p.warning("Session sync skipped (--no-session-sync).");
|
|
422
|
+
sessionSyncAction = "opted-out";
|
|
423
|
+
} else if (!claudeTargeted) {
|
|
424
|
+
// Non-Claude-Code target (e.g. `--ide cursor`) — the hook would
|
|
425
|
+
// never fire, so skip the prompt AND the install. This is the
|
|
426
|
+
// v3.1.0 cross-PR review fix.
|
|
427
|
+
p.warning(
|
|
428
|
+
"Session sync skipped: the SessionStart hook is Claude Code-specific " +
|
|
429
|
+
"and no Claude Code target was configured.",
|
|
430
|
+
);
|
|
431
|
+
sessionSyncAction = "not-applicable";
|
|
432
|
+
} else {
|
|
433
|
+
let proceed = true;
|
|
434
|
+
if (!yes) {
|
|
435
|
+
proceed = await confirm(
|
|
436
|
+
"Install Claude Code SessionStart hook so your library auto-syncs on every session start?",
|
|
437
|
+
true,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (!proceed) {
|
|
441
|
+
p.warning(
|
|
442
|
+
"Session sync skipped. Run `skillrepo session-sync enable` to install it later.",
|
|
443
|
+
);
|
|
444
|
+
sessionSyncAction = "declined";
|
|
445
|
+
} else {
|
|
446
|
+
try {
|
|
447
|
+
const result = mergeSessionHook({ global: flags.global });
|
|
448
|
+
sessionSyncAction = result.action;
|
|
449
|
+
sessionSyncPath = result.path;
|
|
450
|
+
if (result.action === "installed") {
|
|
451
|
+
p.success(`SessionStart hook installed (${result.path})`);
|
|
452
|
+
} else if (result.action === "updated") {
|
|
453
|
+
p.success(`SessionStart hook updated (${result.path})`);
|
|
454
|
+
} else if (result.action === "unchanged") {
|
|
455
|
+
p.success(`SessionStart hook already installed (${result.path})`);
|
|
456
|
+
} else if (result.action === "skipped") {
|
|
457
|
+
// Binary not resolvable — the installer returned a reason.
|
|
458
|
+
p.warning(result.reason ?? "Session sync skipped.");
|
|
459
|
+
}
|
|
460
|
+
} catch (err) {
|
|
461
|
+
// Disk error (corrupt settings file, permissions). The
|
|
462
|
+
// other init steps already ran, so the config and MCP are
|
|
463
|
+
// still in place. Surface the error and continue to the
|
|
464
|
+
// first sync step — do NOT abort.
|
|
465
|
+
p.warning(
|
|
466
|
+
`Session sync failed: ${err?.message ?? String(err)}. ` +
|
|
467
|
+
`Run \`skillrepo session-sync enable\` after fixing the issue.`,
|
|
468
|
+
);
|
|
469
|
+
sessionSyncAction = "failed";
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
p.blank();
|
|
474
|
+
|
|
475
|
+
// ── Step 7: First sync ───────────────────────────────────────
|
|
476
|
+
p.step(7, 7, "Pulling library");
|
|
380
477
|
let syncSummary;
|
|
381
478
|
let syncFailedReason = null;
|
|
382
479
|
try {
|
|
@@ -422,16 +519,46 @@ export async function runInit(argv, io = {}) {
|
|
|
422
519
|
updated: 0,
|
|
423
520
|
removed: 0,
|
|
424
521
|
notModified: false,
|
|
522
|
+
// On a synthesized failure summary we genuinely don't know
|
|
523
|
+
// whether the sync WOULD have been full or delta — the network
|
|
524
|
+
// call never completed. Architect review (v3.1.1) flagged that
|
|
525
|
+
// emitting `fullSync: false` here is misleading for --json
|
|
526
|
+
// consumers: it looks like a legitimate "delta sync returned
|
|
527
|
+
// zero" signal. Using `null` makes the unknown-state
|
|
528
|
+
// explicit — any typed consumer must handle it separately
|
|
529
|
+
// from true/false. The always-present `sync.failureReason`
|
|
530
|
+
// field is still the authoritative "did the sync fail"
|
|
531
|
+
// indicator; fullSync is just additional context.
|
|
532
|
+
fullSync: null,
|
|
425
533
|
syncedAt: new Date().toISOString(),
|
|
426
534
|
};
|
|
427
535
|
}
|
|
428
536
|
|
|
537
|
+
const zeroDeltas =
|
|
538
|
+
syncSummary.added + syncSummary.updated + syncSummary.removed === 0;
|
|
539
|
+
|
|
429
540
|
if (syncFailedReason) {
|
|
430
541
|
// The warning already printed; the step-summary success line
|
|
431
542
|
// would be misleading, so we skip it. Any helpful "next steps"
|
|
432
543
|
// is in the final `SkillRepo is ready` block.
|
|
433
|
-
} else if (syncSummary.notModified
|
|
544
|
+
} else if (syncSummary.notModified) {
|
|
545
|
+
// 304 Not Modified — the client had the current ETag already.
|
|
546
|
+
// Definitively "up to date" regardless of whether the library
|
|
547
|
+
// is empty or populated.
|
|
548
|
+
p.success("Library is up to date.");
|
|
549
|
+
} else if (zeroDeltas && syncSummary.fullSync) {
|
|
550
|
+
// Full sync (no prior .last-sync state existed) with zero
|
|
551
|
+
// results — the account's library is genuinely empty.
|
|
434
552
|
p.success("No skills in library yet (add some with `skillrepo add @owner/name`)");
|
|
553
|
+
} else if (zeroDeltas) {
|
|
554
|
+
// Delta sync with zero results — nothing changed since the
|
|
555
|
+
// last sync. Could be zero skills total, or N skills all
|
|
556
|
+
// unchanged. Without a full-sync roundtrip we can't tell, so
|
|
557
|
+
// the accurate phrasing is "no changes." Before this fix, the
|
|
558
|
+
// init step-7 message conflated this with the truly-empty
|
|
559
|
+
// case, which lied to any user who had skills but had already
|
|
560
|
+
// synced them on a prior run.
|
|
561
|
+
p.success("Library is up to date (no changes since last sync).");
|
|
435
562
|
} else {
|
|
436
563
|
p.success(
|
|
437
564
|
`${syncSummary.added} added, ${syncSummary.updated} updated, ${syncSummary.removed} removed`,
|
|
@@ -457,6 +584,17 @@ export async function runInit(argv, io = {}) {
|
|
|
457
584
|
skipped: skipped.map((r) => r.path),
|
|
458
585
|
failed: failed.map((r) => ({ path: r.path, reason: r.reason })),
|
|
459
586
|
},
|
|
587
|
+
// Session-sync block — action values:
|
|
588
|
+
// "installed" | "updated" | "unchanged" (success states)
|
|
589
|
+
// "opted-out" (--no-session-sync)
|
|
590
|
+
// "declined" (user said no at the prompt)
|
|
591
|
+
// "not-applicable" (no Claude Code target — e.g. `--ide cursor`)
|
|
592
|
+
// "skipped" (binary not resolvable — reason in non-json path)
|
|
593
|
+
// "failed" (disk error during install)
|
|
594
|
+
sessionSync: {
|
|
595
|
+
action: sessionSyncAction,
|
|
596
|
+
path: sessionSyncPath,
|
|
597
|
+
},
|
|
460
598
|
// Sync block always shows the counts (zeroed on failure)
|
|
461
599
|
// and adds `failureReason` when the first sync blew up —
|
|
462
600
|
// downstream scripts can `.failureReason != null` to
|
|
@@ -473,10 +611,27 @@ export async function runInit(argv, io = {}) {
|
|
|
473
611
|
}
|
|
474
612
|
|
|
475
613
|
stdout.write("\n ✓ SkillRepo is ready.\n\n");
|
|
614
|
+
// Pick the command prefix the user can actually run. If they
|
|
615
|
+
// invoked init via `npx skillrepo ...`, bare `skillrepo list` will
|
|
616
|
+
// fail with "command not found" — they need `npx skillrepo list`.
|
|
617
|
+
// Under a global install, the bare command is correct. We default
|
|
618
|
+
// to bare and add the `npx` prefix ONLY when we can detect the
|
|
619
|
+
// current invocation is npx.
|
|
620
|
+
const prefix = isNpxInvocation() ? "npx skillrepo" : "skillrepo";
|
|
476
621
|
stdout.write(" Next steps:\n");
|
|
477
|
-
stdout.write(
|
|
478
|
-
stdout.write(
|
|
479
|
-
stdout.write(
|
|
622
|
+
stdout.write(` • ${prefix} list — see what's in your library\n`);
|
|
623
|
+
stdout.write(` • ${prefix} search <query> — find skills\n`);
|
|
624
|
+
stdout.write(` • ${prefix} add @owner/name — add a skill\n`);
|
|
625
|
+
if (isNpxInvocation()) {
|
|
626
|
+
// Soft recommendation: running under npx works but every command
|
|
627
|
+
// re-downloads the package. Global install is faster AND enables
|
|
628
|
+
// the session-sync feature (which requires a stable binary path).
|
|
629
|
+
stdout.write(
|
|
630
|
+
"\n Tip: `npm install -g skillrepo` for faster commands " +
|
|
631
|
+
"and to enable session-start sync.\n",
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
stdout.write("\n");
|
|
480
635
|
}
|
|
481
636
|
|
|
482
637
|
/**
|
|
@@ -486,10 +641,12 @@ export async function runInit(argv, io = {}) {
|
|
|
486
641
|
function parseInitFlags(argv) {
|
|
487
642
|
// resolveFlags handles --key/--url/--global/--ide/--json and
|
|
488
643
|
// rejects unknown flags via its acceptPositional callback. We
|
|
489
|
-
// intercept --yes and --
|
|
490
|
-
// the callback so we don't need to pre-filter
|
|
644
|
+
// intercept --yes, --force, and --no-session-sync as "positional-
|
|
645
|
+
// shaped" flags via the callback so we don't need to pre-filter
|
|
646
|
+
// argv.
|
|
491
647
|
let yes = false;
|
|
492
648
|
let force = false;
|
|
649
|
+
let noSessionSync = false;
|
|
493
650
|
|
|
494
651
|
const flags = resolveFlags(argv, {
|
|
495
652
|
requireAuth: false, // init MAY prompt for a key, so don't hard-fail
|
|
@@ -510,9 +667,17 @@ function parseInitFlags(argv) {
|
|
|
510
667
|
force = true;
|
|
511
668
|
return 1;
|
|
512
669
|
}
|
|
670
|
+
if (arg === "--no-session-sync") {
|
|
671
|
+
// #884: explicit opt-out for BOTH interactive and --yes
|
|
672
|
+
// modes. CI scripts that bootstrap a project without ever
|
|
673
|
+
// starting a Claude Code session pass this to skip the hook
|
|
674
|
+
// installation entirely.
|
|
675
|
+
noSessionSync = true;
|
|
676
|
+
return 1;
|
|
677
|
+
}
|
|
513
678
|
return false; // anything else is unknown
|
|
514
679
|
},
|
|
515
680
|
});
|
|
516
681
|
|
|
517
|
-
return { flags, yes, force };
|
|
682
|
+
return { flags, yes, force, noSessionSync };
|
|
518
683
|
}
|
package/src/commands/remove.mjs
CHANGED
|
@@ -11,19 +11,14 @@
|
|
|
11
11
|
* Why direct local delete instead of calling sync.mjs to process
|
|
12
12
|
* the tombstone:
|
|
13
13
|
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* in
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* already knows exactly what to delete — there's no reason to
|
|
23
|
-
* go through a tombstone round-trip.
|
|
24
|
-
* - Cross-machine remove still works via the tombstone-on-server
|
|
25
|
-
* path (once #875 lands); `remove` just short-circuits the
|
|
26
|
-
* single-machine case.
|
|
14
|
+
* - `remove` is called with a specific (owner, name), so the CLI
|
|
15
|
+
* already knows exactly what to delete. Going through a full
|
|
16
|
+
* sync round-trip to rediscover the tombstone is wasteful —
|
|
17
|
+
* a direct unlink is the shortest correct path.
|
|
18
|
+
* - Cross-machine remove propagation is handled by the sync
|
|
19
|
+
* path: the ETag factors in `libraryRemovals` (fixed in #875)
|
|
20
|
+
* so a conditional GET from another machine correctly
|
|
21
|
+
* invalidates and returns the tombstone list.
|
|
27
22
|
*
|
|
28
23
|
* Idempotent semantics:
|
|
29
24
|
*
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `skillrepo session-sync enable|disable` (#884).
|
|
3
|
+
*
|
|
4
|
+
* Thin command wrapper over the session-hook installer/remover in
|
|
5
|
+
* `src/lib/mergers/session-hook.mjs`. The command exists so users who
|
|
6
|
+
* want to toggle session-start sync WITHOUT re-running `skillrepo init`
|
|
7
|
+
* can do so with a single command. `init` calls the same underlying
|
|
8
|
+
* `mergeSessionHook` helper at its step 6.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* skillrepo session-sync enable — install the SessionStart hook
|
|
12
|
+
* skillrepo session-sync disable — remove it
|
|
13
|
+
* skillrepo session-sync status — print current state (future — not wired yet)
|
|
14
|
+
*
|
|
15
|
+
* Flags:
|
|
16
|
+
* --global Operate on ~/.claude/settings.local.json instead of the
|
|
17
|
+
* project-local file. Mirrors `init --global` semantics.
|
|
18
|
+
* --json Emit structured JSON instead of human output.
|
|
19
|
+
*
|
|
20
|
+
* Exit codes:
|
|
21
|
+
* 0 success (installed, updated, unchanged, removed, or already disabled)
|
|
22
|
+
* 3 disk error (cannot read/write the settings file)
|
|
23
|
+
* 5 validation error (bad subcommand or flag combination)
|
|
24
|
+
*
|
|
25
|
+
* This command does NOT share the `update --session-hook` exit-0-on-
|
|
26
|
+
* errors contract. That contract exists specifically for the hook
|
|
27
|
+
* runner's startup path; when a user explicitly invokes
|
|
28
|
+
* `skillrepo session-sync enable` at the shell, they expect a
|
|
29
|
+
* non-zero exit on failure so their shell script knows something
|
|
30
|
+
* went wrong.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
mergeSessionHook,
|
|
35
|
+
removeSessionHook,
|
|
36
|
+
} from "../lib/mergers/session-hook.mjs";
|
|
37
|
+
import { resolveFlags } from "../lib/cli-config.mjs";
|
|
38
|
+
import { validationError } from "../lib/errors.mjs";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run `session-sync <subcommand>`. Throws CliError on failure; the
|
|
42
|
+
* dispatcher maps to the appropriate exit code.
|
|
43
|
+
*
|
|
44
|
+
* @param {string[]} argv
|
|
45
|
+
* @param {object} [io]
|
|
46
|
+
* @param {NodeJS.WritableStream} [io.stdout=process.stdout]
|
|
47
|
+
* @param {NodeJS.WritableStream} [io.stderr=process.stderr]
|
|
48
|
+
*/
|
|
49
|
+
export async function runSessionSync(argv, io = {}) {
|
|
50
|
+
const stdout = io.stdout ?? process.stdout;
|
|
51
|
+
|
|
52
|
+
// ── Subcommand + flag parsing ───────────────────────────────────
|
|
53
|
+
//
|
|
54
|
+
// The subcommand is the first positional arg — "enable" or
|
|
55
|
+
// "disable". Use resolveFlags' acceptPositional callback to
|
|
56
|
+
// consume it (same pattern `get`, `search`, `add`, `remove` use
|
|
57
|
+
// for their positional args).
|
|
58
|
+
let subcommand = null;
|
|
59
|
+
const flags = resolveFlags(argv, {
|
|
60
|
+
requireAuth: false, // no server call — key not needed
|
|
61
|
+
skipConfig: true, // don't read the global config file
|
|
62
|
+
acceptPositional(arg) {
|
|
63
|
+
if (arg === "enable" || arg === "disable") {
|
|
64
|
+
if (subcommand !== null) {
|
|
65
|
+
throw validationError(
|
|
66
|
+
`session-sync accepts exactly one subcommand; got "${subcommand}" and "${arg}"`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
subcommand = arg;
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!subcommand) {
|
|
77
|
+
throw validationError(
|
|
78
|
+
"session-sync requires a subcommand (enable or disable).",
|
|
79
|
+
{ hint: "Usage: skillrepo session-sync <enable|disable> [--global] [--json]" },
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Dispatch ────────────────────────────────────────────────────
|
|
84
|
+
if (subcommand === "enable") {
|
|
85
|
+
const result = mergeSessionHook({ global: flags.global });
|
|
86
|
+
if (flags.json) {
|
|
87
|
+
stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
printEnableResult(result, stdout);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (subcommand === "disable") {
|
|
95
|
+
const result = removeSessionHook({ global: flags.global });
|
|
96
|
+
if (flags.json) {
|
|
97
|
+
stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
printDisableResult(result, stdout);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Unreachable — the acceptPositional callback only accepts the
|
|
105
|
+
// two subcommands. Defensive throw so a future addition with a
|
|
106
|
+
// typo'd branch surfaces immediately.
|
|
107
|
+
throw validationError(`session-sync: unknown subcommand "${subcommand}"`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function printEnableResult(result, out) {
|
|
111
|
+
if (result.action === "installed") {
|
|
112
|
+
out.write(`\n ✓ SessionStart hook installed (${result.path})\n\n`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (result.action === "updated") {
|
|
116
|
+
out.write(`\n ✓ SessionStart hook updated (${result.path})\n\n`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (result.action === "unchanged") {
|
|
120
|
+
out.write(`\n ✓ SessionStart hook already installed (${result.path})\n\n`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// "skipped" — reason is set
|
|
124
|
+
out.write(`\n ⚠ Could not enable session sync: ${result.reason}\n\n`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function printDisableResult(result, out) {
|
|
128
|
+
if (result.action === "removed") {
|
|
129
|
+
out.write(`\n ✓ SessionStart hook removed (${result.path})\n\n`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (result.action === "unchanged") {
|
|
133
|
+
out.write(
|
|
134
|
+
`\n ✓ SessionStart hook was not installed (${result.path}) — nothing to do.\n\n`,
|
|
135
|
+
);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// "skipped" with an error — the settings file exists but couldn't
|
|
139
|
+
// be parsed. Both reviewers (round 2) flagged the observability
|
|
140
|
+
// gap: without this branch, a corrupt file looked identical to
|
|
141
|
+
// "file doesn't exist" in human output, and users couldn't tell
|
|
142
|
+
// the difference. The --json path already surfaces result.error
|
|
143
|
+
// via its full serialize, so only human mode was affected.
|
|
144
|
+
if (result.action === "skipped" && result.error) {
|
|
145
|
+
out.write(`\n ⚠ ${result.error}\n\n`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// "skipped" without an error — file genuinely doesn't exist.
|
|
149
|
+
out.write(
|
|
150
|
+
`\n ✓ No settings file at ${result.path} — session sync is not enabled.\n\n`,
|
|
151
|
+
);
|
|
152
|
+
}
|