@ryanreh99/skills-sync 1.0.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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/assets/contracts/build/bundle.schema.json +76 -0
  4. package/dist/assets/contracts/inputs/config.schema.json +13 -0
  5. package/dist/assets/contracts/inputs/mcp-servers.schema.json +56 -0
  6. package/dist/assets/contracts/inputs/pack-manifest.schema.json +33 -0
  7. package/dist/assets/contracts/inputs/pack-sources.schema.json +47 -0
  8. package/dist/assets/contracts/inputs/profile.schema.json +21 -0
  9. package/dist/assets/contracts/inputs/upstreams.schema.json +45 -0
  10. package/dist/assets/contracts/runtime/targets.schema.json +120 -0
  11. package/dist/assets/contracts/state/upstreams-lock.schema.json +38 -0
  12. package/dist/assets/manifests/targets.linux.json +27 -0
  13. package/dist/assets/manifests/targets.macos.json +27 -0
  14. package/dist/assets/manifests/targets.windows.json +27 -0
  15. package/dist/assets/seed/config.json +3 -0
  16. package/dist/assets/seed/packs/personal/mcp/servers.json +20 -0
  17. package/dist/assets/seed/packs/personal/pack.json +7 -0
  18. package/dist/assets/seed/packs/personal/sources.json +31 -0
  19. package/dist/assets/seed/profiles/personal.json +4 -0
  20. package/dist/assets/seed/upstreams.json +23 -0
  21. package/dist/cli.js +532 -0
  22. package/dist/index.js +27 -0
  23. package/dist/lib/adapters/claude.js +49 -0
  24. package/dist/lib/adapters/codex.js +239 -0
  25. package/dist/lib/adapters/common.js +114 -0
  26. package/dist/lib/adapters/copilot.js +53 -0
  27. package/dist/lib/adapters/cursor.js +53 -0
  28. package/dist/lib/adapters/gemini.js +52 -0
  29. package/dist/lib/agents.js +888 -0
  30. package/dist/lib/bindings.js +510 -0
  31. package/dist/lib/build.js +190 -0
  32. package/dist/lib/bundle.js +165 -0
  33. package/dist/lib/config.js +324 -0
  34. package/dist/lib/core.js +447 -0
  35. package/dist/lib/detect.js +56 -0
  36. package/dist/lib/doctor.js +504 -0
  37. package/dist/lib/init.js +292 -0
  38. package/dist/lib/inventory.js +235 -0
  39. package/dist/lib/manage.js +463 -0
  40. package/dist/lib/mcp-config.js +264 -0
  41. package/dist/lib/profile-transfer.js +221 -0
  42. package/dist/lib/upstreams.js +782 -0
  43. package/docs/agent-storage-map.md +153 -0
  44. package/docs/architecture.md +117 -0
  45. package/docs/changelog.md +12 -0
  46. package/docs/commands.md +94 -0
  47. package/docs/contracts.md +112 -0
  48. package/docs/homebrew.md +46 -0
  49. package/docs/quickstart.md +14 -0
  50. package/docs/roadmap.md +5 -0
  51. package/docs/security.md +32 -0
  52. package/docs/user-guide.md +257 -0
  53. package/package.json +61 -0
@@ -0,0 +1,31 @@
1
+ {
2
+ "imports": [
3
+ {
4
+ "upstream": "anthropic",
5
+ "ref": "main",
6
+ "paths": [
7
+ "skills/frontend-design",
8
+ "skills/skill-creator"
9
+ ],
10
+ "destPrefix": "anthropic"
11
+ },
12
+ {
13
+ "upstream": "openai",
14
+ "ref": "main",
15
+ "paths": [
16
+ "skills/.system/skill-creator",
17
+ "skills/.curated/spreadsheet"
18
+ ],
19
+ "destPrefix": "openai"
20
+ },
21
+ {
22
+ "upstream": "awesome-copilot",
23
+ "ref": "main",
24
+ "paths": [
25
+ "skills/copilot-cli-quickstart",
26
+ "skills/suggest-awesome-github-copilot-skills"
27
+ ],
28
+ "destPrefix": "awesome-copilot"
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "personal",
3
+ "packPath": "workspace/packs/personal"
4
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "upstreams": [
3
+ {
4
+ "id": "anthropic",
5
+ "type": "git",
6
+ "repo": "https://github.com/anthropics/skills.git",
7
+ "defaultRef": "main"
8
+ },
9
+ {
10
+ "id": "openai",
11
+ "type": "git",
12
+ "repo": "https://github.com/openai/skills.git",
13
+ "defaultRef": "main"
14
+ },
15
+ {
16
+ "id": "awesome-copilot",
17
+ "type": "git",
18
+ "repo": "https://github.com/github/awesome-copilot.git",
19
+ "defaultRef": "main"
20
+ }
21
+ ]
22
+ }
23
+
package/dist/cli.js ADDED
@@ -0,0 +1,532 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Command } from "commander";
5
+ import { buildProfile } from "./lib/build.js";
6
+ import { cmdApply, cmdUnlink } from "./lib/bindings.js";
7
+ import { cmdAgentDrift, cmdAgentInventory } from "./lib/agents.js";
8
+ import { cmdDetect } from "./lib/detect.js";
9
+ import { cmdDoctor } from "./lib/doctor.js";
10
+ import { cmdInit } from "./lib/init.js";
11
+ import {
12
+ cmdCurrentProfile,
13
+ cmdListProfiles,
14
+ cmdNewProfile,
15
+ cmdRemoveProfile,
16
+ readDefaultProfile,
17
+ writeDefaultProfile
18
+ } from "./lib/config.js";
19
+ import { cmdListEverything, cmdShowProfileInventory } from "./lib/inventory.js";
20
+ import { cmdProfileExport, cmdProfileImport } from "./lib/profile-transfer.js";
21
+ import {
22
+ cmdProfileAddMcp,
23
+ cmdProfileAddSkill,
24
+ cmdProfileRemoveMcp,
25
+ cmdProfileRemoveSkill,
26
+ cmdUpstreamAdd,
27
+ cmdUpstreamRemove
28
+ } from "./lib/manage.js";
29
+ import { cmdListSkills, cmdListUpstreamContent, cmdListUpstreams, cmdSearchSkills } from "./lib/upstreams.js";
30
+
31
+ const VALID_BUILD_LOCK_MODES = new Set(["read", "write", "refresh"]);
32
+ const KNOWN_ROOT_COMMANDS = new Set([
33
+ "init",
34
+ "build",
35
+ "apply",
36
+ "doctor",
37
+ "unlink",
38
+ "list",
39
+ "search",
40
+ "use",
41
+ "current",
42
+ "ls",
43
+ "new",
44
+ "remove",
45
+ "profile",
46
+ "agent",
47
+ "upstream",
48
+ "detect",
49
+ "help"
50
+ ]);
51
+
52
+ const UNKNOWN_COMMAND_CODE = "skills-sync.unknownCommand";
53
+ const COMMANDER_HELP_CODE = "commander.helpDisplayed";
54
+ const COMMANDER_HELP_CODE_ALT = "commander.help";
55
+ const COMMANDER_VERSION_CODE = "commander.version";
56
+
57
+ function redactPathDetails(message) {
58
+ return String(message ?? "")
59
+ .replace(/[A-Za-z]:\\[^\s'"]+/g, "<path>")
60
+ .replace(/~\/[^\s'"]+/g, "<path>")
61
+ .replace(/\/(?:[^/\s]+\/)+[^/\s]+/g, "<path>")
62
+ .replace(/\b[\w.-]+\.(json|toml|md)\b/g, "<file>");
63
+ }
64
+
65
+ function readPackageVersion() {
66
+ const __filename = fileURLToPath(import.meta.url);
67
+ const packageRoot = path.resolve(path.dirname(__filename), "..");
68
+ const packageJsonPath = path.join(packageRoot, "package.json");
69
+ try {
70
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
71
+ if (typeof packageJson.version === "string" && packageJson.version.trim().length > 0) {
72
+ return packageJson.version.trim();
73
+ }
74
+ } catch {
75
+ // Fallback version keeps --version functional even in atypical environments.
76
+ }
77
+ return "0.0.0";
78
+ }
79
+
80
+ function createUnknownCommandError() {
81
+ const error = new Error("Unknown command");
82
+ error.code = UNKNOWN_COMMAND_CODE;
83
+ return error;
84
+ }
85
+
86
+ function writeUnknownCommandError() {
87
+ process.stderr.write("Unknown command. See: skills-sync help\n");
88
+ }
89
+
90
+ function preflightUnknownCommandCheck(argv) {
91
+ if (argv.length === 0) {
92
+ return;
93
+ }
94
+
95
+ const [root, child] = argv;
96
+ const listChildren = new Set(["skills", "upstreams", "profiles", "everything", "upstream-content"]);
97
+ const searchChildren = new Set(["skills"]);
98
+ const profileChildren = new Set(["show", "add-skill", "remove-skill", "add-mcp", "remove-mcp", "export", "import"]);
99
+ const agentChildren = new Set(["inventory", "drift"]);
100
+ const upstreamChildren = new Set(["add", "remove"]);
101
+
102
+ if (root.startsWith("-")) {
103
+ return;
104
+ }
105
+ if (!KNOWN_ROOT_COMMANDS.has(root)) {
106
+ throw createUnknownCommandError();
107
+ }
108
+ if (root === "list" && child && !child.startsWith("-") && !listChildren.has(child)) {
109
+ throw createUnknownCommandError();
110
+ }
111
+ if (root === "search" && child && !child.startsWith("-") && !searchChildren.has(child)) {
112
+ throw createUnknownCommandError();
113
+ }
114
+ if (root === "profile" && child && !child.startsWith("-") && !profileChildren.has(child)) {
115
+ throw createUnknownCommandError();
116
+ }
117
+ if (root === "agent" && child && !child.startsWith("-") && !agentChildren.has(child)) {
118
+ throw createUnknownCommandError();
119
+ }
120
+ if (root === "upstream" && child && !child.startsWith("-") && !upstreamChildren.has(child)) {
121
+ throw createUnknownCommandError();
122
+ }
123
+ }
124
+
125
+ function parseFormatOption(rawFormat) {
126
+ const format = (rawFormat || "text").toLowerCase();
127
+ if (format !== "text" && format !== "json") {
128
+ throw new Error("Invalid --format value. Use text or json.");
129
+ }
130
+ return format;
131
+ }
132
+
133
+ function collectOptionValues(value, previous) {
134
+ if (Array.isArray(previous)) {
135
+ return [...previous, value];
136
+ }
137
+ return [value];
138
+ }
139
+
140
+ function resolveMcpArgsOption({ args, arg }) {
141
+ const variadicArgs = Array.isArray(args) ? args : [];
142
+ const repeatedArgs = Array.isArray(arg) ? arg : [];
143
+ if (variadicArgs.length > 0 && repeatedArgs.length > 0) {
144
+ throw new Error("Use either --args or repeated --arg, not both.");
145
+ }
146
+ if (repeatedArgs.length > 0) {
147
+ return repeatedArgs;
148
+ }
149
+ return variadicArgs;
150
+ }
151
+
152
+ async function cmdBuild(profileName, lockModeRaw) {
153
+ const lockMode = (lockModeRaw || "write").toLowerCase();
154
+ if (!VALID_BUILD_LOCK_MODES.has(lockMode)) {
155
+ throw new Error("Invalid --lock value. Use read, write, or refresh.");
156
+ }
157
+ const resolved = profileName ?? await readDefaultProfile();
158
+ if (!resolved) {
159
+ throw new Error("--profile is required (or set a default profile with 'skills-sync use <name>').");
160
+ }
161
+ await buildProfile(resolved, { lockMode });
162
+ }
163
+
164
+ async function cmdApplyWithOptionalBuild(profileName, shouldBuild, options = {}) {
165
+ const normalizedProfile = typeof profileName === "string" && profileName.trim().length > 0 ? profileName.trim() : null;
166
+ const resolvedProfile = normalizedProfile ?? await readDefaultProfile();
167
+ if (shouldBuild) {
168
+ if (!resolvedProfile) {
169
+ throw new Error(
170
+ "apply --build requires --profile <name> or a default profile."
171
+ );
172
+ }
173
+ await buildProfile(resolvedProfile, { lockMode: "write" });
174
+ }
175
+ await cmdApply(resolvedProfile, { dryRun: options.dryRun === true });
176
+ }
177
+
178
+ function createProgram() {
179
+ const program = new Command();
180
+ program
181
+ .name("skills-sync")
182
+ .description("Profile-scoped skills + MCP sync for multiple AI agents.")
183
+ .version(readPackageVersion())
184
+ .exitOverride()
185
+ .configureOutput({
186
+ writeOut: (str) => process.stdout.write(str),
187
+ writeErr: (str) => process.stderr.write(str),
188
+ outputError: () => {}
189
+ });
190
+
191
+ program
192
+ .command("init")
193
+ .description("Initialize local workspace state (non-destructive). Use --seed to replace with seed content.")
194
+ .option("--seed", "replace local workspace with bundled seed content")
195
+ .option("--dry-run", "show planned init changes without mutating filesystem")
196
+ .option("--profile <name>", "profile name to scaffold and set as default (default: personal)")
197
+ .action((options) =>
198
+ cmdInit({
199
+ seed: options.seed === true,
200
+ dryRun: options.dryRun === true,
201
+ profile: options.profile
202
+ }));
203
+
204
+ program
205
+ .command("build")
206
+ .description("Build deterministic runtime artifacts from a profile.")
207
+ .option("--profile <name>", "profile name (falls back to default profile)")
208
+ .option("--lock <mode>", "lock mode: read|write|refresh", "write")
209
+ .action((options) => cmdBuild(options.profile, options.lock));
210
+
211
+ program
212
+ .command("apply")
213
+ .description("Bind prebuilt runtime artifacts to tool target paths.")
214
+ .option("--profile <name>", "profile name (optional; defaults to runtime bundle profile)")
215
+ .option("--build", "run build before applying")
216
+ .option("--dry-run", "show planned changes without mutating filesystem")
217
+ .action((options) => cmdApplyWithOptionalBuild(options.profile, options.build === true, { dryRun: options.dryRun === true }));
218
+
219
+ program
220
+ .command("unlink")
221
+ .description("Remove bindings created by apply using state file.")
222
+ .option("--dry-run", "show what unlink would remove without mutating filesystem")
223
+ .action((options) => cmdUnlink({ dryRun: options.dryRun === true }));
224
+
225
+ program
226
+ .command("doctor")
227
+ .description("Validate manifests, state, upstream pins, and materialized runtime artifacts.")
228
+ .option(
229
+ "--profile <name>",
230
+ "profile name for source/upstream validation (falls back to default profile)"
231
+ )
232
+ .action(async (options) => cmdDoctor(options.profile ?? await readDefaultProfile()));
233
+
234
+ program
235
+ .command("detect")
236
+ .description("Detect agent support and installation status.")
237
+ .option("--format <format>", "output format: text|json", "text")
238
+ .option("--agents <agents>", "optional comma-separated agent filter, e.g. codex,claude")
239
+ .action((options) => {
240
+ const format = (options.format || "text").toLowerCase();
241
+ if (format !== "text" && format !== "json") {
242
+ throw new Error("Invalid --format value. Use text or json.");
243
+ }
244
+ return cmdDetect({ format, agents: options.agents });
245
+ });
246
+
247
+ const listCommand = program.command("list").description("List available resources.");
248
+ listCommand
249
+ .command("skills")
250
+ .description("List discovered upstream skills (directories under skills/** containing SKILL.md).")
251
+ .option("--upstream <id>", "upstream id (optional; defaults to all configured upstreams)")
252
+ .option("--ref <ref>", "ref/branch/tag")
253
+ .option("--profile <name>", "profile name to infer upstream/ref defaults")
254
+ .option("--verbose", "include skill titles (slower; reads SKILL.md contents)")
255
+ .option("--versbose", "alias for --verbose")
256
+ .option("--format <format>", "output format: text|json", "text")
257
+ .action((options) => {
258
+ const format = (options.format || "text").toLowerCase();
259
+ if (format !== "text" && format !== "json") {
260
+ throw new Error("Invalid --format value. Use text or json.");
261
+ }
262
+ return cmdListSkills({
263
+ upstream: options.upstream,
264
+ ref: options.ref,
265
+ profile: options.profile,
266
+ format,
267
+ verbose: options.verbose === true || options.versbose === true
268
+ });
269
+ });
270
+ listCommand
271
+ .command("upstreams")
272
+ .description("List configured upstream repositories.")
273
+ .option("--format <format>", "output format: text|json", "text")
274
+ .action((options) => {
275
+ const format = parseFormatOption(options.format);
276
+ return cmdListUpstreams({ format });
277
+ });
278
+ listCommand
279
+ .command("profiles")
280
+ .description("List all profiles discovered in local workspace and seed.")
281
+ .option("--format <format>", "output format: text|json", "text")
282
+ .action((options) => {
283
+ const format = parseFormatOption(options.format);
284
+ return cmdListProfiles({ format });
285
+ });
286
+ listCommand
287
+ .command("everything")
288
+ .description("List all profiles with their effective skills and MCP servers.")
289
+ .option("--format <format>", "output format: text|json", "text")
290
+ .action((options) => {
291
+ const format = parseFormatOption(options.format);
292
+ return cmdListEverything({ format });
293
+ });
294
+ listCommand
295
+ .command("upstream-content")
296
+ .description("List skills and discoverable MCP server manifests available in upstream repository refs.")
297
+ .option("--upstream <id>", "upstream id (optional; defaults to all configured upstreams)")
298
+ .option("--ref <ref>", "ref/branch/tag")
299
+ .option("--profile <name>", "profile name to infer upstream/ref defaults")
300
+ .option("--verbose", "include skill titles (slower; reads SKILL.md contents)")
301
+ .option("--versbose", "alias for --verbose")
302
+ .option("--format <format>", "output format: text|json", "text")
303
+ .action((options) => {
304
+ const format = parseFormatOption(options.format);
305
+ return cmdListUpstreamContent({
306
+ upstream: options.upstream,
307
+ ref: options.ref,
308
+ profile: options.profile,
309
+ format,
310
+ verbose: options.verbose === true || options.versbose === true
311
+ });
312
+ });
313
+
314
+ program
315
+ .command("use <name>")
316
+ .description("Set the default profile; auto-scaffolds an empty local profile if missing.")
317
+ .action((name) => writeDefaultProfile(name));
318
+
319
+ program
320
+ .command("current")
321
+ .description("Print the current default profile name.")
322
+ .action(() => cmdCurrentProfile());
323
+
324
+ program
325
+ .command("ls")
326
+ .description("List all available profiles, marking the current default with ->.")
327
+ .action(() => cmdListProfiles());
328
+
329
+ const profileCommand = program.command("profile").description("Inspect and modify profile-level skill/MCP configuration.");
330
+ profileCommand
331
+ .command("show [name]")
332
+ .description("Show skills and MCP servers for a profile (defaults to current profile).")
333
+ .option("--format <format>", "output format: text|json", "text")
334
+ .action((name, options) => {
335
+ const format = parseFormatOption(options.format);
336
+ return cmdShowProfileInventory({
337
+ profile: name,
338
+ format
339
+ });
340
+ });
341
+ profileCommand
342
+ .command("add-skill <name>")
343
+ .description("Add an upstream skill import to a profile.")
344
+ .requiredOption("--upstream <id>", "upstream id")
345
+ .requiredOption("--path <repoPath>", "upstream repository path, e.g. skills/my-skill")
346
+ .option("--ref <ref>", "ref/branch/tag (defaults to upstream defaultRef)")
347
+ .option("--dest-prefix <prefix>", "destination prefix in bundled skills tree")
348
+ .action((name, options) =>
349
+ cmdProfileAddSkill({
350
+ profile: name,
351
+ upstream: options.upstream,
352
+ skillPath: options.path,
353
+ ref: options.ref,
354
+ destPrefix: options.destPrefix
355
+ }));
356
+ profileCommand
357
+ .command("remove-skill <name>")
358
+ .description("Remove skill import path(s) from a profile.")
359
+ .requiredOption("--upstream <id>", "upstream id")
360
+ .requiredOption("--path <repoPath>", "upstream repository path, e.g. skills/my-skill")
361
+ .option("--ref <ref>", "optional ref filter")
362
+ .option("--dest-prefix <prefix>", "optional destination prefix filter")
363
+ .action((name, options) =>
364
+ cmdProfileRemoveSkill({
365
+ profile: name,
366
+ upstream: options.upstream,
367
+ skillPath: options.path,
368
+ ref: options.ref,
369
+ destPrefix: options.destPrefix
370
+ }));
371
+ profileCommand
372
+ .command("add-mcp <name> <server>")
373
+ .description("Add or update an MCP server in a profile.")
374
+ .option("--command <command>", "server command executable (stdio transport)")
375
+ .option("--url <url>", "server URL (HTTP transport)")
376
+ .option("--args <values...>", "optional command args")
377
+ .option(
378
+ "--arg <value>",
379
+ "single command arg (repeat to include values that start with '-')",
380
+ collectOptionValues,
381
+ []
382
+ )
383
+ .option("--env <entries...>", "optional env vars as KEY=VALUE entries")
384
+ .action((name, server, options) =>
385
+ cmdProfileAddMcp({
386
+ profile: name,
387
+ name: server,
388
+ command: options.command,
389
+ url: options.url,
390
+ args: resolveMcpArgsOption({ args: options.args, arg: options.arg }),
391
+ env: options.env
392
+ }));
393
+ profileCommand
394
+ .command("remove-mcp <name> <server>")
395
+ .description("Remove an MCP server from a profile.")
396
+ .action((name, server) =>
397
+ cmdProfileRemoveMcp({
398
+ profile: name,
399
+ name: server
400
+ }));
401
+ profileCommand
402
+ .command("export [name]")
403
+ .description("Export a profile's config (pack manifest, sources, MCP, and local skills) to JSON.")
404
+ .option("--output <path>", "optional output path (defaults to stdout)")
405
+ .action((name, options) =>
406
+ cmdProfileExport({
407
+ profile: name,
408
+ output: options.output
409
+ }));
410
+ profileCommand
411
+ .command("import <name>")
412
+ .description("Import profile config JSON.")
413
+ .requiredOption("--input <path>", "path to exported profile JSON")
414
+ .option("--replace", "overwrite existing local profile files if present")
415
+ .action((name, options) =>
416
+ cmdProfileImport({
417
+ profile: name,
418
+ input: options.input,
419
+ replace: options.replace === true
420
+ }));
421
+
422
+ const agentCommand = program.command("agent").description("Inspect detected agent installs and profile drift.");
423
+ agentCommand
424
+ .command("inventory")
425
+ .description("Inspect installed skills and MCP servers per detected agent.")
426
+ .option("--agents <agents>", "optional comma-separated agent filter, e.g. codex,claude")
427
+ .option("--format <format>", "output format: text|json", "text")
428
+ .action((options) => {
429
+ const format = parseFormatOption(options.format);
430
+ return cmdAgentInventory({
431
+ format,
432
+ agents: options.agents
433
+ });
434
+ });
435
+ agentCommand
436
+ .command("drift")
437
+ .description("Check or reconcile drift between profile-expected config and detected agent installs.")
438
+ .option("--profile <name>", "profile name (falls back to default profile)")
439
+ .option("--agents <agents>", "optional comma-separated agent filter, e.g. codex,claude")
440
+ .option("--dry-run", "report drift only without mutating files")
441
+ .option("--format <format>", "output format: text|json", "text")
442
+ .action((options) => {
443
+ const format = parseFormatOption(options.format);
444
+ return cmdAgentDrift({
445
+ profile: options.profile,
446
+ dryRun: options.dryRun === true,
447
+ format,
448
+ agents: options.agents
449
+ });
450
+ });
451
+
452
+ const upstreamCommand = program.command("upstream").description("Manage configured upstream repositories.");
453
+ upstreamCommand
454
+ .command("add <id>")
455
+ .description("Add an upstream repository.")
456
+ .requiredOption("--repo <url>", "git repository URL")
457
+ .option("--default-ref <ref>", "default ref/branch/tag", "main")
458
+ .option("--type <type>", "upstream type (currently only git)", "git")
459
+ .action((id, options) =>
460
+ cmdUpstreamAdd({
461
+ id,
462
+ repo: options.repo,
463
+ defaultRef: options.defaultRef,
464
+ type: options.type
465
+ }));
466
+ upstreamCommand
467
+ .command("remove <id>")
468
+ .description("Remove an upstream repository.")
469
+ .action((id) => cmdUpstreamRemove({ id }));
470
+
471
+ program
472
+ .command("new <name>")
473
+ .description("Scaffold a new profile and pack.")
474
+ .action((name) => cmdNewProfile(name));
475
+
476
+ program
477
+ .command("remove <name>")
478
+ .description("Delete a profile definition (pack is preserved).")
479
+ .action((name) => cmdRemoveProfile(name));
480
+
481
+ const searchCommand = program.command("search").description("Search available resources.");
482
+ searchCommand
483
+ .command("skills")
484
+ .description("Search upstream skills by keyword (path by default; path+title with --verbose).")
485
+ .option("--query <text>", "search term")
486
+ .option("--upstream <id>", "upstream id (optional; defaults to all configured upstreams)")
487
+ .option("--ref <ref>", "ref/branch/tag")
488
+ .option("--profile <name>", "profile name to infer upstream/ref defaults")
489
+ .option("--interactive", "interactive prompt mode")
490
+ .option("--verbose", "include title metadata and title matching (slower)")
491
+ .option("--versbose", "alias for --verbose")
492
+ .option("--format <format>", "output format: text|json", "text")
493
+ .action((options) => {
494
+ const format = parseFormatOption(options.format);
495
+ return cmdSearchSkills({
496
+ upstream: options.upstream,
497
+ ref: options.ref,
498
+ profile: options.profile,
499
+ query: options.query,
500
+ format,
501
+ interactive: options.interactive === true,
502
+ verbose: options.verbose === true || options.versbose === true
503
+ });
504
+ });
505
+
506
+ program.command("help").description("Show help.").action(() => program.outputHelp());
507
+ return program;
508
+ }
509
+
510
+ export async function runCli(argv = process.argv.slice(2)) {
511
+ try {
512
+ preflightUnknownCommandCheck(argv);
513
+ const program = createProgram();
514
+ await program.parseAsync(["node", "skills-sync", ...argv]);
515
+ return 0;
516
+ } catch (error) {
517
+ if (
518
+ error?.code === COMMANDER_HELP_CODE ||
519
+ error?.code === COMMANDER_HELP_CODE_ALT ||
520
+ error?.message === "(outputHelp)" ||
521
+ error?.code === COMMANDER_VERSION_CODE
522
+ ) {
523
+ return 0;
524
+ }
525
+ if (error?.code === UNKNOWN_COMMAND_CODE || error?.code === "commander.unknownOption" || error?.code === "commander.unknownCommand") {
526
+ writeUnknownCommandError();
527
+ return 2;
528
+ }
529
+ process.stderr.write(`[skills-sync] ERROR: ${redactPathDetails(error.message)}\n`);
530
+ return 1;
531
+ }
532
+ }
package/dist/index.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { runCli } from "./cli.js";
6
+
7
+ export { runCli };
8
+
9
+ function isMainModule() {
10
+ if (!process.argv[1]) {
11
+ return false;
12
+ }
13
+ const invokedFile = path.resolve(process.argv[1]);
14
+ try {
15
+ return import.meta.url === pathToFileURL(fs.realpathSync(invokedFile)).href;
16
+ } catch {
17
+ return import.meta.url === pathToFileURL(invokedFile).href;
18
+ }
19
+ }
20
+
21
+ if (isMainModule()) {
22
+ runCli().then((exitCode) => {
23
+ if (exitCode !== 0) {
24
+ process.exit(exitCode);
25
+ }
26
+ });
27
+ }
@@ -0,0 +1,49 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
3
+ import { logWarn, readJsonFile } from "../core.js";
4
+ import { linkDirectoryProjection } from "./common.js";
5
+
6
+ export async function projectClaudeFromBundle(options) {
7
+ const {
8
+ runtimeInternalRoot,
9
+ bundleSkillsPath,
10
+ bundleMcpPath,
11
+ packRoot,
12
+ localConfigPath = null,
13
+ canOverride = false
14
+ } = options;
15
+ const runtimeRoot = path.join(runtimeInternalRoot, ".claude");
16
+ await fs.ensureDir(runtimeRoot);
17
+
18
+ const skillsMethod = await linkDirectoryProjection(bundleSkillsPath, path.join(runtimeRoot, "skills"));
19
+ await linkDirectoryProjection(bundleSkillsPath, path.join(runtimeRoot, "vendor_imports", "skills"));
20
+
21
+ const canonicalMcp = await readJsonFile(bundleMcpPath);
22
+ let projected = canonicalMcp;
23
+
24
+ if (!canOverride && localConfigPath && (await fs.pathExists(localConfigPath))) {
25
+ try {
26
+ const existing = await readJsonFile(localConfigPath);
27
+ if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
28
+ throw new Error("expected JSON object root");
29
+ }
30
+ projected = {
31
+ ...existing,
32
+ mcpServers: canonicalMcp?.mcpServers ?? {}
33
+ };
34
+ } catch (error) {
35
+ logWarn(`Failed to seed Claude runtime config from local settings: ${error.message}`);
36
+ }
37
+ }
38
+
39
+ const runtimeConfigPath = path.join(runtimeRoot, "mcp.json");
40
+ await fs.writeFile(runtimeConfigPath, `${JSON.stringify(projected, null, 2)}\n`, "utf8");
41
+ const mcpMethod = "generated";
42
+
43
+ const overrideSource = path.join(packRoot, "tool-overrides", "claude");
44
+ if (await fs.pathExists(overrideSource)) {
45
+ await fs.copy(overrideSource, path.join(runtimeRoot, "tool-overrides"));
46
+ }
47
+
48
+ return { skillsMethod, mcpMethod };
49
+ }