agentloom 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -6
- package/dist/cli.js +11 -4
- package/dist/commands/add.js +14 -0
- package/dist/commands/delete.js +89 -3
- package/dist/commands/entity-utils.js +3 -0
- package/dist/commands/find.js +146 -12
- package/dist/commands/rule.d.ts +2 -0
- package/dist/commands/rule.js +86 -0
- package/dist/commands/sync.js +13 -4
- package/dist/commands/update.js +90 -7
- package/dist/core/agents.js +1 -1
- package/dist/core/argv.js +2 -0
- package/dist/core/commands.d.ts +12 -0
- package/dist/core/commands.js +106 -6
- package/dist/core/copy.js +12 -5
- package/dist/core/importer.d.ts +10 -0
- package/dist/core/importer.js +629 -13
- package/dist/core/lockfile.js +8 -0
- package/dist/core/manifest.js +123 -6
- package/dist/core/migration.js +655 -66
- package/dist/core/provider-entity-validation.d.ts +8 -0
- package/dist/core/provider-entity-validation.js +34 -0
- package/dist/core/provider-paths.d.ts +8 -1
- package/dist/core/provider-paths.js +69 -5
- package/dist/core/router.js +7 -1
- package/dist/core/rules.d.ts +34 -0
- package/dist/core/rules.js +149 -0
- package/dist/core/scope.js +1 -0
- package/dist/core/skills.d.ts +1 -0
- package/dist/core/skills.js +21 -2
- package/dist/core/sources.d.ts +2 -0
- package/dist/core/sources.js +34 -5
- package/dist/core/telemetry.d.ts +1 -1
- package/dist/core/telemetry.js +16 -0
- package/dist/sync/index.js +376 -18
- package/dist/types.d.ts +5 -1
- package/package.json +1 -1
package/dist/core/importer.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { cancel, isCancel, multiselect, select, text as promptText, } from "@clack/prompts";
|
|
4
|
+
import TOML from "@iarna/toml";
|
|
5
|
+
import matter from "gray-matter";
|
|
6
|
+
import YAML from "yaml";
|
|
4
7
|
import { buildAgentMarkdown, parseAgentsDir, targetFileNameForAgent, } from "./agents.js";
|
|
5
|
-
import { normalizeCommandSelector, parseCommandsDir, resolveCommandSelections, } from "./commands.js";
|
|
8
|
+
import { normalizeCommandArgumentsForCanonical, normalizeCommandSelector, parseCommandsDir, resolveCommandSelections, } from "./commands.js";
|
|
9
|
+
import { normalizeRuleSelector, parseRulesDir, resolveRuleSelections, stripRuleFileExtension, } from "./rules.js";
|
|
6
10
|
import { applySkillProviderSideEffects, copySkillArtifacts, normalizeSkillSelector, parseSkillsDir, resolveSkillSelections, skillContentMatchesTarget, } from "./skills.js";
|
|
7
|
-
import { ensureDir, hashContent, readJsonIfExists, relativePosix, slugify, writeTextAtomic, } from "./fs.js";
|
|
11
|
+
import { ensureDir, hashContent, isObject, readJsonIfExists, relativePosix, slugify, writeTextAtomic, } from "./fs.js";
|
|
8
12
|
import { ALL_PROVIDERS } from "../types.js";
|
|
9
13
|
import { readLockfile, upsertLockEntry, writeLockfile } from "./lockfile.js";
|
|
10
14
|
import { readCanonicalMcp, writeCanonicalMcp } from "./mcp.js";
|
|
11
|
-
import { discoverSourceAgentsDir, discoverSourceCommandsDir, discoverSourceMcpPath, discoverSourceSkillsDir, prepareSource, } from "./sources.js";
|
|
15
|
+
import { discoverSourceAgentsDir, discoverSourceCommandsDir, discoverSourceCommandsDirs, discoverSourceMcpPath, discoverSourceRulesDir, discoverSourceSkillsDir, prepareSource, } from "./sources.js";
|
|
16
|
+
import { isProviderEntityFileName } from "./provider-entity-validation.js";
|
|
12
17
|
export class NonInteractiveConflictError extends Error {
|
|
13
18
|
constructor(message) {
|
|
14
19
|
super(message);
|
|
@@ -24,10 +29,13 @@ export async function importSource(options) {
|
|
|
24
29
|
const requireAgents = options.requireAgents ?? shouldImportAgents;
|
|
25
30
|
const shouldImportCommands = options.importCommands ?? true;
|
|
26
31
|
const shouldImportMcp = options.importMcp ?? true;
|
|
32
|
+
const shouldImportRules = options.importRules ?? false;
|
|
33
|
+
const requireRules = options.requireRules ?? shouldImportRules;
|
|
27
34
|
const shouldImportSkills = options.importSkills ?? false;
|
|
28
35
|
if (!shouldImportAgents &&
|
|
29
36
|
!shouldImportCommands &&
|
|
30
37
|
!shouldImportMcp &&
|
|
38
|
+
!shouldImportRules &&
|
|
31
39
|
!shouldImportSkills) {
|
|
32
40
|
throw new Error("No import targets selected.");
|
|
33
41
|
}
|
|
@@ -46,36 +54,47 @@ export async function importSource(options) {
|
|
|
46
54
|
const sourceAgentsDir = shouldImportAgents
|
|
47
55
|
? discoverSourceAgentsDir(prepared.importRoot)
|
|
48
56
|
: null;
|
|
49
|
-
const
|
|
50
|
-
?
|
|
51
|
-
:
|
|
57
|
+
const sourceCommandsDirs = shouldImportCommands
|
|
58
|
+
? discoverSourceCommandsDirs(prepared.importRoot)
|
|
59
|
+
: [];
|
|
60
|
+
const sourceCommandsDir = sourceCommandsDirs[0] ??
|
|
61
|
+
(shouldImportCommands
|
|
62
|
+
? discoverSourceCommandsDir(prepared.importRoot)
|
|
63
|
+
: null);
|
|
52
64
|
const sourceMcpPath = shouldImportMcp
|
|
53
65
|
? discoverSourceMcpPath(prepared.importRoot)
|
|
54
66
|
: null;
|
|
67
|
+
const sourceRulesDir = shouldImportRules
|
|
68
|
+
? discoverSourceRulesDir(prepared.importRoot)
|
|
69
|
+
: null;
|
|
55
70
|
const sourceSkillsDir = shouldImportSkills
|
|
56
71
|
? discoverSourceSkillsDir(prepared.importRoot)
|
|
57
72
|
: null;
|
|
58
|
-
const sourceAgents = sourceAgentsDir
|
|
59
|
-
|
|
60
|
-
|
|
73
|
+
const sourceAgents = sourceAgentsDir
|
|
74
|
+
? parseSourceAgentsForImport(sourceAgentsDir)
|
|
75
|
+
: [];
|
|
76
|
+
const sourceCommands = sourceCommandsDirs.length > 0
|
|
77
|
+
? parseSourceCommandsForImport(sourceCommandsDirs)
|
|
61
78
|
: [];
|
|
62
79
|
const sourceMcp = sourceMcpPath
|
|
63
80
|
? normalizeMcp(readJsonIfExists(sourceMcpPath))
|
|
64
81
|
: null;
|
|
82
|
+
const sourceRules = sourceRulesDir ? parseRulesDir(sourceRulesDir) : [];
|
|
65
83
|
const sourceSkills = sourceSkillsDir ? parseSkillsDir(sourceSkillsDir) : [];
|
|
66
84
|
const hasExplicitCommandSelection = (options.commandSelectors?.length ?? 0) > 0;
|
|
67
85
|
const isAggregateImport = shouldImportAgents &&
|
|
68
86
|
shouldImportCommands &&
|
|
69
87
|
shouldImportMcp &&
|
|
88
|
+
(options.importRules === undefined || shouldImportRules) &&
|
|
70
89
|
shouldImportSkills;
|
|
71
90
|
if (shouldImportAgents && requireAgents && !sourceAgentsDir) {
|
|
72
|
-
throw new Error(`No source agents directory found under ${prepared.importRoot} (expected agents/ or .
|
|
91
|
+
throw new Error(`No source agents directory found under ${prepared.importRoot} (expected agents/, .agents/agents/, or .github/agents/).`);
|
|
73
92
|
}
|
|
74
93
|
if (shouldImportAgents && requireAgents && sourceAgents.length === 0) {
|
|
75
94
|
throw new Error(`No agent files found in ${sourceAgentsDir}.`);
|
|
76
95
|
}
|
|
77
96
|
if (shouldImportCommands && options.requireCommands && !sourceCommandsDir) {
|
|
78
|
-
throw new Error(`No source commands directory found under ${prepared.importRoot} (expected .agents/commands/, commands/, or prompts/).`);
|
|
97
|
+
throw new Error(`No source commands directory found under ${prepared.importRoot} (expected .agents/commands/, commands/, prompts/, .gemini/commands/, or .github/prompts/).`);
|
|
79
98
|
}
|
|
80
99
|
if (shouldImportCommands &&
|
|
81
100
|
options.requireCommands &&
|
|
@@ -85,6 +104,12 @@ export async function importSource(options) {
|
|
|
85
104
|
if (shouldImportMcp && options.requireMcp && !sourceMcpPath) {
|
|
86
105
|
throw new Error(`No source mcp.json found under ${prepared.importRoot} (expected mcp.json or .agents/mcp.json).`);
|
|
87
106
|
}
|
|
107
|
+
if (shouldImportRules && requireRules && !sourceRulesDir) {
|
|
108
|
+
throw new Error(`No source rules directory found under ${prepared.importRoot} (expected .agents/rules/ or rules/).`);
|
|
109
|
+
}
|
|
110
|
+
if (shouldImportRules && requireRules && sourceRules.length === 0) {
|
|
111
|
+
throw new Error(`No rule files found in ${sourceRulesDir}.`);
|
|
112
|
+
}
|
|
88
113
|
if (shouldImportSkills && options.requireSkills && !sourceSkillsDir) {
|
|
89
114
|
throw new Error(`No source skills directory found under ${prepared.importRoot} (expected .agents/skills/, skills/, or root SKILL.md).`);
|
|
90
115
|
}
|
|
@@ -96,9 +121,10 @@ export async function importSource(options) {
|
|
|
96
121
|
if (isAggregateImport &&
|
|
97
122
|
sourceAgents.length === 0 &&
|
|
98
123
|
sourceCommands.length === 0 &&
|
|
124
|
+
sourceRules.length === 0 &&
|
|
99
125
|
sourceSkills.length === 0 &&
|
|
100
126
|
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.`);
|
|
127
|
+
throw new Error(`No importable entities found in source "${sourceLocation}".\nExpected agents/, .agents/agents/, .github/agents/, commands/, .agents/commands/, prompts/, .gemini/commands/, .github/prompts/, mcp.json/.agents/mcp.json, rules/.agents/rules/, skills/, .agents/skills/, or root SKILL.md.`);
|
|
102
128
|
}
|
|
103
129
|
const shouldResolveAgents = shouldImportAgents &&
|
|
104
130
|
(sourceAgents.length > 0 ||
|
|
@@ -145,6 +171,21 @@ export async function importSource(options) {
|
|
|
145
171
|
selectedSourceMcpServers = mcpSelection.selectedServerNames;
|
|
146
172
|
mcpSelectionMode = mcpSelection.selectionMode;
|
|
147
173
|
}
|
|
174
|
+
let selectedRules = [];
|
|
175
|
+
let selectedSourceRules = [];
|
|
176
|
+
let ruleSelectionMode = "all";
|
|
177
|
+
if (shouldImportRules && sourceRulesDir) {
|
|
178
|
+
const ruleSelection = await resolveRulesToImport({
|
|
179
|
+
sourceRules,
|
|
180
|
+
selectors: options.ruleSelectors ?? [],
|
|
181
|
+
promptForRules: options.promptForRules ?? true,
|
|
182
|
+
nonInteractive: Boolean(options.nonInteractive),
|
|
183
|
+
selectionMode: options.selectionMode,
|
|
184
|
+
});
|
|
185
|
+
selectedRules = ruleSelection.selectedRules;
|
|
186
|
+
ruleSelectionMode = ruleSelection.selectionMode;
|
|
187
|
+
selectedSourceRules = selectedRules.map((rule) => rule.fileName);
|
|
188
|
+
}
|
|
148
189
|
let selectedSkills = [];
|
|
149
190
|
let selectedSourceSkills = [];
|
|
150
191
|
let skillSelectionMode = "all";
|
|
@@ -240,6 +281,47 @@ export async function importSource(options) {
|
|
|
240
281
|
}
|
|
241
282
|
importedMcpServers.push(...selectedSourceMcpServers);
|
|
242
283
|
}
|
|
284
|
+
const importedRules = [];
|
|
285
|
+
const telemetryRules = [];
|
|
286
|
+
const importedRuleRenameMap = {};
|
|
287
|
+
if (shouldImportRules && sourceRulesDir) {
|
|
288
|
+
if (selectedRules.length > 0) {
|
|
289
|
+
ensureDir(options.paths.rulesDir);
|
|
290
|
+
}
|
|
291
|
+
for (const [index, rule] of selectedRules.entries()) {
|
|
292
|
+
let targetFileName = rule.fileName;
|
|
293
|
+
const mappedTargetFileName = resolveMappedTargetRuleFileName(rule.fileName, options.ruleRenameMap);
|
|
294
|
+
if (mappedTargetFileName) {
|
|
295
|
+
targetFileName = mappedTargetFileName;
|
|
296
|
+
}
|
|
297
|
+
else if (options.rename &&
|
|
298
|
+
selectedRules.length === 1 &&
|
|
299
|
+
importedAgents.length === 0 &&
|
|
300
|
+
importedCommands.length === 0 &&
|
|
301
|
+
importedMcpServers.length === 0 &&
|
|
302
|
+
selectedSkills.length === 0) {
|
|
303
|
+
targetFileName = `${slugify(options.rename) || "rule"}.md`;
|
|
304
|
+
}
|
|
305
|
+
const resolvedFileName = await resolveRuleConflict({
|
|
306
|
+
targetFileName,
|
|
307
|
+
ruleContent: rule.content,
|
|
308
|
+
paths: options.paths,
|
|
309
|
+
yes: !!options.yes,
|
|
310
|
+
nonInteractive: !!options.nonInteractive,
|
|
311
|
+
promptLabel: `${rule.fileName} (${index + 1}/${selectedRules.length})`,
|
|
312
|
+
});
|
|
313
|
+
if (!resolvedFileName)
|
|
314
|
+
continue;
|
|
315
|
+
const targetPath = path.join(options.paths.rulesDir, resolvedFileName);
|
|
316
|
+
writeTextAtomic(targetPath, rule.content);
|
|
317
|
+
importedRules.push(relativePosix(options.paths.agentsRoot, targetPath));
|
|
318
|
+
telemetryRules.push({
|
|
319
|
+
name: stripRuleFileExtension(rule.fileName),
|
|
320
|
+
filePath: relativePosix(prepared.rootPath, rule.sourcePath),
|
|
321
|
+
});
|
|
322
|
+
importedRuleRenameMap[rule.fileName] = resolvedFileName;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
243
325
|
const importedSkills = [];
|
|
244
326
|
const telemetrySkills = [];
|
|
245
327
|
const importedSkillRenameMap = {};
|
|
@@ -258,7 +340,8 @@ export async function importSource(options) {
|
|
|
258
340
|
selectedSkills.length === 1 &&
|
|
259
341
|
importedAgents.length === 0 &&
|
|
260
342
|
importedCommands.length === 0 &&
|
|
261
|
-
importedMcpServers.length === 0
|
|
343
|
+
importedMcpServers.length === 0 &&
|
|
344
|
+
importedRules.length === 0) {
|
|
262
345
|
targetSkillDirName = slugify(options.rename) || "skill";
|
|
263
346
|
}
|
|
264
347
|
const resolvedSkillDirName = await resolveSkillConflict({
|
|
@@ -303,10 +386,12 @@ export async function importSource(options) {
|
|
|
303
386
|
selectedSourceCommandFiles.length < sourceCommands.length;
|
|
304
387
|
const selectedSubsetOfSourceMcp = sourceMcpServerNames.length > 0 &&
|
|
305
388
|
selectedSourceMcpServers.length < sourceMcpServerNames.length;
|
|
389
|
+
const selectedSubsetOfSourceRules = sourceRules.length > 0 && selectedSourceRules.length < sourceRules.length;
|
|
306
390
|
const selectedSubsetOfSourceSkills = sourceSkills.length > 0 &&
|
|
307
391
|
selectedSourceSkills.length < sourceSkills.length;
|
|
308
392
|
const shouldPersistCommandSelection = shouldImportCommands && commandSelectionMode === "custom";
|
|
309
393
|
const shouldPersistMcpSelection = shouldImportMcp && mcpSelectionMode === "custom";
|
|
394
|
+
const shouldPersistRuleSelection = shouldImportRules && ruleSelectionMode === "custom";
|
|
310
395
|
const shouldPersistSkillSelection = shouldImportSkills && skillSelectionMode === "custom";
|
|
311
396
|
const selectedSourceCommandsForLock = shouldPersistCommandSelection || selectedSubsetOfSourceCommands
|
|
312
397
|
? selectedSourceCommandFiles
|
|
@@ -314,6 +399,9 @@ export async function importSource(options) {
|
|
|
314
399
|
const selectedSourceMcpServersForLock = shouldPersistMcpSelection || selectedSubsetOfSourceMcp
|
|
315
400
|
? selectedSourceMcpServers
|
|
316
401
|
: undefined;
|
|
402
|
+
const selectedSourceRulesForLock = shouldPersistRuleSelection || selectedSubsetOfSourceRules
|
|
403
|
+
? selectedSourceRules
|
|
404
|
+
: undefined;
|
|
317
405
|
const selectedSourceSkillsForLock = shouldPersistSkillSelection || selectedSubsetOfSourceSkills
|
|
318
406
|
? selectedSourceSkills
|
|
319
407
|
: undefined;
|
|
@@ -321,6 +409,7 @@ export async function importSource(options) {
|
|
|
321
409
|
const isCommandOnlyImport = !shouldImportAgents &&
|
|
322
410
|
shouldImportCommands &&
|
|
323
411
|
!shouldImportMcp &&
|
|
412
|
+
!shouldImportRules &&
|
|
324
413
|
!shouldImportSkills;
|
|
325
414
|
const existingEntry = isCommandOnlyImport
|
|
326
415
|
? findRelaxedCommandEntry(lockfile.entries, {
|
|
@@ -338,6 +427,7 @@ export async function importSource(options) {
|
|
|
338
427
|
: options.agents,
|
|
339
428
|
selectedSourceCommands: selectedSourceCommandsForLock,
|
|
340
429
|
selectedSourceMcpServers: selectedSourceMcpServersForLock,
|
|
430
|
+
selectedSourceRules: selectedSourceRulesForLock,
|
|
341
431
|
selectedSourceSkills: selectedSourceSkillsForLock,
|
|
342
432
|
skillsProviders: skillsProvidersForLock,
|
|
343
433
|
});
|
|
@@ -360,6 +450,9 @@ export async function importSource(options) {
|
|
|
360
450
|
const lockImportedMcpServers = shouldImportMcp
|
|
361
451
|
? importedMcpServers
|
|
362
452
|
: (existingEntry?.importedMcpServers ?? []);
|
|
453
|
+
const lockImportedRules = shouldImportRules
|
|
454
|
+
? importedRules
|
|
455
|
+
: (existingEntry?.importedRules ?? []);
|
|
363
456
|
const lockImportedSkills = shouldImportSkills
|
|
364
457
|
? importedSkills
|
|
365
458
|
: (existingEntry?.importedSkills ?? []);
|
|
@@ -392,11 +485,19 @@ export async function importSource(options) {
|
|
|
392
485
|
? [...selectedSourceMcpServers]
|
|
393
486
|
: undefined
|
|
394
487
|
: existingEntry?.selectedSourceMcpServers;
|
|
488
|
+
const lockSelectedSourceRules = shouldImportRules
|
|
489
|
+
? shouldPersistRuleSelection || selectedSubsetOfSourceRules
|
|
490
|
+
? [...selectedSourceRules]
|
|
491
|
+
: undefined
|
|
492
|
+
: existingEntry?.selectedSourceRules;
|
|
395
493
|
const lockSelectedSourceSkills = shouldImportSkills
|
|
396
494
|
? shouldPersistSkillSelection || selectedSubsetOfSourceSkills
|
|
397
495
|
? [...selectedSourceSkills]
|
|
398
496
|
: undefined
|
|
399
497
|
: existingEntry?.selectedSourceSkills;
|
|
498
|
+
const lockRuleRenameMap = shouldImportRules
|
|
499
|
+
? normalizeRuleRenameMap(importedRuleRenameMap)
|
|
500
|
+
: existingEntry?.ruleRenameMap;
|
|
400
501
|
const lockSkillsProviders = shouldImportSkills
|
|
401
502
|
? (skillsProvidersForLock ?? existingEntry?.skillsProviders)
|
|
402
503
|
: existingEntry?.skillsProviders;
|
|
@@ -426,6 +527,9 @@ export async function importSource(options) {
|
|
|
426
527
|
commandRenameMap: lockCommandRenameMap,
|
|
427
528
|
importedMcpServers: lockImportedMcpServers,
|
|
428
529
|
selectedSourceMcpServers: lockSelectedSourceMcpServers,
|
|
530
|
+
importedRules: lockImportedRules,
|
|
531
|
+
selectedSourceRules: lockSelectedSourceRules,
|
|
532
|
+
ruleRenameMap: lockRuleRenameMap,
|
|
429
533
|
importedSkills: lockImportedSkills,
|
|
430
534
|
selectedSourceSkills: lockSelectedSourceSkills,
|
|
431
535
|
skillsProviders: lockSkillsProviders,
|
|
@@ -438,6 +542,9 @@ export async function importSource(options) {
|
|
|
438
542
|
commandRenameMap: lockCommandRenameMap ?? {},
|
|
439
543
|
mcp: lockImportedMcpServers,
|
|
440
544
|
selectedSourceMcpServers: lockSelectedSourceMcpServers ?? [],
|
|
545
|
+
rules: lockImportedRules,
|
|
546
|
+
selectedSourceRules: lockSelectedSourceRules ?? [],
|
|
547
|
+
ruleRenameMap: lockRuleRenameMap ?? {},
|
|
441
548
|
skills: lockImportedSkills,
|
|
442
549
|
selectedSourceSkills: lockSelectedSourceSkills ?? [],
|
|
443
550
|
skillsProviders: lockSkillsProviders ?? [],
|
|
@@ -458,6 +565,9 @@ export async function importSource(options) {
|
|
|
458
565
|
commandRenameMap: lockCommandRenameMap,
|
|
459
566
|
importedMcpServers: lockImportedMcpServers,
|
|
460
567
|
selectedSourceMcpServers: lockSelectedSourceMcpServers,
|
|
568
|
+
importedRules: lockImportedRules,
|
|
569
|
+
selectedSourceRules: lockSelectedSourceRules,
|
|
570
|
+
ruleRenameMap: lockRuleRenameMap,
|
|
461
571
|
importedSkills: lockImportedSkills,
|
|
462
572
|
selectedSourceSkills: lockSelectedSourceSkills,
|
|
463
573
|
skillsProviders: lockSkillsProviders,
|
|
@@ -484,7 +594,9 @@ export async function importSource(options) {
|
|
|
484
594
|
importedAgents,
|
|
485
595
|
importedCommands,
|
|
486
596
|
importedMcpServers,
|
|
597
|
+
importedRules,
|
|
487
598
|
importedSkills,
|
|
599
|
+
telemetryRules: telemetryRules.length > 0 ? telemetryRules : undefined,
|
|
488
600
|
telemetrySkills: telemetrySkills.length > 0 ? telemetrySkills : undefined,
|
|
489
601
|
resolvedCommit: prepared.resolvedCommit,
|
|
490
602
|
};
|
|
@@ -493,6 +605,357 @@ export async function importSource(options) {
|
|
|
493
605
|
prepared.cleanup();
|
|
494
606
|
}
|
|
495
607
|
}
|
|
608
|
+
function parseSourceAgentsForImport(sourceAgentsDir) {
|
|
609
|
+
if (isGitHubAgentsDir(sourceAgentsDir)) {
|
|
610
|
+
return parseGitHubAgentsDirForImport(sourceAgentsDir);
|
|
611
|
+
}
|
|
612
|
+
return parseAgentsDir(sourceAgentsDir);
|
|
613
|
+
}
|
|
614
|
+
function parseSourceCommandsForImport(sourceCommandsDir) {
|
|
615
|
+
const sourceCommandsDirs = Array.isArray(sourceCommandsDir)
|
|
616
|
+
? sourceCommandsDir
|
|
617
|
+
: [sourceCommandsDir];
|
|
618
|
+
return mergeCanonicalCommandFiles(sourceCommandsDirs.flatMap((dirPath) => parseSourceCommandsFromDir(dirPath)));
|
|
619
|
+
}
|
|
620
|
+
function parseSourceCommandsFromDir(sourceCommandsDir) {
|
|
621
|
+
if (isGeminiCommandsDir(sourceCommandsDir)) {
|
|
622
|
+
const geminiMarkdownCommands = parseCommandsDir(sourceCommandsDir).filter((command) => isProviderEntityFileName({
|
|
623
|
+
provider: "gemini",
|
|
624
|
+
entity: "command",
|
|
625
|
+
fileName: command.fileName,
|
|
626
|
+
}));
|
|
627
|
+
const parsedCommands = mergeCommandsByCanonicalFileName([
|
|
628
|
+
...parseGeminiTomlCommandsForImport(sourceCommandsDir),
|
|
629
|
+
...geminiMarkdownCommands,
|
|
630
|
+
]);
|
|
631
|
+
return parsedCommands.map((command) => normalizeGeminiCommandForImport(command));
|
|
632
|
+
}
|
|
633
|
+
const commands = parseCommandsDir(sourceCommandsDir);
|
|
634
|
+
if (!isGitHubPromptsDir(sourceCommandsDir)) {
|
|
635
|
+
return commands;
|
|
636
|
+
}
|
|
637
|
+
return commands
|
|
638
|
+
.filter((command) => isProviderEntityFileName({
|
|
639
|
+
provider: "copilot",
|
|
640
|
+
entity: "command",
|
|
641
|
+
fileName: command.fileName,
|
|
642
|
+
}))
|
|
643
|
+
.map((command) => normalizeGitHubPromptForImport(command));
|
|
644
|
+
}
|
|
645
|
+
function mergeCommandsByCanonicalFileName(commands) {
|
|
646
|
+
const byFileName = new Map();
|
|
647
|
+
for (const command of commands) {
|
|
648
|
+
if (!byFileName.has(command.fileName)) {
|
|
649
|
+
byFileName.set(command.fileName, command);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return [...byFileName.values()];
|
|
653
|
+
}
|
|
654
|
+
function isGitHubAgentsDir(sourceAgentsDir) {
|
|
655
|
+
return (path.basename(sourceAgentsDir).toLowerCase() === "agents" &&
|
|
656
|
+
path.basename(path.dirname(sourceAgentsDir)).toLowerCase() === ".github");
|
|
657
|
+
}
|
|
658
|
+
function isGitHubPromptsDir(sourceCommandsDir) {
|
|
659
|
+
return (path.basename(sourceCommandsDir).toLowerCase() === "prompts" &&
|
|
660
|
+
path.basename(path.dirname(sourceCommandsDir)).toLowerCase() === ".github");
|
|
661
|
+
}
|
|
662
|
+
function isGeminiCommandsDir(sourceCommandsDir) {
|
|
663
|
+
return (path.basename(sourceCommandsDir).toLowerCase() === "commands" &&
|
|
664
|
+
path.basename(path.dirname(sourceCommandsDir)).toLowerCase() === ".gemini");
|
|
665
|
+
}
|
|
666
|
+
function parseGeminiTomlCommandsForImport(sourceCommandsDir) {
|
|
667
|
+
return fs
|
|
668
|
+
.readdirSync(sourceCommandsDir, { withFileTypes: true })
|
|
669
|
+
.filter((entry) => entry.isFile())
|
|
670
|
+
.map((entry) => entry.name)
|
|
671
|
+
.filter((fileName) => isProviderEntityFileName({
|
|
672
|
+
provider: "gemini",
|
|
673
|
+
entity: "command",
|
|
674
|
+
fileName,
|
|
675
|
+
}))
|
|
676
|
+
.filter((fileName) => fileName.toLowerCase().endsWith(".toml"))
|
|
677
|
+
.sort((a, b) => a.localeCompare(b))
|
|
678
|
+
.map((fileName) => parseGeminiTomlCommandForImport(path.join(sourceCommandsDir, fileName)))
|
|
679
|
+
.filter((command) => command !== null);
|
|
680
|
+
}
|
|
681
|
+
function mergeCanonicalCommandFiles(commands) {
|
|
682
|
+
const merged = new Map();
|
|
683
|
+
for (const command of commands) {
|
|
684
|
+
const fileName = toCanonicalCommandFileName(command.fileName);
|
|
685
|
+
const normalizedCommand = fileName === command.fileName ? command : { ...command, fileName };
|
|
686
|
+
const existing = merged.get(fileName);
|
|
687
|
+
if (!existing) {
|
|
688
|
+
merged.set(fileName, normalizedCommand);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
const existingHasBody = normalizeImportedCommandBody(existing.body).length > 0;
|
|
692
|
+
const incomingHasBody = normalizeImportedCommandBody(normalizedCommand.body).length > 0;
|
|
693
|
+
if (existingHasBody &&
|
|
694
|
+
incomingHasBody &&
|
|
695
|
+
!sameNormalizedImportedCommandBody(existing.body, normalizedCommand.body)) {
|
|
696
|
+
throw new Error(`Conflicting command bodies found for "${fileName}" in ${existing.sourcePath} and ${normalizedCommand.sourcePath}. Align the provider-specific prompts before importing, or import a single provider directory with --subdir.`);
|
|
697
|
+
}
|
|
698
|
+
const body = existingHasBody ? existing.body : normalizedCommand.body;
|
|
699
|
+
const frontmatter = mergeCommandFrontmatterForImport(existing.frontmatter, normalizedCommand.frontmatter);
|
|
700
|
+
merged.set(fileName, {
|
|
701
|
+
...existing,
|
|
702
|
+
fileName,
|
|
703
|
+
body,
|
|
704
|
+
frontmatter,
|
|
705
|
+
content: buildCommandMarkdownForImport(frontmatter, body),
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
return [...merged.values()];
|
|
709
|
+
}
|
|
710
|
+
function sameNormalizedImportedCommandBody(left, right) {
|
|
711
|
+
return (normalizeImportedCommandBody(left) === normalizeImportedCommandBody(right));
|
|
712
|
+
}
|
|
713
|
+
function normalizeImportedCommandBody(value) {
|
|
714
|
+
return value.trim().replace(/\r\n/g, "\n");
|
|
715
|
+
}
|
|
716
|
+
function mergeCommandFrontmatterForImport(existing, incoming) {
|
|
717
|
+
if (!existing && !incoming) {
|
|
718
|
+
return undefined;
|
|
719
|
+
}
|
|
720
|
+
const merged = existing ? cloneUnknown(existing) : {};
|
|
721
|
+
for (const [key, value] of Object.entries(incoming ?? {})) {
|
|
722
|
+
const current = merged[key];
|
|
723
|
+
if (current === undefined) {
|
|
724
|
+
merged[key] = cloneUnknown(value);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (isObject(current) && isObject(value)) {
|
|
728
|
+
merged[key] = {
|
|
729
|
+
...cloneUnknown(value),
|
|
730
|
+
...cloneUnknown(current),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
735
|
+
}
|
|
736
|
+
function parseGeminiTomlCommandForImport(sourcePath) {
|
|
737
|
+
const raw = fs.readFileSync(sourcePath, "utf8");
|
|
738
|
+
let parsed;
|
|
739
|
+
try {
|
|
740
|
+
parsed = TOML.parse(raw);
|
|
741
|
+
}
|
|
742
|
+
catch {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
if (!isObject(parsed) || typeof parsed.prompt !== "string") {
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
const body = normalizeCommandArgumentsForCanonical(parsed.prompt, "gemini");
|
|
749
|
+
const frontmatter = cloneUnknown(parsed);
|
|
750
|
+
delete frontmatter.prompt;
|
|
751
|
+
const normalizedFrontmatter = Object.keys(frontmatter).length > 0 ? frontmatter : undefined;
|
|
752
|
+
const fileName = toCanonicalCommandFileName(path.basename(sourcePath));
|
|
753
|
+
const content = buildCommandMarkdownForImport(normalizedFrontmatter, body);
|
|
754
|
+
return {
|
|
755
|
+
fileName,
|
|
756
|
+
sourcePath,
|
|
757
|
+
content,
|
|
758
|
+
body,
|
|
759
|
+
frontmatter: normalizedFrontmatter,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function parseGitHubAgentsDirForImport(sourceAgentsDir) {
|
|
763
|
+
return fs
|
|
764
|
+
.readdirSync(sourceAgentsDir)
|
|
765
|
+
.filter((entry) => isProviderEntityFileName({
|
|
766
|
+
provider: "copilot",
|
|
767
|
+
entity: "agent",
|
|
768
|
+
fileName: entry,
|
|
769
|
+
}))
|
|
770
|
+
.sort((a, b) => a.localeCompare(b))
|
|
771
|
+
.map((entry) => parseGitHubAgentForImport(path.join(sourceAgentsDir, entry)));
|
|
772
|
+
}
|
|
773
|
+
function parseGitHubAgentForImport(sourcePath) {
|
|
774
|
+
const raw = fs.readFileSync(sourcePath, "utf8");
|
|
775
|
+
const parsed = matter(raw);
|
|
776
|
+
const data = isObject(parsed.data)
|
|
777
|
+
? parsed.data
|
|
778
|
+
: {};
|
|
779
|
+
const fileName = path.basename(sourcePath);
|
|
780
|
+
const fallbackName = inferAgentNameFromFile(fileName);
|
|
781
|
+
const name = typeof data.name === "string" && data.name.trim().length > 0
|
|
782
|
+
? data.name.trim()
|
|
783
|
+
: fallbackName;
|
|
784
|
+
const description = typeof data.description === "string" && data.description.trim().length > 0
|
|
785
|
+
? data.description.trim()
|
|
786
|
+
: `Imported from Copilot agent "${name}".`;
|
|
787
|
+
const frontmatter = {
|
|
788
|
+
name,
|
|
789
|
+
description,
|
|
790
|
+
};
|
|
791
|
+
for (const provider of ALL_PROVIDERS) {
|
|
792
|
+
if (provider === "copilot")
|
|
793
|
+
continue;
|
|
794
|
+
const value = data[provider];
|
|
795
|
+
if (value === false || isObject(value)) {
|
|
796
|
+
frontmatter[provider] = cloneUnknown(value);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const inferredCopilotConfig = {};
|
|
800
|
+
for (const [key, value] of Object.entries(data)) {
|
|
801
|
+
if (key === "name" || key === "description")
|
|
802
|
+
continue;
|
|
803
|
+
if (ALL_PROVIDERS.includes(key))
|
|
804
|
+
continue;
|
|
805
|
+
inferredCopilotConfig[key] = cloneUnknown(value);
|
|
806
|
+
}
|
|
807
|
+
const explicitCopilot = data.copilot;
|
|
808
|
+
if (explicitCopilot === false) {
|
|
809
|
+
frontmatter.copilot = false;
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
const copilotConfig = isObject(explicitCopilot)
|
|
813
|
+
? cloneUnknown(explicitCopilot)
|
|
814
|
+
: {};
|
|
815
|
+
for (const [key, value] of Object.entries(inferredCopilotConfig)) {
|
|
816
|
+
if (!(key in copilotConfig)) {
|
|
817
|
+
copilotConfig[key] = value;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (Object.keys(copilotConfig).length > 0) {
|
|
821
|
+
frontmatter.copilot = copilotConfig;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return {
|
|
825
|
+
name,
|
|
826
|
+
description,
|
|
827
|
+
body: parsed.content.trimStart(),
|
|
828
|
+
frontmatter: frontmatter,
|
|
829
|
+
sourcePath,
|
|
830
|
+
fileName,
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function normalizeGitHubPromptForImport(command) {
|
|
834
|
+
const fileName = toCanonicalCommandFileName(command.fileName);
|
|
835
|
+
if (!command.frontmatter) {
|
|
836
|
+
return {
|
|
837
|
+
...command,
|
|
838
|
+
fileName,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
const nextFrontmatter = {};
|
|
842
|
+
for (const provider of ALL_PROVIDERS) {
|
|
843
|
+
if (provider === "copilot")
|
|
844
|
+
continue;
|
|
845
|
+
const value = command.frontmatter[provider];
|
|
846
|
+
if (value === false || isObject(value)) {
|
|
847
|
+
nextFrontmatter[provider] = cloneUnknown(value);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const explicitCopilot = command.frontmatter.copilot;
|
|
851
|
+
if (explicitCopilot === false) {
|
|
852
|
+
nextFrontmatter.copilot = false;
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
const copilotConfig = isObject(explicitCopilot)
|
|
856
|
+
? cloneUnknown(explicitCopilot)
|
|
857
|
+
: {};
|
|
858
|
+
for (const [key, value] of Object.entries(command.frontmatter)) {
|
|
859
|
+
if (ALL_PROVIDERS.includes(key))
|
|
860
|
+
continue;
|
|
861
|
+
if (!(key in copilotConfig)) {
|
|
862
|
+
copilotConfig[key] = cloneUnknown(value);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (Object.keys(copilotConfig).length > 0) {
|
|
866
|
+
nextFrontmatter.copilot = copilotConfig;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
const frontmatter = Object.keys(nextFrontmatter).length > 0 ? nextFrontmatter : undefined;
|
|
870
|
+
const body = normalizeCommandArgumentsForCanonical(command.body, "copilot").trimStart();
|
|
871
|
+
return {
|
|
872
|
+
...command,
|
|
873
|
+
fileName,
|
|
874
|
+
body,
|
|
875
|
+
frontmatter,
|
|
876
|
+
content: buildCommandMarkdownForImport(frontmatter, body),
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function normalizeGeminiCommandForImport(command) {
|
|
880
|
+
const fileName = toCanonicalCommandFileName(command.fileName);
|
|
881
|
+
const body = normalizeCommandArgumentsForCanonical(command.body, "gemini");
|
|
882
|
+
if (!command.frontmatter) {
|
|
883
|
+
return {
|
|
884
|
+
...command,
|
|
885
|
+
fileName,
|
|
886
|
+
body,
|
|
887
|
+
content: buildCommandMarkdownForImport(undefined, body),
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
const nextFrontmatter = {};
|
|
891
|
+
for (const provider of ALL_PROVIDERS) {
|
|
892
|
+
if (provider === "gemini")
|
|
893
|
+
continue;
|
|
894
|
+
const value = command.frontmatter[provider];
|
|
895
|
+
if (value === false || isObject(value)) {
|
|
896
|
+
nextFrontmatter[provider] = cloneUnknown(value);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (typeof command.frontmatter.description === "string") {
|
|
900
|
+
nextFrontmatter.description = command.frontmatter.description;
|
|
901
|
+
}
|
|
902
|
+
const explicitGemini = command.frontmatter.gemini;
|
|
903
|
+
if (explicitGemini === false) {
|
|
904
|
+
nextFrontmatter.gemini = false;
|
|
905
|
+
}
|
|
906
|
+
else {
|
|
907
|
+
const geminiConfig = isObject(explicitGemini)
|
|
908
|
+
? cloneUnknown(explicitGemini)
|
|
909
|
+
: {};
|
|
910
|
+
for (const [key, value] of Object.entries(command.frontmatter)) {
|
|
911
|
+
if (key === "description")
|
|
912
|
+
continue;
|
|
913
|
+
if (ALL_PROVIDERS.includes(key))
|
|
914
|
+
continue;
|
|
915
|
+
if (!(key in geminiConfig)) {
|
|
916
|
+
geminiConfig[key] = cloneUnknown(value);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (Object.keys(geminiConfig).length > 0) {
|
|
920
|
+
nextFrontmatter.gemini = geminiConfig;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const frontmatter = Object.keys(nextFrontmatter).length > 0 ? nextFrontmatter : undefined;
|
|
924
|
+
return {
|
|
925
|
+
...command,
|
|
926
|
+
fileName,
|
|
927
|
+
body,
|
|
928
|
+
frontmatter,
|
|
929
|
+
content: buildCommandMarkdownForImport(frontmatter, body),
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function buildCommandMarkdownForImport(frontmatter, body) {
|
|
933
|
+
if (!frontmatter) {
|
|
934
|
+
return body.endsWith("\n") ? body : `${body}\n`;
|
|
935
|
+
}
|
|
936
|
+
const fm = YAML.stringify(frontmatter, { lineWidth: 0 }).trimEnd();
|
|
937
|
+
return `---\n${fm}\n---\n\n${body}${body.endsWith("\n") ? "" : "\n"}`;
|
|
938
|
+
}
|
|
939
|
+
function inferAgentNameFromFile(fileName) {
|
|
940
|
+
const base = fileName
|
|
941
|
+
.replace(/\.agent\.md$/i, "")
|
|
942
|
+
.replace(/\.md$/i, "")
|
|
943
|
+
.trim();
|
|
944
|
+
return base || "agent";
|
|
945
|
+
}
|
|
946
|
+
function toCanonicalCommandFileName(fileName) {
|
|
947
|
+
const lower = fileName.toLowerCase();
|
|
948
|
+
if (lower.endsWith(".prompt.md")) {
|
|
949
|
+
return `${fileName.slice(0, -".prompt.md".length)}.md`;
|
|
950
|
+
}
|
|
951
|
+
if (lower.endsWith(".toml")) {
|
|
952
|
+
return `${fileName.slice(0, -".toml".length)}.md`;
|
|
953
|
+
}
|
|
954
|
+
return fileName;
|
|
955
|
+
}
|
|
956
|
+
function cloneUnknown(value) {
|
|
957
|
+
return JSON.parse(JSON.stringify(value));
|
|
958
|
+
}
|
|
496
959
|
async function resolveAgentsToImport(options) {
|
|
497
960
|
const requestedAgents = normalizeRequestedAgents(options.requestedAgents);
|
|
498
961
|
if (requestedAgents && requestedAgents.length > 0) {
|
|
@@ -653,6 +1116,7 @@ function findMatchingLockEntry(entries, key) {
|
|
|
653
1116
|
sameRequestedAgentsForMatch(entry.requestedAgents, key.requestedAgents) &&
|
|
654
1117
|
sameStringSelectionForMatch(entry.selectedSourceCommands, key.selectedSourceCommands) &&
|
|
655
1118
|
sameStringSelectionForMatch(entry.selectedSourceMcpServers, key.selectedSourceMcpServers) &&
|
|
1119
|
+
sameStringSelectionForMatch(entry.selectedSourceRules, key.selectedSourceRules, { wildcardWhenRightIsUndefined: true }) &&
|
|
656
1120
|
sameStringSelectionForMatch(entry.selectedSourceSkills, key.selectedSourceSkills, { wildcardWhenRightIsUndefined: true }) &&
|
|
657
1121
|
sameStringSelectionForMatch(entry.skillsProviders, key.skillsProviders, {
|
|
658
1122
|
wildcardWhenRightIsUndefined: true,
|
|
@@ -667,6 +1131,7 @@ function findRelaxedCommandEntry(entries, key) {
|
|
|
667
1131
|
return undefined;
|
|
668
1132
|
const mixed = matches.find((entry) => entry.importedAgents.length > 0 ||
|
|
669
1133
|
entry.importedMcpServers.length > 0 ||
|
|
1134
|
+
entry.importedRules.length > 0 ||
|
|
670
1135
|
entry.importedSkills.length > 0);
|
|
671
1136
|
return mixed ?? matches[0];
|
|
672
1137
|
}
|
|
@@ -714,6 +1179,11 @@ function computeTrackedEntitiesForLock(options) {
|
|
|
714
1179
|
(options.selectedSourceMcpServers?.length ?? 0) > 0) {
|
|
715
1180
|
tracked.push("mcp");
|
|
716
1181
|
}
|
|
1182
|
+
if (options.importedRules.length > 0 ||
|
|
1183
|
+
(options.selectedSourceRules?.length ?? 0) > 0 ||
|
|
1184
|
+
Object.keys(options.ruleRenameMap ?? {}).length > 0) {
|
|
1185
|
+
tracked.push("rule");
|
|
1186
|
+
}
|
|
717
1187
|
if (options.importedSkills.length > 0 ||
|
|
718
1188
|
(options.selectedSourceSkills?.length ?? 0) > 0 ||
|
|
719
1189
|
(options.skillsProviders?.length ?? 0) > 0 ||
|
|
@@ -754,6 +1224,44 @@ function resolveMappedTargetFileName(sourceFileName, renameMap) {
|
|
|
754
1224
|
}
|
|
755
1225
|
return undefined;
|
|
756
1226
|
}
|
|
1227
|
+
function normalizeRuleRenameMap(renameMap) {
|
|
1228
|
+
if (!renameMap)
|
|
1229
|
+
return undefined;
|
|
1230
|
+
const normalizedEntries = Object.entries(renameMap)
|
|
1231
|
+
.map(([sourceSelector, importedName]) => {
|
|
1232
|
+
const normalizedSourceSelector = normalizeRuleSelector(sourceSelector);
|
|
1233
|
+
const importedBaseName = path.basename(importedName.trim());
|
|
1234
|
+
if (!normalizedSourceSelector || !importedBaseName) {
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
const ext = path.extname(importedBaseName);
|
|
1238
|
+
const stem = stripRuleFileExtension(importedBaseName);
|
|
1239
|
+
const normalizedTarget = `${slugify(stem) || "rule"}${ext || ".md"}`;
|
|
1240
|
+
return [normalizedSourceSelector, normalizedTarget];
|
|
1241
|
+
})
|
|
1242
|
+
.filter((entry) => entry !== null);
|
|
1243
|
+
if (normalizedEntries.length === 0)
|
|
1244
|
+
return undefined;
|
|
1245
|
+
return Object.fromEntries(normalizedEntries);
|
|
1246
|
+
}
|
|
1247
|
+
function resolveMappedTargetRuleFileName(sourceFileName, renameMap) {
|
|
1248
|
+
if (!renameMap)
|
|
1249
|
+
return undefined;
|
|
1250
|
+
const normalizedSourceName = normalizeRuleSelector(sourceFileName);
|
|
1251
|
+
for (const [sourceSelector, importedName] of Object.entries(renameMap)) {
|
|
1252
|
+
if (normalizeRuleSelector(sourceSelector) !== normalizedSourceName) {
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
const importedBaseName = path.basename(importedName.trim());
|
|
1256
|
+
if (!importedBaseName)
|
|
1257
|
+
return undefined;
|
|
1258
|
+
const ext = path.extname(importedBaseName);
|
|
1259
|
+
if (ext)
|
|
1260
|
+
return importedBaseName;
|
|
1261
|
+
return `${slugify(importedBaseName) || "rule"}.md`;
|
|
1262
|
+
}
|
|
1263
|
+
return undefined;
|
|
1264
|
+
}
|
|
757
1265
|
function normalizeSkillRenameMap(renameMap) {
|
|
758
1266
|
if (!renameMap)
|
|
759
1267
|
return undefined;
|
|
@@ -954,6 +1462,58 @@ async function resolveCommandConflict(options) {
|
|
|
954
1462
|
}
|
|
955
1463
|
return options.targetFileName;
|
|
956
1464
|
}
|
|
1465
|
+
async function resolveRuleConflict(options) {
|
|
1466
|
+
const targetPath = path.join(options.paths.rulesDir, options.targetFileName);
|
|
1467
|
+
if (!fs.existsSync(targetPath))
|
|
1468
|
+
return options.targetFileName;
|
|
1469
|
+
const existing = fs.readFileSync(targetPath, "utf8");
|
|
1470
|
+
if (existing === options.ruleContent)
|
|
1471
|
+
return options.targetFileName;
|
|
1472
|
+
if (options.yes) {
|
|
1473
|
+
return options.targetFileName;
|
|
1474
|
+
}
|
|
1475
|
+
if (options.nonInteractive) {
|
|
1476
|
+
throw new NonInteractiveConflictError(`Conflict for ${options.targetFileName}. Use --yes or run interactively.`);
|
|
1477
|
+
}
|
|
1478
|
+
const choice = await select({
|
|
1479
|
+
message: `Rule conflict for ${options.promptLabel}`,
|
|
1480
|
+
options: [
|
|
1481
|
+
{ value: "overwrite", label: `Overwrite ${options.targetFileName}` },
|
|
1482
|
+
{ value: "skip", label: "Skip this rule" },
|
|
1483
|
+
{ value: "rename", label: "Rename imported rule" },
|
|
1484
|
+
],
|
|
1485
|
+
});
|
|
1486
|
+
if (isCancel(choice)) {
|
|
1487
|
+
cancel("Operation cancelled.");
|
|
1488
|
+
process.exit(1);
|
|
1489
|
+
}
|
|
1490
|
+
if (choice === "skip")
|
|
1491
|
+
return null;
|
|
1492
|
+
if (choice === "rename") {
|
|
1493
|
+
const entered = await promptText({
|
|
1494
|
+
message: `New filename (without extension) for ${options.promptLabel}`,
|
|
1495
|
+
placeholder: options.targetFileName.replace(/\.(md|mdc)$/i, ""),
|
|
1496
|
+
validate(value) {
|
|
1497
|
+
if (!value.trim())
|
|
1498
|
+
return "Name is required.";
|
|
1499
|
+
if (/[\\/]/.test(value))
|
|
1500
|
+
return "Use a simple filename.";
|
|
1501
|
+
return undefined;
|
|
1502
|
+
},
|
|
1503
|
+
});
|
|
1504
|
+
if (isCancel(entered)) {
|
|
1505
|
+
cancel("Operation cancelled.");
|
|
1506
|
+
process.exit(1);
|
|
1507
|
+
}
|
|
1508
|
+
const extension = path.extname(options.targetFileName) || ".md";
|
|
1509
|
+
const renamedFileName = `${slugify(String(entered)) || "rule"}${extension}`;
|
|
1510
|
+
return resolveRuleConflict({
|
|
1511
|
+
...options,
|
|
1512
|
+
targetFileName: renamedFileName,
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
return options.targetFileName;
|
|
1516
|
+
}
|
|
957
1517
|
async function resolveCommandsToImport(options) {
|
|
958
1518
|
const selectors = options.selectors
|
|
959
1519
|
.map((selector) => selector.trim())
|
|
@@ -1073,6 +1633,62 @@ async function resolveMcpServersToImport(options) {
|
|
|
1073
1633
|
selectionMode,
|
|
1074
1634
|
};
|
|
1075
1635
|
}
|
|
1636
|
+
async function resolveRulesToImport(options) {
|
|
1637
|
+
const selectors = options.selectors
|
|
1638
|
+
.map((selector) => selector.trim())
|
|
1639
|
+
.filter(Boolean);
|
|
1640
|
+
if (selectors.length > 0) {
|
|
1641
|
+
const { selected, unmatched } = resolveRuleSelections(options.sourceRules, selectors);
|
|
1642
|
+
if (unmatched.length > 0) {
|
|
1643
|
+
throw new Error(`Rule(s) not found in source: ${unmatched.join(", ")}. Available: ${options.sourceRules.map((item) => item.fileName).join(", ")}`);
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
selectedRules: selected,
|
|
1647
|
+
selectionMode: "custom",
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
const selectionResolution = await resolveSelectionModeWithSkip({
|
|
1651
|
+
entityLabel: "rules",
|
|
1652
|
+
selectionMode: options.selectionMode,
|
|
1653
|
+
promptForSelection: options.promptForRules,
|
|
1654
|
+
nonInteractive: options.nonInteractive,
|
|
1655
|
+
});
|
|
1656
|
+
const selectionMode = selectionResolution.selectionMode;
|
|
1657
|
+
if (selectionResolution.skipImport) {
|
|
1658
|
+
return {
|
|
1659
|
+
selectedRules: [],
|
|
1660
|
+
selectionMode: "custom",
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
if (selectionMode === "all" ||
|
|
1664
|
+
!options.promptForRules ||
|
|
1665
|
+
options.nonInteractive) {
|
|
1666
|
+
return {
|
|
1667
|
+
selectedRules: options.sourceRules,
|
|
1668
|
+
selectionMode,
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
const selected = await multiselect({
|
|
1672
|
+
message: withMultiselectHelp("Select rules to import"),
|
|
1673
|
+
options: options.sourceRules.map((item) => ({
|
|
1674
|
+
value: item.fileName,
|
|
1675
|
+
label: item.fileName,
|
|
1676
|
+
hint: item.name,
|
|
1677
|
+
})),
|
|
1678
|
+
initialValues: options.sourceRules.map((item) => item.fileName),
|
|
1679
|
+
});
|
|
1680
|
+
if (isCancel(selected)) {
|
|
1681
|
+
cancel("Operation cancelled.");
|
|
1682
|
+
process.exit(1);
|
|
1683
|
+
}
|
|
1684
|
+
const selectedNames = Array.isArray(selected)
|
|
1685
|
+
? new Set(selected.map((value) => String(value)))
|
|
1686
|
+
: new Set();
|
|
1687
|
+
return {
|
|
1688
|
+
selectedRules: options.sourceRules.filter((item) => selectedNames.has(item.fileName)),
|
|
1689
|
+
selectionMode,
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1076
1692
|
async function resolveSkillsToImport(options) {
|
|
1077
1693
|
const selectors = options.selectors
|
|
1078
1694
|
.map((item) => item.trim())
|