@solaqua/skul 0.1.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 (58) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +282 -0
  3. package/bin/skul.js +13 -0
  4. package/dist/bundle-discovery.d.ts +30 -0
  5. package/dist/bundle-discovery.js +284 -0
  6. package/dist/bundle-discovery.js.map +1 -0
  7. package/dist/bundle-fetch.d.ts +67 -0
  8. package/dist/bundle-fetch.js +378 -0
  9. package/dist/bundle-fetch.js.map +1 -0
  10. package/dist/bundle-items.d.ts +34 -0
  11. package/dist/bundle-items.js +149 -0
  12. package/dist/bundle-items.js.map +1 -0
  13. package/dist/bundle-manifest.d.ts +26 -0
  14. package/dist/bundle-manifest.js +196 -0
  15. package/dist/bundle-manifest.js.map +1 -0
  16. package/dist/bundle-materialization.d.ts +52 -0
  17. package/dist/bundle-materialization.js +587 -0
  18. package/dist/bundle-materialization.js.map +1 -0
  19. package/dist/bundle-translation.d.ts +38 -0
  20. package/dist/bundle-translation.js +502 -0
  21. package/dist/bundle-translation.js.map +1 -0
  22. package/dist/cli.d.ts +126 -0
  23. package/dist/cli.js +648 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/fs-utils.d.ts +7 -0
  26. package/dist/fs-utils.js +27 -0
  27. package/dist/fs-utils.js.map +1 -0
  28. package/dist/git-context.d.ts +14 -0
  29. package/dist/git-context.js +53 -0
  30. package/dist/git-context.js.map +1 -0
  31. package/dist/git-exclude.d.ts +13 -0
  32. package/dist/git-exclude.js +76 -0
  33. package/dist/git-exclude.js.map +1 -0
  34. package/dist/git-index.d.ts +106 -0
  35. package/dist/git-index.js +224 -0
  36. package/dist/git-index.js.map +1 -0
  37. package/dist/index.d.ts +21 -0
  38. package/dist/index.js +4190 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/registry.d.ts +91 -0
  41. package/dist/registry.js +551 -0
  42. package/dist/registry.js.map +1 -0
  43. package/dist/root-instruction-content.d.ts +11 -0
  44. package/dist/root-instruction-content.js +61 -0
  45. package/dist/root-instruction-content.js.map +1 -0
  46. package/dist/root-instruction-render.d.ts +31 -0
  47. package/dist/root-instruction-render.js +121 -0
  48. package/dist/root-instruction-render.js.map +1 -0
  49. package/dist/root-instruction-state.d.ts +41 -0
  50. package/dist/root-instruction-state.js +273 -0
  51. package/dist/root-instruction-state.js.map +1 -0
  52. package/dist/state-layout.d.ts +11 -0
  53. package/dist/state-layout.js +26 -0
  54. package/dist/state-layout.js.map +1 -0
  55. package/dist/tool-mapping.d.ts +35 -0
  56. package/dist/tool-mapping.js +227 -0
  57. package/dist/tool-mapping.js.map +1 -0
  58. package/package.json +72 -0
package/dist/cli.js ADDED
@@ -0,0 +1,648 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isHeadlessMode = isHeadlessMode;
4
+ exports.createHeadlessPromptClient = createHeadlessPromptClient;
5
+ exports.createPromptClient = createPromptClient;
6
+ exports.createPromptClientForSelections = createPromptClientForSelections;
7
+ exports.createHelpText = createHelpText;
8
+ exports.parseCliArgs = parseCliArgs;
9
+ const commander_1 = require("commander");
10
+ const bundle_discovery_1 = require("./bundle-discovery");
11
+ const bundle_items_1 = require("./bundle-items");
12
+ const tool_mapping_1 = require("./tool-mapping");
13
+ const VALID_TOOL_NAMES = new Set((0, tool_mapping_1.listToolDefinitions)().map((t) => t.name));
14
+ const COMMANDS = [
15
+ "add",
16
+ "list",
17
+ "status",
18
+ "check",
19
+ "update",
20
+ "shadow",
21
+ "sync",
22
+ "reset",
23
+ "remove",
24
+ "apply",
25
+ ];
26
+ const PROGRAM_HELP_DETAILS = [
27
+ "",
28
+ "Root instructions:",
29
+ " cursor, codex, opencode, kiro, copilot, and antigravity target AGENTS.md; globally, codex targets .codex/AGENTS.md, opencode targets .config/opencode/AGENTS.md, kiro targets .kiro/steering/AGENTS.md, copilot targets .github/copilot-instructions.md, and antigravity targets .gemini/GEMINI.md; claude-code targets CLAUDE.md.",
30
+ " Untracked root instructions are composed locally and hidden through .git/info/exclude.",
31
+ " Tracked root instructions are rendered from HEAD plus Skul overlay content and marked skip-worktree.",
32
+ "",
33
+ "Safety and recovery:",
34
+ " Use 'skul sync' for fast-forward pulls. For manual git updates, run 'skul shadow --suspend' before and 'skul shadow --refresh' after, as long as the root instruction file is still tracked.",
35
+ " Shadow refresh refuses staged changes, unstaged edits, unmerged files, incompatible index flags, and manual edits to the current shadow render.",
36
+ " 'skul status' reports tracked shadow health; manual edits to shadowed root instruction files are a current limitation.",
37
+ ].join("\n");
38
+ const ADD_HELP_DETAILS = [
39
+ "",
40
+ "Root instruction targets:",
41
+ " Bundles may contribute root instruction files alongside skills/ and commands/ content.",
42
+ " If the target root instruction file is already tracked, Skul creates a tracked shadow instead of leaving a visible git diff.",
43
+ ].join("\n");
44
+ const SHADOW_HELP_DETAILS = [
45
+ "",
46
+ "Lifecycle:",
47
+ " --suspend restores tracked root instruction files from HEAD and clears skip-worktree.",
48
+ " --refresh rebuilds the effective shadow from the latest HEAD plus Skul overlay content and re-enables skip-worktree.",
49
+ " The manual suspend/refresh flow only works when the root instruction file is still tracked after the Git update.",
50
+ " Refresh refuses staged changes, unstaged edits, unmerged files, incompatible index flags, and manual edits to the current shadow render.",
51
+ "",
52
+ "Examples:",
53
+ " skul shadow --suspend",
54
+ " git pull --ff-only",
55
+ " skul shadow --refresh",
56
+ ].join("\n");
57
+ const SYNC_HELP_DETAILS = [
58
+ "",
59
+ "Lifecycle:",
60
+ " Typical workflow: skul shadow --suspend -> git pull --ff-only -> skul shadow --refresh",
61
+ " Prefer this in headless automation so tracked root instruction shadows are suspended and restored in one step.",
62
+ " If git pull fails, Skul attempts to restore the prior shadow state before returning the error.",
63
+ " If upstream stops tracking a shadowed root instruction, sync retires the shadow and removes its local state.",
64
+ ].join("\n");
65
+ let clackPromptsModulePromise;
66
+ // Commander is loaded in CommonJS mode for the CLI entrypoint, so use a tiny
67
+ // dynamic-import bridge for the ESM-only @clack/prompts package.
68
+ const loadEsmModule = new Function("specifier", "return import(specifier);");
69
+ /**
70
+ * Returns true if the CLI should run in headless (non-interactive) mode.
71
+ * Detected via the SKUL_NO_TUI environment variable.
72
+ */
73
+ function isHeadlessMode() {
74
+ return (process.env["SKUL_NO_TUI"] === "1" || process.env["SKUL_NO_TUI"] === "true");
75
+ }
76
+ /**
77
+ * Creates a prompt client that throws immediately instead of opening interactive
78
+ * prompts. Use this when SKUL_NO_TUI is set so agents never block waiting for input.
79
+ */
80
+ function createHeadlessPromptClient() {
81
+ return {
82
+ async selectBundle(source) {
83
+ const hint = source ? `skul add ${source} <bundle>` : "skul add <bundle>";
84
+ throw new Error(`Bundle name is required in headless mode.\nHint: run '${hint}' to specify the bundle explicitly`);
85
+ },
86
+ async selectBundleFromSelections(_availableBundles, source) {
87
+ const hint = source
88
+ ? `skul remove ${source} <bundle>`
89
+ : "skul remove <bundle>";
90
+ throw new Error(`Bundle name is required in headless mode.\nHint: run '${hint}' to specify the bundle explicitly`);
91
+ },
92
+ async selectBundleItems() {
93
+ throw new Error("Bundle item selection requires an interactive terminal.\nHint: rerun with --include <item> instead of --select-items");
94
+ },
95
+ async selectBundleItemChoices() {
96
+ throw new Error("Bundle item selection requires an interactive terminal.\nHint: rerun with --include <item> instead of --select-items");
97
+ },
98
+ async selectAgents(availableAgents) {
99
+ throw new Error(`Agent selection is required in headless mode.\nHint: run 'skul add --agent <name>' to specify agents explicitly. Available: ${availableAgents.join(", ")}`);
100
+ },
101
+ async resolveFileConflict(conflictPath) {
102
+ throw new Error(`Conflict on ${conflictPath}: file already exists (headless mode)\nHint: run interactively to confirm overwrite`);
103
+ },
104
+ async confirmManagedFileRemoval(conflictPath, operation) {
105
+ throw new Error(`Modified managed file blocks ${operation} in headless mode: ${conflictPath}\nHint: run 'skul status' to inspect managed files, or run the command interactively to confirm`);
106
+ },
107
+ };
108
+ }
109
+ /** Creates the interactive prompt client used by the terminal UI. */
110
+ function createPromptClient(availableBundles = []) {
111
+ const bundleSelections = availableBundles.map((bundle) => ({ bundle }));
112
+ return createPromptClientForSelections(bundleSelections);
113
+ }
114
+ /** Creates an interactive prompt client from richer bundle selection metadata. */
115
+ function createPromptClientForSelections(availableBundles, loadPrompts = loadClackPromptsModule) {
116
+ return {
117
+ async selectBundle(source) {
118
+ if (availableBundles.length === 0) {
119
+ throw new Error(source
120
+ ? `No bundles cached for ${source}. Run 'skul add ${source} <bundle>' to fetch one first`
121
+ : "No bundles cached. Run 'skul add <source> <bundle>' to fetch one first");
122
+ }
123
+ const { isCancel, select } = await loadPrompts();
124
+ const choice = await select({
125
+ message: source ? `Select a bundle from ${source}` : "Select a bundle",
126
+ options: availableBundles.map((bundle) => ({
127
+ value: bundle,
128
+ label: formatBundleSelectionLabel(bundle, availableBundles),
129
+ })),
130
+ });
131
+ if (isCancel(choice)) {
132
+ throw new Error("Interactive bundle selection was cancelled");
133
+ }
134
+ return choice;
135
+ },
136
+ async selectBundleFromSelections(availableSelections, source) {
137
+ return createPromptClientForSelections(availableSelections, loadPrompts).selectBundle(source);
138
+ },
139
+ async selectBundleItems(availableItems, selectedItems, purpose = "install") {
140
+ return promptForBundleItemChoices(availableItems.map((item) => ({ value: item, label: item })), selectedItems, purpose, loadPrompts);
141
+ },
142
+ async selectBundleItemChoices(availableItems, selectedItems, purpose = "install") {
143
+ return promptForBundleItemChoices(availableItems, selectedItems, purpose, loadPrompts);
144
+ },
145
+ async selectAgents(availableAgents) {
146
+ if (availableAgents.length === 0) {
147
+ throw new Error("This bundle has no available agents");
148
+ }
149
+ const { isCancel, multiselect } = await loadPrompts();
150
+ const selected = await multiselect({
151
+ message: "Choose agents to install for (Space toggles choices; Enter confirms)",
152
+ options: availableAgents.map((agent) => ({
153
+ value: agent,
154
+ label: agent,
155
+ })),
156
+ required: true,
157
+ });
158
+ if (isCancel(selected)) {
159
+ throw new Error("Agent selection was cancelled");
160
+ }
161
+ return selected;
162
+ },
163
+ async resolveFileConflict(conflictPath) {
164
+ const { isCancel, confirm } = await loadPrompts();
165
+ const shouldOverwrite = await confirm({
166
+ message: `${conflictPath} already exists — overwrite it?`,
167
+ });
168
+ if (isCancel(shouldOverwrite) || !shouldOverwrite) {
169
+ throw new Error(`Conflict not resolved: ${conflictPath} already exists`);
170
+ }
171
+ return { action: "overwrite" };
172
+ },
173
+ async confirmManagedFileRemoval(conflictPath, operation) {
174
+ const { confirm, isCancel } = await loadPrompts();
175
+ const message = operation === "replace"
176
+ ? `Managed file was modified and must be removed before replacement: ${conflictPath}`
177
+ : operation === "remove"
178
+ ? `Managed file was modified and must be removed during bundle removal: ${conflictPath}`
179
+ : `Managed file was modified and must be removed during reset: ${conflictPath}`;
180
+ const confirmed = await confirm({
181
+ message,
182
+ initialValue: false,
183
+ });
184
+ if (isCancel(confirmed)) {
185
+ throw new Error("Managed file removal confirmation was cancelled");
186
+ }
187
+ return confirmed;
188
+ },
189
+ };
190
+ }
191
+ async function promptForBundleItemChoices(availableItems, selectedItems, purpose, loadPrompts) {
192
+ if (availableItems.length === 0) {
193
+ throw new Error("This bundle has no selectable items");
194
+ }
195
+ const { isCancel, multiselect } = await loadPrompts();
196
+ const choice = await multiselect({
197
+ message: `Choose bundle items to ${purpose} (Space toggles choices; Enter confirms)`,
198
+ options: availableItems,
199
+ initialValues: selectedItems,
200
+ required: true,
201
+ });
202
+ if (isCancel(choice)) {
203
+ throw new Error("Interactive bundle item selection was cancelled");
204
+ }
205
+ return choice;
206
+ }
207
+ function loadClackPromptsModule() {
208
+ clackPromptsModulePromise ??= loadEsmModule("@clack/prompts");
209
+ return clackPromptsModulePromise;
210
+ }
211
+ /** Renders CLI help text for either the full program or one subcommand. */
212
+ function createHelpText(command) {
213
+ const output = [];
214
+ const program = createProgram({
215
+ selectBundle: async () => ({ bundle: "" }),
216
+ selectBundleFromSelections: async (availableBundles) => availableBundles[0] ?? { bundle: "" },
217
+ selectBundleItems: async () => [],
218
+ selectBundleItemChoices: async () => [],
219
+ selectAgents: async (agents) => agents,
220
+ resolveFileConflict: async () => ({ action: "overwrite" }),
221
+ confirmManagedFileRemoval: async () => true,
222
+ });
223
+ const writeOutput = (chunk) => {
224
+ output.push(chunk);
225
+ };
226
+ program.configureOutput({
227
+ writeOut: writeOutput,
228
+ writeErr: writeOutput,
229
+ });
230
+ if (command) {
231
+ const subcommand = program.commands.find((c) => c.name() === command);
232
+ if (subcommand) {
233
+ subcommand.configureOutput({
234
+ writeOut: writeOutput,
235
+ writeErr: writeOutput,
236
+ });
237
+ subcommand.outputHelp();
238
+ return output.join("");
239
+ }
240
+ }
241
+ program.outputHelp();
242
+ return output.join("");
243
+ }
244
+ /** Parses CLI arguments into a normalized command payload. */
245
+ async function parseCliArgs(argv, prompts = createPromptClient()) {
246
+ const [rawCommand] = argv;
247
+ const command = rawCommand;
248
+ if (!command ||
249
+ command === "help" ||
250
+ command === "-h" ||
251
+ command === "--help") {
252
+ return { kind: "help" };
253
+ }
254
+ if (!COMMANDS.includes(command)) {
255
+ const suggestion = findClosestCommand(command, COMMANDS);
256
+ const hint = suggestion ? `, did you mean "${suggestion}"?` : "";
257
+ throw new Error(`Unknown command: "${command}"${hint}`);
258
+ }
259
+ const normalizedArgv = rawCommand && rawCommand !== command ? [command, ...argv.slice(1)] : argv;
260
+ const restArgs = normalizedArgv.slice(1);
261
+ if (restArgs.includes("--help") || restArgs.includes("-h")) {
262
+ return { kind: "help", command: command };
263
+ }
264
+ const context = {};
265
+ const program = createProgram(prompts, context);
266
+ try {
267
+ await program.parseAsync(normalizedArgv, { from: "user" });
268
+ }
269
+ catch (error) {
270
+ throw normalizeParseError(error, command);
271
+ }
272
+ return context.result ?? { kind: "help" };
273
+ }
274
+ function collectToolOption(value, previous) {
275
+ if (!VALID_TOOL_NAMES.has(value)) {
276
+ throw new Error(`Unknown tool: ${value}\nValid tools: ${Array.from(VALID_TOOL_NAMES).sort().join(", ")}`);
277
+ }
278
+ if (previous.includes(value)) {
279
+ return previous;
280
+ }
281
+ return [...previous, value];
282
+ }
283
+ function collectBundleItemOption(value, previous) {
284
+ const normalized = (0, bundle_items_1.normalizeBundleItemSelector)(value);
285
+ if (previous.includes(normalized)) {
286
+ return previous;
287
+ }
288
+ return [...previous, normalized];
289
+ }
290
+ function normalizeRefSelector(value) {
291
+ const normalizedValue = value.trim();
292
+ if (normalizedValue.length === 0) {
293
+ throw new Error("Ref selector must be a non-empty string");
294
+ }
295
+ return normalizedValue;
296
+ }
297
+ function resolveRequestedRefSelector(options) {
298
+ if (options.ref !== undefined) {
299
+ return normalizeRefSelector(options.ref);
300
+ }
301
+ return undefined;
302
+ }
303
+ function createProgram(prompts, context = {}) {
304
+ const program = new commander_1.Command();
305
+ program
306
+ .name("skul")
307
+ .description("Manage project-scoped AI configuration bundles\n\nEnv vars:\n SKUL_NO_TUI=1 Run in headless/non-interactive mode (auto-resolves prompts)")
308
+ .addHelpText("after", PROGRAM_HELP_DETAILS)
309
+ .helpCommand(false)
310
+ .configureOutput({
311
+ writeOut: () => undefined,
312
+ writeErr: () => undefined,
313
+ })
314
+ .exitOverride();
315
+ program
316
+ .command("add")
317
+ .description("Add a bundle to the active set and materialize its files or root instructions")
318
+ .argument("[source]", "Bundle source (e.g. github.com/user/repo)")
319
+ .argument("[bundle]", "Bundle name")
320
+ .option("-a, --agent <name>", "Select a specific tool to materialize (repeatable)", collectToolOption, [])
321
+ .option("--ref <selector>", "Track a specific branch, tag, or commit instead of remote HEAD")
322
+ .option("--include <item>", "Install only a bundle item such as skills/name, agents/name, commands/name, or root-instruction (repeatable)", collectBundleItemOption, [])
323
+ .option("--select-items", "Choose bundle items interactively before installing")
324
+ .option("-n, --dry-run", "Preview what would be written without making any changes")
325
+ .option("-s, --ssh", "Clone the bundle source using SSH instead of HTTPS")
326
+ .option("-g, --global", "Install to global tool config under ~/")
327
+ .option("--disable-model-invocation", "Force disable-model-invocation on all skills even when the skill does not set it")
328
+ .addHelpText("after", ADD_HELP_DETAILS)
329
+ .action(async (source, bundle, opts) => {
330
+ const agents = opts.agent;
331
+ const includeItems = opts.include;
332
+ const selectItems = opts.selectItems ?? false;
333
+ const dryRun = opts.dryRun ?? false;
334
+ const global = opts.global ?? false;
335
+ const disableModelInvocation = opts.disableModelInvocation ?? false;
336
+ const ref = resolveRequestedRefSelector(opts);
337
+ if (!source && !bundle) {
338
+ throw new Error("Command add requires a source or bundle name\nHint: run 'skul add <source>' to select a bundle from that source, or 'skul add <bundle>' for a cached unique bundle");
339
+ }
340
+ if (selectItems && disableModelInvocation) {
341
+ throw new Error("--select-items and --disable-model-invocation cannot be used together\nHint: skills not selected in the prompt would remain on disk without the flag, leaving the installation inconsistent");
342
+ }
343
+ if (source && !bundle) {
344
+ // If the single argument looks like a git source (host/owner/repo), treat the
345
+ // repo slug as the bundle name so `skul add github.com/user/react-bundle` works.
346
+ try {
347
+ const detectedProtocol = opts.ssh
348
+ ? "ssh"
349
+ : (0, bundle_discovery_1.detectSourceProtocol)(source);
350
+ const normalizedSource = (0, bundle_discovery_1.normalizeBundleSource)(source);
351
+ const repoSlug = normalizedSource.split("/").at(-1);
352
+ context.result = {
353
+ kind: "command",
354
+ command: "add",
355
+ options: {
356
+ source: normalizedSource,
357
+ bundle: repoSlug,
358
+ protocol: detectedProtocol,
359
+ agents,
360
+ ...(includeItems.length > 0 ? { includeItems } : {}),
361
+ ...(selectItems ? { selectItems } : {}),
362
+ dryRun,
363
+ ...(ref !== undefined ? { ref } : {}),
364
+ inferredBundleFromSource: true,
365
+ global,
366
+ ...(disableModelInvocation ? { disableModelInvocation } : {}),
367
+ },
368
+ };
369
+ }
370
+ catch {
371
+ // Not a valid source — treat as a plain bundle name.
372
+ context.result = {
373
+ kind: "command",
374
+ command: "add",
375
+ options: {
376
+ bundle: source,
377
+ protocol: "https",
378
+ agents,
379
+ ...(includeItems.length > 0 ? { includeItems } : {}),
380
+ ...(selectItems ? { selectItems } : {}),
381
+ dryRun,
382
+ ...(ref !== undefined ? { ref } : {}),
383
+ global,
384
+ ...(disableModelInvocation ? { disableModelInvocation } : {}),
385
+ },
386
+ };
387
+ }
388
+ return;
389
+ }
390
+ const explicitSource = source;
391
+ const detectedProtocol = opts.ssh
392
+ ? "ssh"
393
+ : (0, bundle_discovery_1.detectSourceProtocol)(explicitSource);
394
+ const normalizedSource = (0, bundle_discovery_1.normalizeBundleSource)(explicitSource);
395
+ context.result = {
396
+ kind: "command",
397
+ command: "add",
398
+ options: {
399
+ source: normalizedSource,
400
+ bundle: bundle,
401
+ protocol: detectedProtocol,
402
+ agents,
403
+ ...(includeItems.length > 0 ? { includeItems } : {}),
404
+ ...(selectItems ? { selectItems } : {}),
405
+ dryRun,
406
+ ...(ref !== undefined ? { ref } : {}),
407
+ global,
408
+ ...(disableModelInvocation ? { disableModelInvocation } : {}),
409
+ },
410
+ };
411
+ });
412
+ program
413
+ .command("list")
414
+ .description("List available bundles in the local library")
415
+ .option("-j, --json", "Output as JSON (for scripting and agent use)")
416
+ .option("-s, --source <source>", "Filter results to a single cached source")
417
+ .action((opts) => {
418
+ context.result = {
419
+ kind: "command",
420
+ command: "list",
421
+ options: {
422
+ json: opts.json ?? false,
423
+ ...(opts.source !== undefined
424
+ ? { source: (0, bundle_discovery_1.normalizeBundleSource)(opts.source) }
425
+ : {}),
426
+ },
427
+ };
428
+ });
429
+ program
430
+ .command("status")
431
+ .description("Show desired state, current worktree materialization, and tracked root-instruction shadow health")
432
+ .option("-j, --json", "Output as JSON (for scripting and agent use)")
433
+ .option("-g, --global", "Show global tool config state")
434
+ .action((opts) => {
435
+ context.result = {
436
+ kind: "command",
437
+ command: "status",
438
+ options: { json: opts.json ?? false, global: opts.global ?? false },
439
+ };
440
+ });
441
+ program
442
+ .command("check")
443
+ .description("Check remote-backed bundles for upstream updates")
444
+ .argument("[bundle]", "Bundle name to check")
445
+ .option("-j, --json", "Output as JSON (for scripting and agent use)")
446
+ .action((bundle, opts) => {
447
+ context.result = {
448
+ kind: "command",
449
+ command: "check",
450
+ options: {
451
+ ...(bundle !== undefined ? { bundle } : {}),
452
+ json: opts.json ?? false,
453
+ },
454
+ };
455
+ });
456
+ program
457
+ .command("update")
458
+ .description("Update remote-backed bundles to the latest upstream revision")
459
+ .argument("[bundle]", "Bundle name to update")
460
+ .option("-n, --dry-run", "Preview what would be updated without making any changes")
461
+ .action((bundle, opts) => {
462
+ context.result = {
463
+ kind: "command",
464
+ command: "update",
465
+ options: {
466
+ ...(bundle !== undefined ? { bundle } : {}),
467
+ dryRun: opts.dryRun ?? false,
468
+ },
469
+ };
470
+ });
471
+ const shadowCommand = program
472
+ .command("shadow")
473
+ .description("Suspend or refresh tracked root instruction shadows")
474
+ .option("--suspend", "Restore tracked root instructions from HEAD and clear skip-worktree")
475
+ .option("--refresh", "Rebuild tracked root-instruction shadows from HEAD and re-enable skip-worktree")
476
+ .addHelpText("after", SHADOW_HELP_DETAILS)
477
+ .action((opts) => {
478
+ if (opts.suspend === opts.refresh) {
479
+ throw new Error("Command shadow requires exactly one of --suspend or --refresh");
480
+ }
481
+ context.result = {
482
+ kind: "command",
483
+ command: "shadow",
484
+ options: { action: opts.suspend ? "suspend" : "refresh" },
485
+ };
486
+ });
487
+ const syncCommand = program
488
+ .command("sync")
489
+ .description("Safely pull git updates around tracked root instruction shadows")
490
+ .addHelpText("after", SYNC_HELP_DETAILS)
491
+ .action(() => {
492
+ context.result = { kind: "command", command: "sync", options: {} };
493
+ });
494
+ program
495
+ .command("apply")
496
+ .description("Materialize all desired-state bundles into the current worktree")
497
+ .option("-n, --dry-run", "Preview what would be written without making any changes")
498
+ .option("-g, --global", "Apply global desired-state bundles")
499
+ .action((opts) => {
500
+ context.result = {
501
+ kind: "command",
502
+ command: "apply",
503
+ options: { dryRun: opts.dryRun ?? false, global: opts.global ?? false },
504
+ };
505
+ });
506
+ program
507
+ .command("reset")
508
+ .description("Remove all Skul-managed files and restore tracked root instructions in the current worktree")
509
+ .option("-n, --dry-run", "Preview what would be deleted without removing any files")
510
+ .option("-g, --global", "Reset globally managed tool config")
511
+ .action((opts) => {
512
+ context.result = {
513
+ kind: "command",
514
+ command: "reset",
515
+ options: { dryRun: opts.dryRun ?? false, global: opts.global ?? false },
516
+ };
517
+ });
518
+ program
519
+ .command("remove")
520
+ .description("Remove a bundle from the active set and recompose or restore any root instructions it owns")
521
+ .argument("[source]", "Bundle source (e.g. github.com/user/repo)")
522
+ .argument("[bundle]", "Bundle name to remove")
523
+ .option("--include <item>", "Remove only a bundle item such as skills/name, agents/name, commands/name, or root-instruction (repeatable)", collectBundleItemOption, [])
524
+ .option("--select-items", "Choose bundle items interactively before removing")
525
+ .option("-n, --dry-run", "Preview what would be deleted without removing any files")
526
+ .option("-g, --global", "Remove from globally managed tool config")
527
+ .action((source, bundle, opts) => {
528
+ const includeItems = opts.include;
529
+ const selectItems = opts.selectItems ?? false;
530
+ const dryRun = opts.dryRun ?? false;
531
+ const global = opts.global ?? false;
532
+ if (!source && !bundle) {
533
+ if (selectItems || includeItems.length > 0) {
534
+ context.result = {
535
+ kind: "command",
536
+ command: "remove",
537
+ options: {
538
+ ...(includeItems.length > 0 ? { includeItems } : {}),
539
+ ...(selectItems ? { selectItems } : {}),
540
+ dryRun,
541
+ global,
542
+ },
543
+ };
544
+ return;
545
+ }
546
+ throw new Error("Command remove requires a source or bundle name\nHint: run 'skul remove <source>' to select a bundle from that source, or 'skul remove <bundle>' for an active bundle");
547
+ }
548
+ if (source && !bundle) {
549
+ try {
550
+ const normalizedSource = (0, bundle_discovery_1.normalizeBundleSource)(source);
551
+ const repoSlug = normalizedSource.split("/").at(-1);
552
+ context.result = {
553
+ kind: "command",
554
+ command: "remove",
555
+ options: {
556
+ source: normalizedSource,
557
+ bundle: repoSlug,
558
+ ...(includeItems.length > 0 ? { includeItems } : {}),
559
+ ...(selectItems ? { selectItems } : {}),
560
+ dryRun,
561
+ inferredBundleFromSource: true,
562
+ global,
563
+ },
564
+ };
565
+ }
566
+ catch {
567
+ context.result = {
568
+ kind: "command",
569
+ command: "remove",
570
+ options: {
571
+ bundle: source,
572
+ ...(includeItems.length > 0 ? { includeItems } : {}),
573
+ ...(selectItems ? { selectItems } : {}),
574
+ dryRun,
575
+ global,
576
+ },
577
+ };
578
+ }
579
+ return;
580
+ }
581
+ context.result = {
582
+ kind: "command",
583
+ command: "remove",
584
+ options: {
585
+ source: (0, bundle_discovery_1.normalizeBundleSource)(source),
586
+ bundle: bundle,
587
+ ...(includeItems.length > 0 ? { includeItems } : {}),
588
+ ...(selectItems ? { selectItems } : {}),
589
+ dryRun,
590
+ global,
591
+ },
592
+ };
593
+ });
594
+ return program;
595
+ }
596
+ function normalizeParseError(error, command) {
597
+ if (!(error instanceof commander_1.CommanderError)) {
598
+ return error instanceof Error ? error : new Error(String(error));
599
+ }
600
+ if (error.code === "commander.excessArguments") {
601
+ if (command === "add") {
602
+ return new Error("Command add accepts at most 2 positional arguments");
603
+ }
604
+ if (command === "remove") {
605
+ return new Error("Command remove accepts at most 2 positional arguments");
606
+ }
607
+ if (command === "check" || command === "update") {
608
+ return new Error(`Command ${command} accepts at most 1 positional argument`);
609
+ }
610
+ return new Error(`Command ${command} does not accept positional arguments`);
611
+ }
612
+ return new Error(error.message.replace(/^error: /, ""));
613
+ }
614
+ function levenshtein(a, b) {
615
+ const m = a.length;
616
+ const n = b.length;
617
+ const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
618
+ for (let i = 1; i <= m; i++) {
619
+ for (let j = 1; j <= n; j++) {
620
+ dp[i][j] =
621
+ a[i - 1] === b[j - 1]
622
+ ? dp[i - 1][j - 1]
623
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
624
+ }
625
+ }
626
+ return dp[m][n];
627
+ }
628
+ function findClosestCommand(input, commands) {
629
+ const MAX_DISTANCE = 3;
630
+ let best;
631
+ let bestDistance = Infinity;
632
+ for (const cmd of commands) {
633
+ const d = levenshtein(input, cmd);
634
+ if (d < bestDistance) {
635
+ bestDistance = d;
636
+ best = cmd;
637
+ }
638
+ }
639
+ return bestDistance <= MAX_DISTANCE ? best : undefined;
640
+ }
641
+ function formatBundleSelectionLabel(selection, availableBundles) {
642
+ const hasDuplicateBundleName = availableBundles.some((bundle) => bundle.bundle === selection.bundle && bundle.source !== selection.source);
643
+ if (hasDuplicateBundleName && selection.source) {
644
+ return `${selection.bundle} (${selection.source})`;
645
+ }
646
+ return selection.bundle;
647
+ }
648
+ //# sourceMappingURL=cli.js.map