agentloom 0.1.0 → 0.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.
Files changed (53) hide show
  1. package/README.md +91 -72
  2. package/bin/cli.mjs +3 -2
  3. package/dist/cli.d.ts +1 -0
  4. package/dist/cli.js +149 -17
  5. package/dist/commands/add.d.ts +7 -0
  6. package/dist/commands/add.js +122 -31
  7. package/dist/commands/agent.d.ts +2 -0
  8. package/dist/commands/agent.js +85 -0
  9. package/dist/commands/command.d.ts +2 -0
  10. package/dist/commands/command.js +98 -0
  11. package/dist/commands/delete.d.ts +9 -0
  12. package/dist/commands/delete.js +444 -0
  13. package/dist/commands/entity-utils.d.ts +13 -0
  14. package/dist/commands/entity-utils.js +58 -0
  15. package/dist/commands/find.d.ts +21 -0
  16. package/dist/commands/find.js +944 -0
  17. package/dist/commands/mcp.js +133 -55
  18. package/dist/commands/skills.d.ts +2 -1
  19. package/dist/commands/skills.js +105 -9
  20. package/dist/commands/sync.d.ts +6 -0
  21. package/dist/commands/sync.js +12 -10
  22. package/dist/commands/update.d.ts +7 -0
  23. package/dist/commands/update.js +286 -21
  24. package/dist/core/argv.d.ts +2 -1
  25. package/dist/core/argv.js +42 -2
  26. package/dist/core/commands.d.ts +13 -0
  27. package/dist/core/commands.js +65 -0
  28. package/dist/core/copy.d.ts +6 -0
  29. package/dist/core/copy.js +126 -65
  30. package/dist/core/importer.d.ts +28 -1
  31. package/dist/core/importer.js +1104 -41
  32. package/dist/core/lockfile.js +86 -3
  33. package/dist/core/manage-agents-bootstrap.d.ts +10 -0
  34. package/dist/core/manage-agents-bootstrap.js +40 -0
  35. package/dist/core/manifest.js +7 -1
  36. package/dist/core/router.d.ts +16 -0
  37. package/dist/core/router.js +66 -0
  38. package/dist/core/scope.d.ts +1 -1
  39. package/dist/core/scope.js +12 -8
  40. package/dist/core/settings.d.ts +4 -3
  41. package/dist/core/settings.js +10 -8
  42. package/dist/core/skills.d.ts +23 -0
  43. package/dist/core/skills.js +328 -0
  44. package/dist/core/sources.d.ts +3 -1
  45. package/dist/core/sources.js +31 -1
  46. package/dist/core/telemetry.d.ts +26 -0
  47. package/dist/core/telemetry.js +124 -0
  48. package/dist/core/version-notifier.d.ts +1 -0
  49. package/dist/core/version-notifier.js +22 -4
  50. package/dist/sync/index.d.ts +7 -1
  51. package/dist/sync/index.js +395 -131
  52. package/dist/types.d.ts +16 -1
  53. package/package.json +5 -4
@@ -1,90 +1,491 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { cancel, isCancel, select, text as promptText } from "@clack/prompts";
3
+ import { cancel, isCancel, multiselect, select, text as promptText, } from "@clack/prompts";
4
4
  import { buildAgentMarkdown, parseAgentsDir, targetFileNameForAgent, } from "./agents.js";
5
+ import { normalizeCommandSelector, parseCommandsDir, resolveCommandSelections, } from "./commands.js";
6
+ import { applySkillProviderSideEffects, copySkillArtifacts, normalizeSkillSelector, parseSkillsDir, resolveSkillSelections, skillContentMatchesTarget, } from "./skills.js";
5
7
  import { ensureDir, hashContent, readJsonIfExists, relativePosix, slugify, writeTextAtomic, } from "./fs.js";
8
+ import { ALL_PROVIDERS } from "../types.js";
6
9
  import { readLockfile, upsertLockEntry, writeLockfile } from "./lockfile.js";
7
10
  import { readCanonicalMcp, writeCanonicalMcp } from "./mcp.js";
8
- import { discoverSourceAgentsDir, discoverSourceMcpPath, prepareSource, } from "./sources.js";
11
+ import { discoverSourceAgentsDir, discoverSourceCommandsDir, discoverSourceMcpPath, discoverSourceSkillsDir, prepareSource, } from "./sources.js";
9
12
  export class NonInteractiveConflictError extends Error {
10
13
  constructor(message) {
11
14
  super(message);
12
15
  this.name = "NonInteractiveConflictError";
13
16
  }
14
17
  }
18
+ const MULTISELECT_HELP_TEXT = "↑↓ move, space select, enter confirm";
19
+ function withMultiselectHelp(message) {
20
+ return `${message}\n${MULTISELECT_HELP_TEXT}`;
21
+ }
15
22
  export async function importSource(options) {
23
+ const shouldImportAgents = options.importAgents ?? true;
24
+ const requireAgents = options.requireAgents ?? shouldImportAgents;
25
+ const shouldImportCommands = options.importCommands ?? true;
26
+ const shouldImportMcp = options.importMcp ?? true;
27
+ const shouldImportSkills = options.importSkills ?? false;
28
+ if (!shouldImportAgents &&
29
+ !shouldImportCommands &&
30
+ !shouldImportMcp &&
31
+ !shouldImportSkills) {
32
+ throw new Error("No import targets selected.");
33
+ }
16
34
  const prepared = prepareSource({
17
35
  source: options.source,
18
36
  ref: options.ref,
19
37
  subdir: options.subdir,
20
38
  });
39
+ const normalizedSubdir = options.subdir?.replace(/^\/+|\/+$/g, "");
40
+ const sourceLocation = prepared.spec.type === "github"
41
+ ? `https://github.com/${prepared.spec.source}/tree/${prepared.resolvedCommit}${normalizedSubdir ? `/${normalizedSubdir}` : ""}`
42
+ : options.subdir
43
+ ? `${options.source} (subdir: ${options.subdir})`
44
+ : options.source;
21
45
  try {
22
- const sourceAgentsDir = discoverSourceAgentsDir(prepared.importRoot);
23
- const sourceMcpPath = discoverSourceMcpPath(prepared.importRoot);
24
- const sourceAgents = parseAgentsDir(sourceAgentsDir);
25
- if (sourceAgents.length === 0) {
46
+ const sourceAgentsDir = shouldImportAgents
47
+ ? discoverSourceAgentsDir(prepared.importRoot)
48
+ : null;
49
+ const sourceCommandsDir = shouldImportCommands
50
+ ? discoverSourceCommandsDir(prepared.importRoot)
51
+ : null;
52
+ const sourceMcpPath = shouldImportMcp
53
+ ? discoverSourceMcpPath(prepared.importRoot)
54
+ : null;
55
+ const sourceSkillsDir = shouldImportSkills
56
+ ? discoverSourceSkillsDir(prepared.importRoot)
57
+ : null;
58
+ const sourceAgents = sourceAgentsDir ? parseAgentsDir(sourceAgentsDir) : [];
59
+ const sourceCommands = sourceCommandsDir
60
+ ? parseCommandsDir(sourceCommandsDir)
61
+ : [];
62
+ const sourceMcp = sourceMcpPath
63
+ ? normalizeMcp(readJsonIfExists(sourceMcpPath))
64
+ : null;
65
+ const sourceSkills = sourceSkillsDir ? parseSkillsDir(sourceSkillsDir) : [];
66
+ const hasExplicitCommandSelection = (options.commandSelectors?.length ?? 0) > 0;
67
+ const isAggregateImport = shouldImportAgents &&
68
+ shouldImportCommands &&
69
+ shouldImportMcp &&
70
+ shouldImportSkills;
71
+ if (shouldImportAgents && requireAgents && !sourceAgentsDir) {
72
+ throw new Error(`No source agents directory found under ${prepared.importRoot} (expected agents/ or .agents/agents/).`);
73
+ }
74
+ if (shouldImportAgents && requireAgents && sourceAgents.length === 0) {
26
75
  throw new Error(`No agent files found in ${sourceAgentsDir}.`);
27
76
  }
28
- ensureDir(options.paths.agentsDir);
29
- const importedAgents = [];
30
- const importedAgentHashes = [];
31
- for (const [index, agent] of sourceAgents.entries()) {
32
- let targetFileName = targetFileNameForAgent(agent);
33
- if (options.rename && sourceAgents.length === 1) {
34
- targetFileName = `${slugify(options.rename) || "agent"}.md`;
35
- }
36
- const resolvedFileName = await resolveAgentConflict({
37
- targetFileName,
38
- agentContent: buildAgentMarkdown(agent.frontmatter, agent.body),
39
- paths: options.paths,
77
+ if (shouldImportCommands && options.requireCommands && !sourceCommandsDir) {
78
+ throw new Error(`No source commands directory found under ${prepared.importRoot} (expected .agents/commands/, commands/, or prompts/).`);
79
+ }
80
+ if (shouldImportCommands &&
81
+ options.requireCommands &&
82
+ sourceCommands.length === 0) {
83
+ throw new Error(`No command files found in ${sourceCommandsDir}.`);
84
+ }
85
+ if (shouldImportMcp && options.requireMcp && !sourceMcpPath) {
86
+ throw new Error(`No source mcp.json found under ${prepared.importRoot} (expected mcp.json or .agents/mcp.json).`);
87
+ }
88
+ if (shouldImportSkills && options.requireSkills && !sourceSkillsDir) {
89
+ throw new Error(`No source skills directory found under ${prepared.importRoot} (expected .agents/skills/, skills/, or root SKILL.md).`);
90
+ }
91
+ if (shouldImportSkills &&
92
+ options.requireSkills &&
93
+ sourceSkills.length === 0) {
94
+ throw new Error(`No skills found in ${sourceSkillsDir}.`);
95
+ }
96
+ if (isAggregateImport &&
97
+ sourceAgents.length === 0 &&
98
+ sourceCommands.length === 0 &&
99
+ sourceSkills.length === 0 &&
100
+ Object.keys(sourceMcp?.mcpServers ?? {}).length === 0) {
101
+ throw new Error(`No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, commands/, .agents/commands/, prompts/, mcp.json/.agents/mcp.json, skills/, .agents/skills/, or root SKILL.md.`);
102
+ }
103
+ const shouldResolveAgents = shouldImportAgents &&
104
+ (sourceAgents.length > 0 ||
105
+ (options.agents?.length ?? 0) > 0 ||
106
+ requireAgents);
107
+ const selection = shouldResolveAgents
108
+ ? await resolveAgentsToImport({
109
+ sourceAgents,
110
+ requestedAgents: options.agents,
40
111
  yes: !!options.yes,
41
112
  nonInteractive: !!options.nonInteractive,
42
- promptLabel: `${agent.name} (${index + 1}/${sourceAgents.length})`,
113
+ promptForAgentSelection: options.promptForAgentSelection ?? true,
114
+ selectionMode: options.selectionMode,
115
+ })
116
+ : { selectedAgents: [] };
117
+ let selectedSourceCommands = [];
118
+ let selectedSourceCommandFiles = [];
119
+ let commandSelectionMode = "all";
120
+ if (shouldImportCommands && sourceCommandsDir) {
121
+ const commandSelection = await resolveCommandsToImport({
122
+ sourceCommands,
123
+ selectors: options.commandSelectors ?? [],
124
+ promptForCommands: Boolean(options.promptForCommands),
125
+ nonInteractive: Boolean(options.nonInteractive),
126
+ selectionMode: options.selectionMode,
43
127
  });
44
- if (!resolvedFileName)
45
- continue;
46
- const targetPath = path.join(options.paths.agentsDir, resolvedFileName);
47
- const content = buildAgentMarkdown(agent.frontmatter, agent.body);
48
- writeTextAtomic(targetPath, content);
49
- importedAgents.push(relativePosix(options.paths.agentsRoot, targetPath));
50
- importedAgentHashes.push(hashContent(content));
128
+ selectedSourceCommands = commandSelection.selectedCommands;
129
+ commandSelectionMode = commandSelection.selectionMode;
130
+ selectedSourceCommandFiles = selectedSourceCommands.map((command) => command.fileName);
51
131
  }
52
- const importedMcpServers = [];
53
- if (sourceMcpPath) {
54
- const sourceMcp = normalizeMcp(readJsonIfExists(sourceMcpPath));
55
- const targetMcp = readCanonicalMcp(options.paths);
56
- const merged = await resolveMcpConflict({
132
+ let selectedSourceMcpServers = [];
133
+ let sourceMcpServerNames = sourceMcp
134
+ ? Object.keys(sourceMcp.mcpServers).sort()
135
+ : [];
136
+ let mcpSelectionMode = "all";
137
+ if (shouldImportMcp && sourceMcp) {
138
+ const mcpSelection = await resolveMcpServersToImport({
57
139
  sourceMcp,
58
- targetMcp,
59
- yes: !!options.yes,
60
- nonInteractive: !!options.nonInteractive,
140
+ selectors: options.mcpSelectors ?? [],
141
+ promptForMcp: options.promptForMcp ?? true,
142
+ nonInteractive: Boolean(options.nonInteractive),
143
+ selectionMode: options.selectionMode,
144
+ });
145
+ selectedSourceMcpServers = mcpSelection.selectedServerNames;
146
+ mcpSelectionMode = mcpSelection.selectionMode;
147
+ }
148
+ let selectedSkills = [];
149
+ let selectedSourceSkills = [];
150
+ let skillSelectionMode = "all";
151
+ if (shouldImportSkills && sourceSkillsDir) {
152
+ const skillSelection = await resolveSkillsToImport({
153
+ sourceSkills,
154
+ selectors: options.skillSelectors ?? [],
155
+ promptForSkills: options.promptForSkills ?? true,
156
+ nonInteractive: Boolean(options.nonInteractive),
157
+ selectionMode: options.selectionMode,
61
158
  });
62
- writeCanonicalMcp(options.paths, merged);
63
- importedMcpServers.push(...Object.keys(sourceMcp.mcpServers));
159
+ selectedSkills = skillSelection.selectedSkills;
160
+ skillSelectionMode = skillSelection.selectionMode;
161
+ selectedSourceSkills = selectedSkills.map((skill) => skill.name);
64
162
  }
163
+ const importedAgents = [];
164
+ if (shouldImportAgents && selection.selectedAgents.length > 0) {
165
+ ensureDir(options.paths.agentsDir);
166
+ for (const [index, agent] of selection.selectedAgents.entries()) {
167
+ let targetFileName = targetFileNameForAgent(agent);
168
+ if (options.rename &&
169
+ selection.selectedAgents.length === 1 &&
170
+ !(shouldImportCommands && hasExplicitCommandSelection)) {
171
+ targetFileName = `${slugify(options.rename) || "agent"}.md`;
172
+ }
173
+ const resolvedFileName = await resolveAgentConflict({
174
+ targetFileName,
175
+ agentContent: buildAgentMarkdown(agent.frontmatter, agent.body),
176
+ paths: options.paths,
177
+ yes: !!options.yes,
178
+ nonInteractive: !!options.nonInteractive,
179
+ promptLabel: `${agent.name} (${index + 1}/${selection.selectedAgents.length})`,
180
+ });
181
+ if (!resolvedFileName)
182
+ continue;
183
+ const targetPath = path.join(options.paths.agentsDir, resolvedFileName);
184
+ const content = buildAgentMarkdown(agent.frontmatter, agent.body);
185
+ writeTextAtomic(targetPath, content);
186
+ importedAgents.push(relativePosix(options.paths.agentsRoot, targetPath));
187
+ }
188
+ }
189
+ const importedCommands = [];
190
+ const importedCommandRenameMap = {};
191
+ if (shouldImportCommands && sourceCommandsDir) {
192
+ if (selectedSourceCommands.length > 0) {
193
+ ensureDir(options.paths.commandsDir);
194
+ }
195
+ for (const [index, command] of selectedSourceCommands.entries()) {
196
+ let targetFileName = command.fileName;
197
+ const mappedTargetFileName = resolveMappedTargetFileName(command.fileName, options.commandRenameMap);
198
+ if (mappedTargetFileName) {
199
+ targetFileName = mappedTargetFileName;
200
+ }
201
+ else if (options.rename &&
202
+ selectedSourceCommands.length === 1 &&
203
+ (!shouldImportAgents || hasExplicitCommandSelection)) {
204
+ targetFileName = `${slugify(options.rename) || "command"}.md`;
205
+ }
206
+ const resolvedFileName = await resolveCommandConflict({
207
+ targetFileName,
208
+ commandContent: command.content,
209
+ paths: options.paths,
210
+ yes: !!options.yes,
211
+ nonInteractive: !!options.nonInteractive,
212
+ promptLabel: `${command.fileName} (${index + 1}/${selectedSourceCommands.length})`,
213
+ });
214
+ if (!resolvedFileName)
215
+ continue;
216
+ const targetPath = path.join(options.paths.commandsDir, resolvedFileName);
217
+ writeTextAtomic(targetPath, command.content);
218
+ importedCommands.push(relativePosix(options.paths.agentsRoot, targetPath));
219
+ importedCommandRenameMap[command.fileName] = resolvedFileName;
220
+ }
221
+ }
222
+ const importedMcpServers = [];
223
+ if (shouldImportMcp && sourceMcp) {
224
+ const selectedSourceMcp = {
225
+ version: 1,
226
+ mcpServers: Object.fromEntries(selectedSourceMcpServers.map((serverName) => [
227
+ serverName,
228
+ sourceMcp.mcpServers[serverName] ?? {},
229
+ ])),
230
+ };
231
+ if (selectedSourceMcpServers.length > 0) {
232
+ const targetMcp = readCanonicalMcp(options.paths);
233
+ const merged = await resolveMcpConflict({
234
+ sourceMcp: selectedSourceMcp,
235
+ targetMcp,
236
+ yes: !!options.yes,
237
+ nonInteractive: !!options.nonInteractive,
238
+ });
239
+ writeCanonicalMcp(options.paths, merged);
240
+ }
241
+ importedMcpServers.push(...selectedSourceMcpServers);
242
+ }
243
+ const importedSkills = [];
244
+ const telemetrySkills = [];
245
+ const importedSkillRenameMap = {};
246
+ let skillsProvidersForLock;
247
+ if (shouldImportSkills && sourceSkillsDir) {
248
+ if (selectedSkills.length > 0) {
249
+ ensureDir(options.paths.skillsDir);
250
+ }
251
+ for (const [index, sourceSkill] of selectedSkills.entries()) {
252
+ let targetSkillDirName = slugify(sourceSkill.name) || "skill";
253
+ const mappedTargetSkillDirName = resolveMappedTargetSkillName(sourceSkill.name, options.skillRenameMap);
254
+ if (mappedTargetSkillDirName) {
255
+ targetSkillDirName = mappedTargetSkillDirName;
256
+ }
257
+ else if (options.rename &&
258
+ selectedSkills.length === 1 &&
259
+ importedAgents.length === 0 &&
260
+ importedCommands.length === 0 &&
261
+ importedMcpServers.length === 0) {
262
+ targetSkillDirName = slugify(options.rename) || "skill";
263
+ }
264
+ const resolvedSkillDirName = await resolveSkillConflict({
265
+ sourceSkill,
266
+ targetSkillDirName,
267
+ paths: options.paths,
268
+ yes: !!options.yes,
269
+ nonInteractive: !!options.nonInteractive,
270
+ promptLabel: `${sourceSkill.name} (${index + 1}/${selectedSkills.length})`,
271
+ });
272
+ if (!resolvedSkillDirName)
273
+ continue;
274
+ const targetSkillDir = path.join(options.paths.skillsDir, resolvedSkillDirName);
275
+ if (!skillContentMatchesTarget(sourceSkill, targetSkillDir)) {
276
+ fs.rmSync(targetSkillDir, { recursive: true, force: true });
277
+ copySkillArtifacts(sourceSkill, targetSkillDir);
278
+ }
279
+ importedSkills.push(resolvedSkillDirName);
280
+ telemetrySkills.push({
281
+ name: sourceSkill.name,
282
+ filePath: relativePosix(prepared.rootPath, sourceSkill.skillPath),
283
+ });
284
+ importedSkillRenameMap[sourceSkill.name] = resolvedSkillDirName;
285
+ }
286
+ if (importedSkills.length > 0) {
287
+ const resolvedSkillsProviders = normalizeSkillsProviders(options.skillsProviders && options.skillsProviders.length > 0
288
+ ? options.skillsProviders
289
+ : await options.resolveSkillsProviders?.());
290
+ if (resolvedSkillsProviders && resolvedSkillsProviders.length > 0) {
291
+ applySkillProviderSideEffects({
292
+ paths: options.paths,
293
+ providers: resolvedSkillsProviders,
294
+ warn(message) {
295
+ console.warn(`Warning: ${message}`);
296
+ },
297
+ });
298
+ skillsProvidersForLock = resolvedSkillsProviders;
299
+ }
300
+ }
301
+ }
302
+ const selectedSubsetOfSourceCommands = sourceCommands.length > 0 &&
303
+ selectedSourceCommandFiles.length < sourceCommands.length;
304
+ const selectedSubsetOfSourceMcp = sourceMcpServerNames.length > 0 &&
305
+ selectedSourceMcpServers.length < sourceMcpServerNames.length;
306
+ const selectedSubsetOfSourceSkills = sourceSkills.length > 0 &&
307
+ selectedSourceSkills.length < sourceSkills.length;
308
+ const shouldPersistCommandSelection = shouldImportCommands && commandSelectionMode === "custom";
309
+ const shouldPersistMcpSelection = shouldImportMcp && mcpSelectionMode === "custom";
310
+ const shouldPersistSkillSelection = shouldImportSkills && skillSelectionMode === "custom";
311
+ const selectedSourceCommandsForLock = shouldPersistCommandSelection || selectedSubsetOfSourceCommands
312
+ ? selectedSourceCommandFiles
313
+ : undefined;
314
+ const selectedSourceMcpServersForLock = shouldPersistMcpSelection || selectedSubsetOfSourceMcp
315
+ ? selectedSourceMcpServers
316
+ : undefined;
317
+ const selectedSourceSkillsForLock = shouldPersistSkillSelection || selectedSubsetOfSourceSkills
318
+ ? selectedSourceSkills
319
+ : undefined;
65
320
  const lockfile = readLockfile(options.paths);
321
+ const isCommandOnlyImport = !shouldImportAgents &&
322
+ shouldImportCommands &&
323
+ !shouldImportMcp &&
324
+ !shouldImportSkills;
325
+ const existingEntry = isCommandOnlyImport
326
+ ? findRelaxedCommandEntry(lockfile.entries, {
327
+ source: prepared.spec.source,
328
+ sourceType: prepared.spec.type,
329
+ subdir: options.subdir,
330
+ requestedAgents: options.agents,
331
+ })
332
+ : findMatchingLockEntry(lockfile.entries, {
333
+ source: prepared.spec.source,
334
+ sourceType: prepared.spec.type,
335
+ subdir: options.subdir,
336
+ requestedAgents: shouldImportAgents
337
+ ? selection.requestedAgentsForLock
338
+ : options.agents,
339
+ selectedSourceCommands: selectedSourceCommandsForLock,
340
+ selectedSourceMcpServers: selectedSourceMcpServersForLock,
341
+ selectedSourceSkills: selectedSourceSkillsForLock,
342
+ skillsProviders: skillsProvidersForLock,
343
+ });
344
+ const shouldMergeCommandOnlyEntry = isCommandOnlyImport &&
345
+ Boolean(existingEntry) &&
346
+ (existingEntry?.importedAgents.length ?? 0) === 0 &&
347
+ (existingEntry?.importedMcpServers.length ?? 0) === 0 &&
348
+ (existingEntry?.importedSkills.length ?? 0) === 0;
349
+ const lockImportedAgents = shouldImportAgents
350
+ ? importedAgents
351
+ : (existingEntry?.importedAgents ?? []);
352
+ const lockImportedCommands = shouldImportCommands
353
+ ? shouldMergeCommandOnlyEntry
354
+ ? uniqueStrings([
355
+ ...(existingEntry?.importedCommands ?? []),
356
+ ...importedCommands,
357
+ ])
358
+ : importedCommands
359
+ : (existingEntry?.importedCommands ?? []);
360
+ const lockImportedMcpServers = shouldImportMcp
361
+ ? importedMcpServers
362
+ : (existingEntry?.importedMcpServers ?? []);
363
+ const lockImportedSkills = shouldImportSkills
364
+ ? importedSkills
365
+ : (existingEntry?.importedSkills ?? []);
366
+ let lockSelectedSourceCommands;
367
+ if (shouldImportCommands) {
368
+ if (shouldMergeCommandOnlyEntry) {
369
+ if (shouldPersistCommandSelection || selectedSubsetOfSourceCommands) {
370
+ lockSelectedSourceCommands = uniqueStrings([
371
+ ...(existingEntry?.selectedSourceCommands ?? []),
372
+ ...selectedSourceCommandFiles,
373
+ ]);
374
+ }
375
+ else {
376
+ lockSelectedSourceCommands = undefined;
377
+ }
378
+ }
379
+ else if (shouldPersistCommandSelection ||
380
+ selectedSubsetOfSourceCommands) {
381
+ lockSelectedSourceCommands = [...selectedSourceCommandFiles];
382
+ }
383
+ else {
384
+ lockSelectedSourceCommands = undefined;
385
+ }
386
+ }
387
+ else {
388
+ lockSelectedSourceCommands = existingEntry?.selectedSourceCommands;
389
+ }
390
+ const lockSelectedSourceMcpServers = shouldImportMcp
391
+ ? shouldPersistMcpSelection || selectedSubsetOfSourceMcp
392
+ ? [...selectedSourceMcpServers]
393
+ : undefined
394
+ : existingEntry?.selectedSourceMcpServers;
395
+ const lockSelectedSourceSkills = shouldImportSkills
396
+ ? shouldPersistSkillSelection || selectedSubsetOfSourceSkills
397
+ ? [...selectedSourceSkills]
398
+ : undefined
399
+ : existingEntry?.selectedSourceSkills;
400
+ const lockSkillsProviders = shouldImportSkills
401
+ ? (skillsProvidersForLock ?? existingEntry?.skillsProviders)
402
+ : existingEntry?.skillsProviders;
403
+ const lockSkillRenameMap = shouldImportSkills
404
+ ? normalizeSkillRenameMap(importedSkillRenameMap)
405
+ : existingEntry?.skillRenameMap;
406
+ let lockCommandRenameMap;
407
+ if (shouldImportCommands) {
408
+ if (shouldMergeCommandOnlyEntry) {
409
+ lockCommandRenameMap = mergeCommandRenameMaps(existingEntry?.commandRenameMap, importedCommandRenameMap);
410
+ }
411
+ else {
412
+ lockCommandRenameMap = normalizeCommandRenameMap(importedCommandRenameMap);
413
+ }
414
+ }
415
+ else {
416
+ lockCommandRenameMap = existingEntry?.commandRenameMap;
417
+ }
418
+ const lockRequestedAgents = shouldImportAgents
419
+ ? selection.requestedAgentsForLock
420
+ : existingEntry?.requestedAgents;
421
+ const trackedEntities = computeTrackedEntitiesForLock({
422
+ requestedAgents: lockRequestedAgents,
423
+ importedAgents: lockImportedAgents,
424
+ importedCommands: lockImportedCommands,
425
+ selectedSourceCommands: lockSelectedSourceCommands,
426
+ commandRenameMap: lockCommandRenameMap,
427
+ importedMcpServers: lockImportedMcpServers,
428
+ selectedSourceMcpServers: lockSelectedSourceMcpServers,
429
+ importedSkills: lockImportedSkills,
430
+ selectedSourceSkills: lockSelectedSourceSkills,
431
+ skillsProviders: lockSkillsProviders,
432
+ skillRenameMap: lockSkillRenameMap,
433
+ });
66
434
  const contentHash = hashContent(JSON.stringify({
67
- agents: importedAgentHashes,
68
- mcp: importedMcpServers,
435
+ agents: lockImportedAgents,
436
+ commands: lockImportedCommands,
437
+ selectedSourceCommands: lockSelectedSourceCommands ?? [],
438
+ commandRenameMap: lockCommandRenameMap ?? {},
439
+ mcp: lockImportedMcpServers,
440
+ selectedSourceMcpServers: lockSelectedSourceMcpServers ?? [],
441
+ skills: lockImportedSkills,
442
+ selectedSourceSkills: lockSelectedSourceSkills ?? [],
443
+ skillsProviders: lockSkillsProviders ?? [],
444
+ skillRenameMap: lockSkillRenameMap ?? {},
445
+ trackedEntities: trackedEntities ?? [],
69
446
  }));
70
447
  const lockEntry = {
71
448
  source: prepared.spec.source,
72
449
  sourceType: prepared.spec.type,
73
450
  requestedRef: options.ref,
451
+ requestedAgents: lockRequestedAgents,
74
452
  resolvedCommit: prepared.resolvedCommit,
75
453
  subdir: options.subdir,
76
454
  importedAt: new Date().toISOString(),
77
- importedAgents,
78
- importedMcpServers,
455
+ importedAgents: lockImportedAgents,
456
+ importedCommands: lockImportedCommands,
457
+ selectedSourceCommands: lockSelectedSourceCommands,
458
+ commandRenameMap: lockCommandRenameMap,
459
+ importedMcpServers: lockImportedMcpServers,
460
+ selectedSourceMcpServers: lockSelectedSourceMcpServers,
461
+ importedSkills: lockImportedSkills,
462
+ selectedSourceSkills: lockSelectedSourceSkills,
463
+ skillsProviders: lockSkillsProviders,
464
+ skillRenameMap: lockSkillRenameMap,
465
+ trackedEntities,
79
466
  contentHash,
80
467
  };
81
- upsertLockEntry(lockfile, lockEntry);
468
+ if (existingEntry) {
469
+ const existingIndex = lockfile.entries.indexOf(existingEntry);
470
+ if (existingIndex >= 0) {
471
+ lockfile.entries[existingIndex] = lockEntry;
472
+ }
473
+ else {
474
+ upsertLockEntry(lockfile, lockEntry);
475
+ }
476
+ }
477
+ else {
478
+ upsertLockEntry(lockfile, lockEntry);
479
+ }
82
480
  writeLockfile(options.paths, lockfile);
83
481
  return {
84
482
  source: prepared.spec.source,
85
483
  sourceType: prepared.spec.type,
86
484
  importedAgents,
485
+ importedCommands,
87
486
  importedMcpServers,
487
+ importedSkills,
488
+ telemetrySkills: telemetrySkills.length > 0 ? telemetrySkills : undefined,
88
489
  resolvedCommit: prepared.resolvedCommit,
89
490
  };
90
491
  }
@@ -92,6 +493,364 @@ export async function importSource(options) {
92
493
  prepared.cleanup();
93
494
  }
94
495
  }
496
+ async function resolveAgentsToImport(options) {
497
+ const requestedAgents = normalizeRequestedAgents(options.requestedAgents);
498
+ if (requestedAgents && requestedAgents.length > 0) {
499
+ const selected = resolveRequestedAgents(options.sourceAgents, requestedAgents);
500
+ if (selected.length === 0) {
501
+ throw new Error("No agents matched the requested --agent filters.");
502
+ }
503
+ return {
504
+ selectedAgents: selected,
505
+ // Persist stable selectors so updates keep targeting the same source agents
506
+ // even if a previously-fallback selector later gains a new exact match.
507
+ requestedAgentsForLock: getStableRequestedAgentsForLock({
508
+ selectedAgents: selected,
509
+ sourceAgents: options.sourceAgents,
510
+ }),
511
+ };
512
+ }
513
+ const selectionMode = await resolveSelectionMode({
514
+ entityLabel: "agents",
515
+ selectionMode: options.selectionMode,
516
+ promptForSelection: options.promptForAgentSelection,
517
+ nonInteractive: options.nonInteractive,
518
+ });
519
+ if (selectionMode === "all") {
520
+ return { selectedAgents: options.sourceAgents };
521
+ }
522
+ if (options.yes ||
523
+ options.nonInteractive ||
524
+ options.sourceAgents.length <= 1 ||
525
+ !options.promptForAgentSelection) {
526
+ return {
527
+ selectedAgents: options.sourceAgents,
528
+ requestedAgentsForLock: getStableRequestedAgentsForLock({
529
+ selectedAgents: options.sourceAgents,
530
+ sourceAgents: options.sourceAgents,
531
+ }),
532
+ };
533
+ }
534
+ const selected = await promptAgentSelection(options.sourceAgents);
535
+ if (selected.length === 0) {
536
+ throw new Error("No agents selected. Use --agent <name> or rerun and select at least one agent.");
537
+ }
538
+ const requestedAgentsForLock = getStableRequestedAgentsForLock({
539
+ selectedAgents: selected,
540
+ sourceAgents: options.sourceAgents,
541
+ });
542
+ return { selectedAgents: selected, requestedAgentsForLock };
543
+ }
544
+ function normalizeRequestedAgents(requestedAgents) {
545
+ if (!requestedAgents || requestedAgents.length === 0)
546
+ return undefined;
547
+ const normalized = requestedAgents.map((item) => item.trim()).filter(Boolean);
548
+ return normalized.length > 0 ? [...new Set(normalized)] : undefined;
549
+ }
550
+ function getStableRequestedAgentsForLock(options) {
551
+ const nameCounts = new Map();
552
+ for (const agent of options.sourceAgents) {
553
+ const normalizedName = normalizeAgentSelector(agent.name);
554
+ if (!normalizedName)
555
+ continue;
556
+ nameCounts.set(normalizedName, (nameCounts.get(normalizedName) ?? 0) + 1);
557
+ }
558
+ const selectors = options.selectedAgents.map((agent) => {
559
+ const normalizedName = normalizeAgentSelector(agent.name);
560
+ if (normalizedName && nameCounts.get(normalizedName) === 1) {
561
+ return agent.name;
562
+ }
563
+ return agent.fileName.replace(/\.md$/i, "");
564
+ });
565
+ return normalizeRequestedAgents(selectors);
566
+ }
567
+ function resolveRequestedAgents(sourceAgents, requestedAgents) {
568
+ const selectedPaths = new Set();
569
+ const missing = [];
570
+ const ambiguous = [];
571
+ for (const requestedAgent of requestedAgents) {
572
+ const exactMatches = sourceAgents.filter((agent) => agentMatchesSelector(agent, requestedAgent, false));
573
+ const matches = exactMatches.length > 0
574
+ ? exactMatches
575
+ : sourceAgents.filter((agent) => agentMatchesSelector(agent, requestedAgent, true));
576
+ if (matches.length === 0) {
577
+ missing.push(requestedAgent);
578
+ continue;
579
+ }
580
+ if (matches.length > 1) {
581
+ ambiguous.push(`${requestedAgent} (${matches.map((agent) => agent.name).join(", ")})`);
582
+ continue;
583
+ }
584
+ if (matches[0]) {
585
+ selectedPaths.add(matches[0].sourcePath);
586
+ }
587
+ }
588
+ if (missing.length > 0) {
589
+ throw new Error(`Requested agent(s) not found: ${missing.join(", ")}. Available agents: ${sourceAgents.map((agent) => agent.name).join(", ")}.`);
590
+ }
591
+ if (ambiguous.length > 0) {
592
+ throw new Error(`Requested agent selector is ambiguous: ${ambiguous.join("; ")}. Use the exact frontmatter name.`);
593
+ }
594
+ return sourceAgents.filter((agent) => selectedPaths.has(agent.sourcePath));
595
+ }
596
+ function agentMatchesSelector(agent, selector, includeSlugFallback) {
597
+ const normalizedSelector = normalizeAgentSelector(selector);
598
+ if (!normalizedSelector)
599
+ return false;
600
+ if (getExactAgentSelectorCandidates(agent).has(normalizedSelector)) {
601
+ return true;
602
+ }
603
+ if (!includeSlugFallback)
604
+ return false;
605
+ const selectorSlug = normalizeAgentSelector(slugify(selector));
606
+ return (selectorSlug !== "" &&
607
+ getSlugAgentSelectorCandidates(agent).has(selectorSlug));
608
+ }
609
+ function getExactAgentSelectorCandidates(agent) {
610
+ const fileBaseName = agent.fileName.replace(/\.md$/i, "");
611
+ return new Set([
612
+ agent.name,
613
+ fileBaseName,
614
+ path.basename(agent.sourcePath).replace(/\.md$/i, ""),
615
+ ]
616
+ .map(normalizeAgentSelector)
617
+ .filter(Boolean));
618
+ }
619
+ function getSlugAgentSelectorCandidates(agent) {
620
+ const fileBaseName = agent.fileName.replace(/\.md$/i, "");
621
+ return new Set([
622
+ slugify(agent.name),
623
+ slugify(fileBaseName),
624
+ slugify(path.basename(agent.sourcePath).replace(/\.md$/i, "")),
625
+ ]
626
+ .map(normalizeAgentSelector)
627
+ .filter(Boolean));
628
+ }
629
+ function normalizeAgentSelector(value) {
630
+ return value.trim().toLowerCase();
631
+ }
632
+ async function promptAgentSelection(sourceAgents) {
633
+ const choice = await multiselect({
634
+ message: withMultiselectHelp("Select agents to import"),
635
+ options: sourceAgents.map((agent) => ({
636
+ value: agent.sourcePath,
637
+ label: agent.name,
638
+ hint: agent.description,
639
+ })),
640
+ initialValues: sourceAgents.map((agent) => agent.sourcePath),
641
+ });
642
+ if (isCancel(choice)) {
643
+ cancel("Operation cancelled.");
644
+ process.exit(1);
645
+ }
646
+ const selected = new Set(Array.isArray(choice) ? choice.map(String) : []);
647
+ return sourceAgents.filter((agent) => selected.has(agent.sourcePath));
648
+ }
649
+ function findMatchingLockEntry(entries, key) {
650
+ return entries.find((entry) => entry.source === key.source &&
651
+ entry.sourceType === key.sourceType &&
652
+ entry.subdir === key.subdir &&
653
+ sameRequestedAgentsForMatch(entry.requestedAgents, key.requestedAgents) &&
654
+ sameStringSelectionForMatch(entry.selectedSourceCommands, key.selectedSourceCommands) &&
655
+ sameStringSelectionForMatch(entry.selectedSourceMcpServers, key.selectedSourceMcpServers) &&
656
+ sameStringSelectionForMatch(entry.selectedSourceSkills, key.selectedSourceSkills, { wildcardWhenRightIsUndefined: true }) &&
657
+ sameStringSelectionForMatch(entry.skillsProviders, key.skillsProviders, {
658
+ wildcardWhenRightIsUndefined: true,
659
+ }));
660
+ }
661
+ function findRelaxedCommandEntry(entries, key) {
662
+ const matches = entries.filter((entry) => entry.source === key.source &&
663
+ entry.sourceType === key.sourceType &&
664
+ entry.subdir === key.subdir &&
665
+ sameRequestedAgentsForMatch(entry.requestedAgents, key.requestedAgents));
666
+ if (matches.length === 0)
667
+ return undefined;
668
+ const mixed = matches.find((entry) => entry.importedAgents.length > 0 ||
669
+ entry.importedMcpServers.length > 0 ||
670
+ entry.importedSkills.length > 0);
671
+ return mixed ?? matches[0];
672
+ }
673
+ function sameRequestedAgentsForMatch(left, right) {
674
+ const normalizedLeft = normalizeRequestedAgentsForMatch(left);
675
+ const normalizedRight = normalizeRequestedAgentsForMatch(right);
676
+ if (normalizedLeft.length !== normalizedRight.length) {
677
+ return false;
678
+ }
679
+ return normalizedLeft.every((value, index) => value === normalizedRight[index]);
680
+ }
681
+ function normalizeRequestedAgentsForMatch(value) {
682
+ if (!Array.isArray(value) || value.length === 0)
683
+ return [];
684
+ return [
685
+ ...new Set(value.map((item) => item.trim().toLowerCase()).filter(Boolean)),
686
+ ].sort();
687
+ }
688
+ function sameStringSelectionForMatch(left, right, options = {}) {
689
+ if (options.wildcardWhenRightIsUndefined && right === undefined) {
690
+ return true;
691
+ }
692
+ const normalizedLeft = normalizeRequestedAgentsForMatch(left);
693
+ const normalizedRight = normalizeRequestedAgentsForMatch(right);
694
+ if (normalizedLeft.length !== normalizedRight.length) {
695
+ return false;
696
+ }
697
+ return normalizedLeft.every((value, index) => value === normalizedRight[index]);
698
+ }
699
+ function uniqueStrings(values) {
700
+ return [...new Set(values)];
701
+ }
702
+ function computeTrackedEntitiesForLock(options) {
703
+ const tracked = [];
704
+ if (options.importedAgents.length > 0 ||
705
+ (options.requestedAgents?.length ?? 0) > 0) {
706
+ tracked.push("agent");
707
+ }
708
+ if (options.importedCommands.length > 0 ||
709
+ (options.selectedSourceCommands?.length ?? 0) > 0 ||
710
+ Object.keys(options.commandRenameMap ?? {}).length > 0) {
711
+ tracked.push("command");
712
+ }
713
+ if (options.importedMcpServers.length > 0 ||
714
+ (options.selectedSourceMcpServers?.length ?? 0) > 0) {
715
+ tracked.push("mcp");
716
+ }
717
+ if (options.importedSkills.length > 0 ||
718
+ (options.selectedSourceSkills?.length ?? 0) > 0 ||
719
+ (options.skillsProviders?.length ?? 0) > 0 ||
720
+ Object.keys(options.skillRenameMap ?? {}).length > 0) {
721
+ tracked.push("skill");
722
+ }
723
+ return tracked.length > 0 ? tracked : undefined;
724
+ }
725
+ function normalizeCommandRenameMap(renameMap) {
726
+ if (!renameMap)
727
+ return undefined;
728
+ const normalized = Object.fromEntries(Object.entries(renameMap).filter(([sourceFileName, importedFileName]) => sourceFileName.trim().length > 0 && importedFileName.trim().length > 0));
729
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
730
+ }
731
+ function mergeCommandRenameMaps(existing, updates) {
732
+ const merged = {
733
+ ...(existing ?? {}),
734
+ ...(updates ?? {}),
735
+ };
736
+ return normalizeCommandRenameMap(merged);
737
+ }
738
+ function resolveMappedTargetFileName(sourceFileName, renameMap) {
739
+ if (!renameMap)
740
+ return undefined;
741
+ const normalizedSourceName = normalizeCommandSelector(sourceFileName);
742
+ for (const [sourceSelector, importedName] of Object.entries(renameMap)) {
743
+ if (normalizeCommandSelector(sourceSelector) !== normalizedSourceName) {
744
+ continue;
745
+ }
746
+ const importedBaseName = path.basename(importedName.trim());
747
+ if (!importedBaseName)
748
+ return undefined;
749
+ const importedExtension = path.extname(importedBaseName);
750
+ if (importedExtension)
751
+ return importedBaseName;
752
+ const sourceExtension = path.extname(sourceFileName) || ".md";
753
+ return `${slugify(importedBaseName) || "command"}${sourceExtension}`;
754
+ }
755
+ return undefined;
756
+ }
757
+ function normalizeSkillRenameMap(renameMap) {
758
+ if (!renameMap)
759
+ return undefined;
760
+ const normalizedEntries = Object.entries(renameMap)
761
+ .map(([sourceSelector, importedName]) => {
762
+ const normalizedSourceSelector = normalizeSkillSelector(sourceSelector);
763
+ const normalizedImportedName = slugify(path.basename(importedName.trim()));
764
+ if (!normalizedSourceSelector || !normalizedImportedName) {
765
+ return null;
766
+ }
767
+ return [normalizedSourceSelector, normalizedImportedName];
768
+ })
769
+ .filter((entry) => entry !== null);
770
+ if (normalizedEntries.length === 0)
771
+ return undefined;
772
+ return Object.fromEntries(normalizedEntries);
773
+ }
774
+ function resolveMappedTargetSkillName(sourceSkillName, renameMap) {
775
+ if (!renameMap)
776
+ return undefined;
777
+ const normalizedSourceName = normalizeSkillSelector(sourceSkillName);
778
+ if (!normalizedSourceName)
779
+ return undefined;
780
+ for (const [sourceSelector, importedName] of Object.entries(renameMap)) {
781
+ if (normalizeSkillSelector(sourceSelector) !== normalizedSourceName) {
782
+ continue;
783
+ }
784
+ return slugify(path.basename(importedName.trim())) || "skill";
785
+ }
786
+ return undefined;
787
+ }
788
+ function normalizeSkillsProviders(providers) {
789
+ if (!providers || providers.length === 0)
790
+ return undefined;
791
+ const selected = new Set();
792
+ for (const provider of providers) {
793
+ const normalized = provider.trim().toLowerCase();
794
+ if (ALL_PROVIDERS.includes(normalized)) {
795
+ selected.add(normalized);
796
+ }
797
+ }
798
+ return selected.size > 0 ? [...selected] : undefined;
799
+ }
800
+ async function resolveSkillConflict(options) {
801
+ const targetPath = path.join(options.paths.skillsDir, options.targetSkillDirName);
802
+ if (!fs.existsSync(targetPath))
803
+ return options.targetSkillDirName;
804
+ if (!fs.statSync(targetPath).isDirectory()) {
805
+ throw new Error(`Cannot import skill ${options.promptLabel}: ${targetPath} exists and is not a directory.`);
806
+ }
807
+ if (skillContentMatchesTarget(options.sourceSkill, targetPath)) {
808
+ return options.targetSkillDirName;
809
+ }
810
+ if (options.yes) {
811
+ return options.targetSkillDirName;
812
+ }
813
+ if (options.nonInteractive) {
814
+ throw new NonInteractiveConflictError(`Conflict for skill "${options.targetSkillDirName}". Use --yes or run interactively.`);
815
+ }
816
+ const choice = await select({
817
+ message: `Skill conflict for ${options.promptLabel}`,
818
+ options: [
819
+ { value: "overwrite", label: `Overwrite ${options.targetSkillDirName}` },
820
+ { value: "skip", label: "Skip this skill" },
821
+ { value: "rename", label: "Rename imported skill" },
822
+ ],
823
+ });
824
+ if (isCancel(choice)) {
825
+ cancel("Operation cancelled.");
826
+ process.exit(1);
827
+ }
828
+ if (choice === "skip")
829
+ return null;
830
+ if (choice === "rename") {
831
+ const entered = await promptText({
832
+ message: `New directory name for ${options.promptLabel}`,
833
+ placeholder: options.targetSkillDirName,
834
+ validate(value) {
835
+ if (!value.trim())
836
+ return "Name is required.";
837
+ if (/[\\/]/.test(value))
838
+ return "Use a simple directory name.";
839
+ return undefined;
840
+ },
841
+ });
842
+ if (isCancel(entered)) {
843
+ cancel("Operation cancelled.");
844
+ process.exit(1);
845
+ }
846
+ const renamed = slugify(String(entered)) || "skill";
847
+ return resolveSkillConflict({
848
+ ...options,
849
+ targetSkillDirName: renamed,
850
+ });
851
+ }
852
+ return options.targetSkillDirName;
853
+ }
95
854
  async function resolveAgentConflict(options) {
96
855
  const targetPath = path.join(options.paths.agentsDir, options.targetFileName);
97
856
  if (!fs.existsSync(targetPath))
@@ -143,6 +902,310 @@ async function resolveAgentConflict(options) {
143
902
  }
144
903
  return options.targetFileName;
145
904
  }
905
+ async function resolveCommandConflict(options) {
906
+ const targetPath = path.join(options.paths.commandsDir, options.targetFileName);
907
+ if (!fs.existsSync(targetPath))
908
+ return options.targetFileName;
909
+ const existing = fs.readFileSync(targetPath, "utf8");
910
+ if (existing === options.commandContent)
911
+ return options.targetFileName;
912
+ if (options.yes) {
913
+ return options.targetFileName;
914
+ }
915
+ if (options.nonInteractive) {
916
+ throw new NonInteractiveConflictError(`Conflict for ${options.targetFileName}. Use --yes or run interactively.`);
917
+ }
918
+ const choice = await select({
919
+ message: `Command conflict for ${options.promptLabel}`,
920
+ options: [
921
+ { value: "overwrite", label: `Overwrite ${options.targetFileName}` },
922
+ { value: "skip", label: "Skip this command" },
923
+ { value: "rename", label: "Rename imported command" },
924
+ ],
925
+ });
926
+ if (isCancel(choice)) {
927
+ cancel("Operation cancelled.");
928
+ process.exit(1);
929
+ }
930
+ if (choice === "skip")
931
+ return null;
932
+ if (choice === "rename") {
933
+ const entered = await promptText({
934
+ message: `New filename (without extension) for ${options.promptLabel}`,
935
+ placeholder: options.targetFileName.replace(/\.(md|mdc)$/i, ""),
936
+ validate(value) {
937
+ if (!value.trim())
938
+ return "Name is required.";
939
+ if (/[\\/]/.test(value))
940
+ return "Use a simple filename.";
941
+ return undefined;
942
+ },
943
+ });
944
+ if (isCancel(entered)) {
945
+ cancel("Operation cancelled.");
946
+ process.exit(1);
947
+ }
948
+ const extension = path.extname(options.targetFileName) || ".md";
949
+ const renamedFileName = `${slugify(String(entered)) || "command"}${extension}`;
950
+ return resolveCommandConflict({
951
+ ...options,
952
+ targetFileName: renamedFileName,
953
+ });
954
+ }
955
+ return options.targetFileName;
956
+ }
957
+ async function resolveCommandsToImport(options) {
958
+ const selectors = options.selectors
959
+ .map((selector) => selector.trim())
960
+ .filter(Boolean);
961
+ if (selectors.length > 0) {
962
+ const { selected, unmatched } = resolveCommandSelections(options.sourceCommands, selectors);
963
+ if (unmatched.length > 0) {
964
+ throw new Error(`Command(s) not found in source: ${unmatched.join(", ")}. Available: ${options.sourceCommands.map((item) => item.fileName).join(", ")}`);
965
+ }
966
+ return {
967
+ selectedCommands: selected,
968
+ selectionMode: "custom",
969
+ };
970
+ }
971
+ const selectionResolution = await resolveSelectionModeWithSkip({
972
+ entityLabel: "commands",
973
+ selectionMode: options.selectionMode,
974
+ promptForSelection: options.promptForCommands,
975
+ nonInteractive: options.nonInteractive,
976
+ });
977
+ const selectionMode = selectionResolution.selectionMode;
978
+ if (selectionResolution.skipImport) {
979
+ return {
980
+ selectedCommands: [],
981
+ selectionMode: "custom",
982
+ };
983
+ }
984
+ if (selectionMode === "all" ||
985
+ !options.promptForCommands ||
986
+ options.nonInteractive) {
987
+ return {
988
+ selectedCommands: options.sourceCommands,
989
+ selectionMode,
990
+ };
991
+ }
992
+ const selected = await multiselect({
993
+ message: withMultiselectHelp("Select commands to import"),
994
+ options: options.sourceCommands.map((item) => ({
995
+ value: item.fileName,
996
+ label: item.fileName,
997
+ })),
998
+ initialValues: options.sourceCommands.map((item) => item.fileName),
999
+ });
1000
+ if (isCancel(selected)) {
1001
+ cancel("Operation cancelled.");
1002
+ process.exit(1);
1003
+ }
1004
+ const selectedNames = Array.isArray(selected)
1005
+ ? new Set(selected.map((value) => String(value)))
1006
+ : new Set();
1007
+ return {
1008
+ selectedCommands: options.sourceCommands.filter((item) => selectedNames.has(item.fileName)),
1009
+ selectionMode,
1010
+ };
1011
+ }
1012
+ async function resolveMcpServersToImport(options) {
1013
+ const available = Object.keys(options.sourceMcp.mcpServers).sort();
1014
+ const selectors = options.selectors
1015
+ .map((item) => item.trim())
1016
+ .filter(Boolean);
1017
+ if (selectors.length > 0) {
1018
+ const selected = new Set();
1019
+ const unmatched = [];
1020
+ for (const selector of selectors) {
1021
+ if (!Object.prototype.hasOwnProperty.call(options.sourceMcp.mcpServers, selector)) {
1022
+ unmatched.push(selector);
1023
+ continue;
1024
+ }
1025
+ selected.add(selector);
1026
+ }
1027
+ if (unmatched.length > 0) {
1028
+ throw new Error(`MCP server(s) not found in source: ${unmatched.join(", ")}. Available: ${available.join(", ")}`);
1029
+ }
1030
+ return {
1031
+ selectedServerNames: [...selected],
1032
+ selectionMode: "custom",
1033
+ };
1034
+ }
1035
+ const selectionResolution = await resolveSelectionModeWithSkip({
1036
+ entityLabel: "MCP servers",
1037
+ selectionMode: options.selectionMode,
1038
+ promptForSelection: options.promptForMcp,
1039
+ nonInteractive: options.nonInteractive,
1040
+ });
1041
+ const selectionMode = selectionResolution.selectionMode;
1042
+ if (selectionResolution.skipImport) {
1043
+ return {
1044
+ selectedServerNames: [],
1045
+ selectionMode: "custom",
1046
+ };
1047
+ }
1048
+ if (selectionMode === "all" ||
1049
+ !options.promptForMcp ||
1050
+ options.nonInteractive) {
1051
+ return {
1052
+ selectedServerNames: available,
1053
+ selectionMode,
1054
+ };
1055
+ }
1056
+ const selected = await multiselect({
1057
+ message: withMultiselectHelp("Select MCP servers to import"),
1058
+ options: available.map((serverName) => ({
1059
+ value: serverName,
1060
+ label: serverName,
1061
+ })),
1062
+ initialValues: available,
1063
+ });
1064
+ if (isCancel(selected)) {
1065
+ cancel("Operation cancelled.");
1066
+ process.exit(1);
1067
+ }
1068
+ const selectedNames = Array.isArray(selected)
1069
+ ? new Set(selected.map((value) => String(value)))
1070
+ : new Set();
1071
+ return {
1072
+ selectedServerNames: available.filter((serverName) => selectedNames.has(serverName)),
1073
+ selectionMode,
1074
+ };
1075
+ }
1076
+ async function resolveSkillsToImport(options) {
1077
+ const selectors = options.selectors
1078
+ .map((item) => item.trim())
1079
+ .filter(Boolean);
1080
+ if (selectors.length > 0) {
1081
+ const { selected, unmatched } = resolveSkillSelections(options.sourceSkills, selectors);
1082
+ if (unmatched.length > 0) {
1083
+ throw new Error(`Skill(s) not found in source: ${unmatched.join(", ")}. Available: ${options.sourceSkills.map((skill) => skill.name).join(", ")}`);
1084
+ }
1085
+ return {
1086
+ selectedSkills: selected,
1087
+ selectionMode: "custom",
1088
+ };
1089
+ }
1090
+ const selectionResolution = await resolveSelectionModeWithSkip({
1091
+ entityLabel: "skills",
1092
+ selectionMode: options.selectionMode,
1093
+ promptForSelection: options.promptForSkills,
1094
+ nonInteractive: options.nonInteractive,
1095
+ });
1096
+ const selectionMode = selectionResolution.selectionMode;
1097
+ if (selectionResolution.skipImport) {
1098
+ return {
1099
+ selectedSkills: [],
1100
+ selectionMode: "custom",
1101
+ };
1102
+ }
1103
+ if (selectionMode === "all" ||
1104
+ !options.promptForSkills ||
1105
+ options.nonInteractive) {
1106
+ return {
1107
+ selectedSkills: options.sourceSkills,
1108
+ selectionMode,
1109
+ };
1110
+ }
1111
+ const selected = await multiselect({
1112
+ message: withMultiselectHelp("Select skills to import"),
1113
+ options: options.sourceSkills.map((skill) => ({
1114
+ value: skill.name,
1115
+ label: skill.name,
1116
+ })),
1117
+ initialValues: options.sourceSkills.map((skill) => skill.name),
1118
+ });
1119
+ if (isCancel(selected)) {
1120
+ cancel("Operation cancelled.");
1121
+ process.exit(1);
1122
+ }
1123
+ const selectedNames = Array.isArray(selected)
1124
+ ? new Set(selected.map((value) => String(value)))
1125
+ : new Set();
1126
+ return {
1127
+ selectedSkills: options.sourceSkills.filter((skill) => selectedNames.has(skill.name)),
1128
+ selectionMode,
1129
+ };
1130
+ }
1131
+ async function resolveSelectionMode(options) {
1132
+ if (options.selectionMode) {
1133
+ return options.selectionMode;
1134
+ }
1135
+ if (!options.promptForSelection || options.nonInteractive) {
1136
+ return "all";
1137
+ }
1138
+ const choice = await select({
1139
+ message: `How should ${options.entityLabel} be tracked for future updates?`,
1140
+ options: [
1141
+ {
1142
+ value: "all",
1143
+ label: "Sync everything from source",
1144
+ hint: "Include new items on update",
1145
+ },
1146
+ {
1147
+ value: "custom",
1148
+ label: "Use custom selection",
1149
+ hint: "Update only currently selected items",
1150
+ },
1151
+ ],
1152
+ initialValue: "all",
1153
+ });
1154
+ if (isCancel(choice)) {
1155
+ cancel("Operation cancelled.");
1156
+ process.exit(1);
1157
+ }
1158
+ return choice === "custom" ? "custom" : "all";
1159
+ }
1160
+ async function resolveSelectionModeWithSkip(options) {
1161
+ if (options.selectionMode) {
1162
+ return {
1163
+ selectionMode: options.selectionMode,
1164
+ skipImport: false,
1165
+ };
1166
+ }
1167
+ if (!options.promptForSelection || options.nonInteractive) {
1168
+ return {
1169
+ selectionMode: "all",
1170
+ skipImport: false,
1171
+ };
1172
+ }
1173
+ const choice = await select({
1174
+ message: `How should ${options.entityLabel} be tracked for future updates?`,
1175
+ options: [
1176
+ {
1177
+ value: "all",
1178
+ label: "Sync everything from source",
1179
+ hint: "Include new items on update",
1180
+ },
1181
+ {
1182
+ value: "custom",
1183
+ label: "Use custom selection",
1184
+ hint: "Update only currently selected items",
1185
+ },
1186
+ {
1187
+ value: "skip",
1188
+ label: `Skip importing ${options.entityLabel}`,
1189
+ hint: "Track none from this source",
1190
+ },
1191
+ ],
1192
+ initialValue: "all",
1193
+ });
1194
+ if (isCancel(choice)) {
1195
+ cancel("Operation cancelled.");
1196
+ process.exit(1);
1197
+ }
1198
+ if (choice === "skip") {
1199
+ return {
1200
+ selectionMode: "custom",
1201
+ skipImport: true,
1202
+ };
1203
+ }
1204
+ return {
1205
+ selectionMode: choice === "custom" ? "custom" : "all",
1206
+ skipImport: false,
1207
+ };
1208
+ }
146
1209
  function normalizeMcp(raw) {
147
1210
  if (!raw) {
148
1211
  return {