skillrepo 3.1.1 → 3.1.2
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 +4 -2
- package/package.json +1 -1
- package/src/commands/init-session-sync.mjs +307 -0
- package/src/commands/init.mjs +74 -111
- package/src/commands/session-sync-actions.mjs +92 -0
- package/src/lib/binary-locator.mjs +99 -0
- package/src/lib/cli-config.mjs +7 -72
- package/src/lib/cli-version.mjs +56 -0
- package/src/lib/global-install.mjs +387 -0
- package/src/lib/mcp-merge.mjs +16 -5
- package/src/lib/mergers/session-hook.mjs +80 -68
- package/src/lib/transient-runners.mjs +204 -0
- package/src/test/commands/init.test.mjs +662 -1
- package/src/test/commands/session-sync-actions.test.mjs +74 -0
- package/src/test/helpers/mock-spawn.mjs +121 -0
- package/src/test/lib/cli-config.test.mjs +66 -9
- package/src/test/lib/cli-version.test.mjs +47 -0
- package/src/test/lib/global-install.test.mjs +424 -0
- package/src/test/lib/mcp-merge.test.mjs +3 -3
- package/src/test/lib/transient-runners.test.mjs +270 -0
- package/src/test/mergers/session-hook.test.mjs +284 -14
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ below), and runs the first library sync.
|
|
|
53
53
|
|------|-------------|
|
|
54
54
|
| `--key, -k <key>` | Access key. Falls back to `SKILLREPO_ACCESS_KEY` env var, then interactive prompt. |
|
|
55
55
|
| `--url, -u <url>` | Server URL. Defaults to `https://skillrepo.dev`. Use for self-hosted. |
|
|
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
|
+
| `--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. Under `npx`, this also auto-runs `npm install -g skillrepo@<this-version>` if no global install is present (so the session-sync hook can use the resulting absolute path). |
|
|
57
57
|
| `--force` | Re-prompt for a new key even if `~/.claude/skillrepo/config.json` is valid. |
|
|
58
58
|
| `--ide <list>` | Comma-separated vendor override. One or more of `claude`, `cursor`, `windsurf`, `vscode`, or `all`. |
|
|
59
59
|
| `--global` | Write skills to `~/.claude/skills/` (personal) instead of `.claude/skills/` (project). |
|
|
@@ -142,7 +142,9 @@ Installs (or removes) a Claude Code [SessionStart hook](https://docs.claude.com/
|
|
|
142
142
|
|
|
143
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
144
|
|
|
145
|
-
**
|
|
145
|
+
**Under `npx skillrepo init`, the CLI offers to install itself globally.** Session sync needs the binary at a stable absolute path (the `npx` cache path is transient and would break on the next cache eviction). Rather than skipping with a warning the way v3.1.1 did, v3.1.2 prompts during init: *"SkillRepo needs a global install to enable session sync. Install `skillrepo` globally now? (Y/n)"* — saying yes runs `npm install -g skillrepo@<version>` (pinned to the version you just invoked) and then installs the hook with the resulting absolute path. Under `--yes` the install runs without prompting; under `--no-session-sync` it's skipped entirely. If the install fails (permissions, network, registry), init prints actionable next-steps and continues; the rest of init still completes successfully.
|
|
146
|
+
|
|
147
|
+
`skillrepo session-sync enable` does **not** auto-install — it's an explicit, deliberate command and assumes you already have a global install. If invoked under `npx` without a global install present, it returns a clear "install globally first" message rather than mutating your global package set.
|
|
146
148
|
|
|
147
149
|
**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
150
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `skillrepo init` step 6 — Claude Code SessionStart hook installation.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `init.mjs` for readability (#894 / v3.1.2). The
|
|
5
|
+
* v3.1.2 auto-install-global feature added five branches to what
|
|
6
|
+
* was a two-branch step in v3.1.0; nesting that inside init.mjs
|
|
7
|
+
* crossed the readability threshold. This module owns the entire
|
|
8
|
+
* decision tree and returns a flat result the caller integrates
|
|
9
|
+
* into the init summary + JSON output.
|
|
10
|
+
*
|
|
11
|
+
* ## Decision tree
|
|
12
|
+
*
|
|
13
|
+
* noSessionSync → "opted-out" (no further action)
|
|
14
|
+
* !claudeTargeted → "not-applicable" (Claude-specific
|
|
15
|
+
* hook would never fire)
|
|
16
|
+
* non-npx install → resolve via PATH, install hook
|
|
17
|
+
* npx with EXISTING global → use the global directly
|
|
18
|
+
* npx without global, --yes → auto-install global, install hook
|
|
19
|
+
* npx without global, prompt → ask first, then auto-install or
|
|
20
|
+
* decline
|
|
21
|
+
*
|
|
22
|
+
* The auto-install branch runs `npm install -g skillrepo@<version>`
|
|
23
|
+
* itself so the user doesn't have to. v3.1.1 skipped the install
|
|
24
|
+
* with a "do it yourself" warning AFTER prompting for consent —
|
|
25
|
+
* the prompt-then-fail UX bug v3.1.2 fixes.
|
|
26
|
+
*
|
|
27
|
+
* ## Return shape
|
|
28
|
+
*
|
|
29
|
+
* {
|
|
30
|
+
* action: SessionSyncAction, // see ./session-sync-actions.mjs
|
|
31
|
+
* path: string | null, // settings.local.json display path
|
|
32
|
+
* globalInstallActive: boolean, // true when a stable global
|
|
33
|
+
* // skillrepo binary is now on
|
|
34
|
+
* // PATH (used by Next-Steps to
|
|
35
|
+
* // suppress the now-stale
|
|
36
|
+
* // "install globally" tip)
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* ## Failure semantics
|
|
40
|
+
*
|
|
41
|
+
* Every recoverable failure (auto-install error, hook write error,
|
|
42
|
+
* version-read error) sets `action` to a descriptive enum value and
|
|
43
|
+
* returns. The caller (init.mjs) does NOT abort init — the rest of
|
|
44
|
+
* init has already succeeded and the user's library is on disk.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { mergeSessionHook } from "../lib/mergers/session-hook.mjs";
|
|
48
|
+
import {
|
|
49
|
+
installSkillrepoGlobally,
|
|
50
|
+
resolveGlobalBinary,
|
|
51
|
+
} from "../lib/global-install.mjs";
|
|
52
|
+
import { getCliVersion } from "../lib/cli-version.mjs";
|
|
53
|
+
import {
|
|
54
|
+
detectTransientRunner,
|
|
55
|
+
isTransientRunnerInvocation,
|
|
56
|
+
globalInstallCommandFor,
|
|
57
|
+
} from "../lib/transient-runners.mjs";
|
|
58
|
+
import { confirm as realConfirm } from "../lib/prompt.mjs";
|
|
59
|
+
import { SessionSyncAction, isHookActive } from "./session-sync-actions.mjs";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {Object} SessionSyncResult
|
|
63
|
+
* @property {string} action - One of the `SessionSyncAction` enum values.
|
|
64
|
+
* @property {string | null} path - Display path of the settings file
|
|
65
|
+
* the hook was written to (or `null` if no write happened).
|
|
66
|
+
* @property {boolean} globalInstallActive - True if a stable global
|
|
67
|
+
* skillrepo binary is on PATH after this step. Used by the
|
|
68
|
+
* Next-Steps Tip block to decide whether to suggest
|
|
69
|
+
* `npm install -g`.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {Object} SessionSyncOptions
|
|
74
|
+
* @property {boolean} noSessionSync - True if `--no-session-sync`.
|
|
75
|
+
* @property {boolean} claudeTargeted - True if Claude Code is in the
|
|
76
|
+
* vendor target list (via `--ide claude` or `--global` or
|
|
77
|
+
* auto-detection).
|
|
78
|
+
* @property {boolean} yes - True if `--yes`.
|
|
79
|
+
* @property {boolean} json - True if `--json`. Affects spawn output
|
|
80
|
+
* mode (silent vs inherit) so npm progress doesn't pollute
|
|
81
|
+
* the JSON stdout.
|
|
82
|
+
* @property {boolean} global - True if `--global` (writes to
|
|
83
|
+
* `~/.claude/settings.local.json`).
|
|
84
|
+
* @property {object} p - Printer (from init.mjs makePrinter).
|
|
85
|
+
* @property {object} [deps] - Test-only injection.
|
|
86
|
+
* @property {Function} [deps.spawn] - For installSkillrepoGlobally.
|
|
87
|
+
* @property {Function} [deps.getCliVersion] - For testing the version-
|
|
88
|
+
* read failure recovery path (ESM exports can't be
|
|
89
|
+
* monkey-patched).
|
|
90
|
+
* @property {Function} [deps.confirmFn] - For testing the interactive
|
|
91
|
+
* prompt path. Default: real `confirm` from prompt.mjs.
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Run step 6. Always returns a result; never throws on user-recoverable
|
|
96
|
+
* failures. (`mergeSessionHook` may throw a disk error on a corrupt
|
|
97
|
+
* settings file — that's caught and surfaced as `action: "failed"`.)
|
|
98
|
+
*
|
|
99
|
+
* @param {SessionSyncOptions} options
|
|
100
|
+
* @returns {Promise<SessionSyncResult>}
|
|
101
|
+
*/
|
|
102
|
+
export async function installSessionSyncHook({
|
|
103
|
+
noSessionSync,
|
|
104
|
+
claudeTargeted,
|
|
105
|
+
yes,
|
|
106
|
+
json,
|
|
107
|
+
global,
|
|
108
|
+
p,
|
|
109
|
+
deps = {},
|
|
110
|
+
}) {
|
|
111
|
+
if (noSessionSync) {
|
|
112
|
+
p.warning("Session sync skipped (--no-session-sync).");
|
|
113
|
+
return result(SessionSyncAction.OptedOut);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!claudeTargeted) {
|
|
117
|
+
p.warning(
|
|
118
|
+
"Session sync skipped: the SessionStart hook is Claude Code-specific " +
|
|
119
|
+
"and no Claude Code target was configured.",
|
|
120
|
+
);
|
|
121
|
+
return result(SessionSyncAction.NotApplicable);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const npxMode = isTransientRunnerInvocation();
|
|
125
|
+
if (!npxMode) {
|
|
126
|
+
return await runNonNpxBranch({ yes, global, p, deps });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Transient-runner mode (npx/pnpx/yarn dlx/bunx). Decide between
|
|
130
|
+
// using a pre-existing global and
|
|
131
|
+
// offering to install one.
|
|
132
|
+
const existingGlobalBinary = resolveGlobalBinary();
|
|
133
|
+
if (existingGlobalBinary) {
|
|
134
|
+
return runUseExistingGlobalBranch({
|
|
135
|
+
binaryPath: existingGlobalBinary,
|
|
136
|
+
global,
|
|
137
|
+
p,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return await runAutoInstallBranch({ yes, json, global, p, deps });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Branch implementations ───────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Non-npx case: stable install with skillrepo on PATH (or PATH
|
|
147
|
+
* misconfigured — handled gracefully). Existing v3.1.0/v3.1.1 path,
|
|
148
|
+
* preserved unchanged.
|
|
149
|
+
*/
|
|
150
|
+
async function runNonNpxBranch({ yes, global, p, deps }) {
|
|
151
|
+
const confirmFn = deps.confirmFn ?? realConfirm;
|
|
152
|
+
const proceed =
|
|
153
|
+
yes ||
|
|
154
|
+
(await confirmFn(
|
|
155
|
+
"Install Claude Code SessionStart hook so your library auto-syncs on every session start?",
|
|
156
|
+
true,
|
|
157
|
+
));
|
|
158
|
+
if (!proceed) {
|
|
159
|
+
p.warning(
|
|
160
|
+
"Session sync skipped. Run `skillrepo session-sync enable` to install it later.",
|
|
161
|
+
);
|
|
162
|
+
return result(SessionSyncAction.Declined);
|
|
163
|
+
}
|
|
164
|
+
return tryMergeAndPrint({ global, p });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Pre-existing global on PATH (likely user installed manually before
|
|
169
|
+
* running `npx skillrepo init`). No install offer; use the existing
|
|
170
|
+
* binary directly via the binaryPath bypass.
|
|
171
|
+
*/
|
|
172
|
+
function runUseExistingGlobalBranch({ binaryPath, global, p }) {
|
|
173
|
+
p.success(
|
|
174
|
+
`Found global skillrepo at ${binaryPath} — using it for session sync.`,
|
|
175
|
+
);
|
|
176
|
+
const r = tryMergeAndPrint({ global, p, binaryPath });
|
|
177
|
+
// Pre-existing global IS active regardless of hook write outcome.
|
|
178
|
+
return { ...r, globalInstallActive: true };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Auto-install branch (the v3.1.2 fix): under npx with no global,
|
|
183
|
+
* install ourselves globally then install the hook with the resulting
|
|
184
|
+
* absolute path.
|
|
185
|
+
*/
|
|
186
|
+
async function runAutoInstallBranch({ yes, json, global, p, deps }) {
|
|
187
|
+
const confirmFn = deps.confirmFn ?? realConfirm;
|
|
188
|
+
const proceed =
|
|
189
|
+
yes ||
|
|
190
|
+
(await confirmFn(
|
|
191
|
+
"SkillRepo needs a global install to enable session sync. " +
|
|
192
|
+
"Install `skillrepo` globally now?",
|
|
193
|
+
true,
|
|
194
|
+
));
|
|
195
|
+
if (!proceed) {
|
|
196
|
+
p.warning("Session sync skipped (declined).");
|
|
197
|
+
printManualSteps(p, detectTransientRunner());
|
|
198
|
+
return result(SessionSyncAction.Declined);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// `getCliVersion` throws on a corrupt CLI tarball. Catch so init
|
|
202
|
+
// doesn't crash on a condition that should never happen but might.
|
|
203
|
+
const getVersionFn = deps.getCliVersion ?? getCliVersion;
|
|
204
|
+
let version;
|
|
205
|
+
try {
|
|
206
|
+
version = getVersionFn();
|
|
207
|
+
} catch (err) {
|
|
208
|
+
p.warning(
|
|
209
|
+
`Could not determine CLI version for global install: ${err?.message ?? String(err)}.`,
|
|
210
|
+
);
|
|
211
|
+
printManualSteps(p, detectTransientRunner());
|
|
212
|
+
return result(SessionSyncAction.Skipped);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// p.success is silenced under --json; in human mode it tells the
|
|
216
|
+
// user what's about to happen before npm's stream begins.
|
|
217
|
+
p.success(`Running: npm install -g skillrepo@${version}`);
|
|
218
|
+
const installResult = await installSkillrepoGlobally({
|
|
219
|
+
version,
|
|
220
|
+
outputMode: json ? "silent" : "inherit",
|
|
221
|
+
spawn: deps.spawn,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
if (!installResult.success) {
|
|
225
|
+
p.warning(
|
|
226
|
+
`Could not install skillrepo globally: ${installResult.error}`,
|
|
227
|
+
);
|
|
228
|
+
printManualSteps(p, detectTransientRunner());
|
|
229
|
+
return result(SessionSyncAction.Skipped);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
p.success(
|
|
233
|
+
`Installed skillrepo@${version} globally (${installResult.binaryPath}).`,
|
|
234
|
+
);
|
|
235
|
+
const r = tryMergeAndPrint({
|
|
236
|
+
global,
|
|
237
|
+
p,
|
|
238
|
+
binaryPath: installResult.binaryPath,
|
|
239
|
+
});
|
|
240
|
+
return { ...r, globalInstallActive: true };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── Shared helpers ───────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Call `mergeSessionHook` and translate the result into a print +
|
|
247
|
+
* structured return. The optional `binaryPath` bypasses the
|
|
248
|
+
* `isNpxInvocation` guard inside `resolveSkillrepoBinary` (the v3.1.2
|
|
249
|
+
* contract that lets npx-mode init still install a hook with a stable
|
|
250
|
+
* absolute path).
|
|
251
|
+
*/
|
|
252
|
+
function tryMergeAndPrint({ global, p, binaryPath }) {
|
|
253
|
+
try {
|
|
254
|
+
const r = mergeSessionHook({ global, binaryPath });
|
|
255
|
+
if (r.action === SessionSyncAction.Installed) {
|
|
256
|
+
p.success(`SessionStart hook installed (${r.path})`);
|
|
257
|
+
} else if (r.action === SessionSyncAction.Updated) {
|
|
258
|
+
p.success(`SessionStart hook updated (${r.path})`);
|
|
259
|
+
} else if (r.action === SessionSyncAction.Unchanged) {
|
|
260
|
+
p.success(`SessionStart hook already installed (${r.path})`);
|
|
261
|
+
} else if (r.action === SessionSyncAction.Skipped) {
|
|
262
|
+
// Reached only by the bare-install path when `which skillrepo`
|
|
263
|
+
// returns nothing (and we didn't auto-install). Surface the
|
|
264
|
+
// helper's actionable reason verbatim.
|
|
265
|
+
p.warning(r.reason ?? "Session sync skipped.");
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
action: r.action,
|
|
269
|
+
path: r.path,
|
|
270
|
+
globalInstallActive: isHookActive(r.action),
|
|
271
|
+
};
|
|
272
|
+
} catch (err) {
|
|
273
|
+
// Disk error (corrupt settings file). The other init steps
|
|
274
|
+
// succeeded — surface and continue.
|
|
275
|
+
p.warning(
|
|
276
|
+
`Session sync failed: ${err?.message ?? String(err)}. ` +
|
|
277
|
+
`Run \`skillrepo session-sync enable\` after fixing the issue.`,
|
|
278
|
+
);
|
|
279
|
+
return result(SessionSyncAction.Failed);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function printManualSteps(p, runnerName) {
|
|
284
|
+
// Use the runner's canonical install command (`pnpm add -g`,
|
|
285
|
+
// `bun add -g`, etc.) so the suggestion matches the package
|
|
286
|
+
// manager the user is actually invoking through. Falls back to
|
|
287
|
+
// `npm install -g` when no runner is detected (non-transient
|
|
288
|
+
// failure path) — the universal command that works alongside
|
|
289
|
+
// every runner.
|
|
290
|
+
const installCmd =
|
|
291
|
+
globalInstallCommandFor(runnerName) ?? "npm install -g skillrepo";
|
|
292
|
+
p.warning(
|
|
293
|
+
"To enable session sync later: " +
|
|
294
|
+
`run \`${installCmd}\` (with sudo if needed), then ` +
|
|
295
|
+
"`skillrepo session-sync enable`.",
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Build a default-shaped step-6 result for an action that didn't
|
|
300
|
+
* install anything (opted-out / not-applicable / declined / skipped).
|
|
301
|
+
* Both `path` and `globalInstallActive` are always falsy in those
|
|
302
|
+
* cases — installed/updated/unchanged use `tryMergeAndPrint`'s
|
|
303
|
+
* return shape directly.
|
|
304
|
+
*/
|
|
305
|
+
function result(action) {
|
|
306
|
+
return { action, path: null, globalInstallActive: false };
|
|
307
|
+
}
|
package/src/commands/init.mjs
CHANGED
|
@@ -43,12 +43,16 @@
|
|
|
43
43
|
import { validateAccessKey } from "../lib/http.mjs";
|
|
44
44
|
import { detectIdes, formatDetectedIdes } from "../lib/detect-ides.mjs";
|
|
45
45
|
import { readConfig, writeConfig } from "../lib/config.mjs";
|
|
46
|
-
import { resolveFlags, effectiveVendors
|
|
46
|
+
import { resolveFlags, effectiveVendors } from "../lib/cli-config.mjs";
|
|
47
|
+
import {
|
|
48
|
+
detectTransientRunner,
|
|
49
|
+
globalInstallCommandFor,
|
|
50
|
+
} from "../lib/transient-runners.mjs";
|
|
47
51
|
import { mergeMcpForVendors, printManualMcpInstructions } from "../lib/mcp-merge.mjs";
|
|
48
52
|
import { runSync } from "../lib/sync.mjs";
|
|
49
53
|
import { mergeEnvLocal } from "../lib/mergers/env-local.mjs";
|
|
50
54
|
import { mergeGitignore } from "../lib/mergers/gitignore.mjs";
|
|
51
|
-
import {
|
|
55
|
+
import { installSessionSyncHook } from "./init-session-sync.mjs";
|
|
52
56
|
import { resolveKeyFromEnvFiles } from "../lib/resolve-key.mjs";
|
|
53
57
|
import {
|
|
54
58
|
promptSecret,
|
|
@@ -138,13 +142,29 @@ const BLACK_HOLE_STREAM = {
|
|
|
138
142
|
* @param {object} [io]
|
|
139
143
|
* @param {NodeJS.WritableStream} [io.stdout=process.stdout]
|
|
140
144
|
* @param {NodeJS.WritableStream} [io.stderr=process.stderr]
|
|
145
|
+
* @param {object} [deps] - Test-only dependency injection. Production
|
|
146
|
+
* callers leave this empty. Forwarded to step 6
|
|
147
|
+
* (`installSessionSyncHook`); see `init-session-sync.mjs` for
|
|
148
|
+
* the supported keys (`spawn`, `getCliVersion`, `confirmFn`).
|
|
141
149
|
*/
|
|
142
|
-
export async function runInit(argv, io = {}) {
|
|
150
|
+
export async function runInit(argv, io = {}, deps = {}) {
|
|
143
151
|
const stdout = io.stdout ?? process.stdout;
|
|
144
152
|
const stderr = io.stderr ?? process.stderr;
|
|
145
153
|
|
|
146
154
|
const { flags, yes, force, noSessionSync } = parseInitFlags(argv);
|
|
147
155
|
|
|
156
|
+
// `--json` is for non-interactive consumers (CI scripts, programmatic
|
|
157
|
+
// callers). Without `--yes`, init would hang at step 5 (MCP merge
|
|
158
|
+
// per-vendor confirm prompt) waiting for stdin that no one is going
|
|
159
|
+
// to provide. Surface that loudly at parse time rather than letting
|
|
160
|
+
// the user discover it via a 5-minute test-runner timeout.
|
|
161
|
+
if (flags.json && !yes) {
|
|
162
|
+
throw validationError(
|
|
163
|
+
"--json requires --yes (init has interactive prompts that --json suppresses).",
|
|
164
|
+
{ hint: "Pass --yes alongside --json for non-interactive runs." },
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
148
168
|
// In --json mode, suppress all step-progress output so stdout
|
|
149
169
|
// carries only the final JSON blob.
|
|
150
170
|
const p = makePrinter(stdout, stderr, flags.json);
|
|
@@ -377,99 +397,29 @@ export async function runInit(argv, io = {}) {
|
|
|
377
397
|
}
|
|
378
398
|
p.blank();
|
|
379
399
|
|
|
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.
|
|
400
|
+
// ── Step 6: Session sync (#884, v3.1.2 auto-install #894) ─────
|
|
410
401
|
//
|
|
411
|
-
//
|
|
412
|
-
// sync
|
|
413
|
-
// (step
|
|
402
|
+
// The full decision tree (six branches) lives in
|
|
403
|
+
// `init-session-sync.mjs`. This step is inserted between MCP
|
|
404
|
+
// merge (step 5) and the first sync (step 7) — order matters
|
|
405
|
+
// because the hook's `skillrepo update` calls expect the config
|
|
406
|
+
// to already be written (step 3).
|
|
414
407
|
p.step(6, 7, "Session sync");
|
|
415
|
-
let sessionSyncAction = "skipped";
|
|
416
|
-
let sessionSyncPath = null;
|
|
417
408
|
const claudeTargeted =
|
|
418
409
|
Boolean(flags.global) ||
|
|
419
410
|
(Array.isArray(vendors) && vendors.includes("claudeCode"));
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
}
|
|
411
|
+
const sessionSync = await installSessionSyncHook({
|
|
412
|
+
noSessionSync,
|
|
413
|
+
claudeTargeted,
|
|
414
|
+
yes,
|
|
415
|
+
json: flags.json,
|
|
416
|
+
global: flags.global,
|
|
417
|
+
p,
|
|
418
|
+
deps,
|
|
419
|
+
});
|
|
420
|
+
const sessionSyncAction = sessionSync.action;
|
|
421
|
+
const sessionSyncPath = sessionSync.path;
|
|
422
|
+
const globalInstallActive = sessionSync.globalInstallActive;
|
|
473
423
|
p.blank();
|
|
474
424
|
|
|
475
425
|
// ── Step 7: First sync ───────────────────────────────────────
|
|
@@ -584,13 +534,9 @@ export async function runInit(argv, io = {}) {
|
|
|
584
534
|
skipped: skipped.map((r) => r.path),
|
|
585
535
|
failed: failed.map((r) => ({ path: r.path, reason: r.reason })),
|
|
586
536
|
},
|
|
587
|
-
// Session-sync block
|
|
588
|
-
//
|
|
589
|
-
//
|
|
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)
|
|
537
|
+
// Session-sync block. `action` is one of the
|
|
538
|
+
// `SessionSyncAction` enum values defined in
|
|
539
|
+
// `./session-sync-actions.mjs` (single source of truth).
|
|
594
540
|
sessionSync: {
|
|
595
541
|
action: sessionSyncAction,
|
|
596
542
|
path: sessionSyncPath,
|
|
@@ -611,24 +557,41 @@ export async function runInit(argv, io = {}) {
|
|
|
611
557
|
}
|
|
612
558
|
|
|
613
559
|
stdout.write("\n ✓ SkillRepo is ready.\n\n");
|
|
614
|
-
// Pick the command prefix the user can actually run.
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
|
|
560
|
+
// Pick the command prefix the user can actually run. The user is
|
|
561
|
+
// either:
|
|
562
|
+
// - running a stable global install → bare `skillrepo list` works
|
|
563
|
+
// - running via a transient runner (npx/pnpx/yarn dlx/bunx) AND
|
|
564
|
+
// the auto-install in step 6 put a global on PATH
|
|
565
|
+
// (`globalInstallActive`) → bare `skillrepo list` ALSO works
|
|
566
|
+
// (subject to PATH refresh on Windows; see the
|
|
567
|
+
// path-not-updated branch in global-install.mjs)
|
|
568
|
+
// - running via a transient runner with NO global → bare
|
|
569
|
+
// `skillrepo` would fail; they need to invoke through their
|
|
570
|
+
// runner. v3.1.2 detects WHICH runner (npx/pnpx/yarn dlx/bunx)
|
|
571
|
+
// so the prefix matches what they actually used.
|
|
572
|
+
const transientRunner = detectTransientRunner();
|
|
573
|
+
const showRunnerPrefix = transientRunner && !globalInstallActive;
|
|
574
|
+
const prefix = showRunnerPrefix ? `${transientRunner} skillrepo` : "skillrepo";
|
|
621
575
|
stdout.write(" Next steps:\n");
|
|
622
576
|
stdout.write(` • ${prefix} list — see what's in your library\n`);
|
|
623
577
|
stdout.write(` • ${prefix} search <query> — find skills\n`);
|
|
624
578
|
stdout.write(` • ${prefix} add @owner/name — add a skill\n`);
|
|
625
|
-
if (
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
579
|
+
if (showRunnerPrefix) {
|
|
580
|
+
// The Tip is only useful when the user is still on a transient
|
|
581
|
+
// runner post-init: it tells them how to switch to a faster,
|
|
582
|
+
// sync-enabled global install. After a successful auto-install
|
|
583
|
+
// (or when a pre-existing global was used), the global IS active
|
|
584
|
+
// and the tip would be stale.
|
|
585
|
+
//
|
|
586
|
+
// Use the runner's CANONICAL global-install command so a `pnpx`
|
|
587
|
+
// user gets `pnpm add -g skillrepo` instead of `npm install -g
|
|
588
|
+
// skillrepo`. Falls back to the universal `npm install -g` for
|
|
589
|
+
// any unrecognized runner.
|
|
590
|
+
const installCmd =
|
|
591
|
+
globalInstallCommandFor(transientRunner) ?? "npm install -g skillrepo";
|
|
629
592
|
stdout.write(
|
|
630
|
-
|
|
631
|
-
"and
|
|
593
|
+
`\n Tip: \`${installCmd}\` for faster commands ` +
|
|
594
|
+
"and session-start sync.\n",
|
|
632
595
|
);
|
|
633
596
|
}
|
|
634
597
|
stdout.write("\n");
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the `sessionSync.action` enum surfaced
|
|
3
|
+
* by `skillrepo init` (#894 / v3.1.2).
|
|
4
|
+
*
|
|
5
|
+
* The enum appears in three places that MUST stay in sync:
|
|
6
|
+
* - the `--json` output's `sessionSync.action` field (consumer
|
|
7
|
+
* contract for CI scripts and programmatic callers)
|
|
8
|
+
* - the human-readable summary at the end of init
|
|
9
|
+
* - the README's `init` flag table
|
|
10
|
+
*
|
|
11
|
+
* Before this module existed, the values were declared inline in a
|
|
12
|
+
* JSDoc comment block and re-typed as string literals at every
|
|
13
|
+
* assignment site — drift risk. Centralizing here means adding a
|
|
14
|
+
* new value is one edit (this file + the consumer that needs it),
|
|
15
|
+
* not a grep-and-pray pass across init.mjs.
|
|
16
|
+
*
|
|
17
|
+
* The enum is `Object.freeze`d so callers can't mutate it. JS
|
|
18
|
+
* doesn't have first-class enums; this is the standard idiom.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {"installed" | "updated" | "unchanged" | "opted-out" | "declined" | "not-applicable" | "skipped" | "failed"} SessionSyncActionValue
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Action enum constants. Use these instead of string literals at
|
|
27
|
+
* assignment sites. Values:
|
|
28
|
+
*
|
|
29
|
+
* - `Installed` — fresh hook write
|
|
30
|
+
* - `Updated` — existing hook found, command differed,
|
|
31
|
+
* replaced in place (e.g. binary path changed)
|
|
32
|
+
* - `Unchanged` — identical hook already present, no write
|
|
33
|
+
* - `OptedOut` — `--no-session-sync` was passed
|
|
34
|
+
* - `Declined` — user said no at the prompt
|
|
35
|
+
* - `NotApplicable` — Claude Code wasn't a vendor target (the hook
|
|
36
|
+
* is Claude-specific, would never fire)
|
|
37
|
+
* - `Skipped` — install offer was accepted but the install
|
|
38
|
+
* failed (npm error, version-read error,
|
|
39
|
+
* prerequisite missing). Distinct from
|
|
40
|
+
* `OptedOut` (user explicit) and `Declined`
|
|
41
|
+
* (user said no).
|
|
42
|
+
* - `Failed` — disk error while writing the settings file
|
|
43
|
+
* (corrupt JSON, permissions, etc.)
|
|
44
|
+
*/
|
|
45
|
+
export const SessionSyncAction = Object.freeze({
|
|
46
|
+
Installed: "installed",
|
|
47
|
+
Updated: "updated",
|
|
48
|
+
Unchanged: "unchanged",
|
|
49
|
+
OptedOut: "opted-out",
|
|
50
|
+
Declined: "declined",
|
|
51
|
+
NotApplicable: "not-applicable",
|
|
52
|
+
Skipped: "skipped",
|
|
53
|
+
Failed: "failed",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* True when the action represents a state where the SessionStart
|
|
58
|
+
* hook is currently in place on disk (and therefore the global
|
|
59
|
+
* `skillrepo` binary referenced by the hook is also active on PATH,
|
|
60
|
+
* because `mergeSessionHook` only emits these actions when it
|
|
61
|
+
* successfully wrote/found a working hook).
|
|
62
|
+
*
|
|
63
|
+
* Used by `init` to suppress the now-stale "install globally"
|
|
64
|
+
* Next-Steps tip after step 6 has put a hook in place.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} action
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
export function isHookActive(action) {
|
|
70
|
+
return (
|
|
71
|
+
action === SessionSyncAction.Installed ||
|
|
72
|
+
action === SessionSyncAction.Updated ||
|
|
73
|
+
action === SessionSyncAction.Unchanged
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Frozen array of all valid `sessionSync.action` string values.
|
|
79
|
+
* Useful for runtime validation in tests and for the JSON-schema-
|
|
80
|
+
* style documentation generator.
|
|
81
|
+
*
|
|
82
|
+
* Built from `Object.values(SessionSyncAction)` so it stays in sync
|
|
83
|
+
* automatically. We use a frozen array (rather than a Set) because
|
|
84
|
+
* `Object.freeze` on a Set does NOT prevent `.add()` from mutating
|
|
85
|
+
* the internal storage — JS frozen-Set semantics are surprising.
|
|
86
|
+
* Frozen arrays ARE immutable end-to-end. Membership checks are
|
|
87
|
+
* `.includes()` instead of `.has()`; the values list is small (8)
|
|
88
|
+
* so the linear scan is irrelevant.
|
|
89
|
+
*/
|
|
90
|
+
export const SESSION_SYNC_ACTION_VALUES = Object.freeze(
|
|
91
|
+
Object.values(SessionSyncAction),
|
|
92
|
+
);
|