@codeharbor/agent-playbook 0.1.3 → 0.2.1
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 +17 -3
- package/package.json +4 -3
- package/src/cli.js +1307 -46
package/src/cli.js
CHANGED
|
@@ -10,6 +10,8 @@ const SKILLS_DIR_NAME = "skills";
|
|
|
10
10
|
const DEFAULT_SESSION_DIR = "sessions";
|
|
11
11
|
const LOCAL_CLI_DIR = "agent-playbook";
|
|
12
12
|
const HOOK_SOURCE_VALUE = "agent-playbook";
|
|
13
|
+
const STATE_FILE_NAME = "state.json";
|
|
14
|
+
const DISABLED_DIR_NAME = ".disabled";
|
|
13
15
|
|
|
14
16
|
const packageJson = readJsonSafe(path.join(__dirname, "..", "package.json"));
|
|
15
17
|
const VERSION = packageJson.version || "0.0.0";
|
|
@@ -34,6 +36,8 @@ function main(argv, context) {
|
|
|
34
36
|
return handleSessionLog(options);
|
|
35
37
|
case "self-improve":
|
|
36
38
|
return handleSelfImprove(options);
|
|
39
|
+
case "skills":
|
|
40
|
+
return handleSkills(options, parsed.positionals, context);
|
|
37
41
|
case "help":
|
|
38
42
|
case "--help":
|
|
39
43
|
case "-h":
|
|
@@ -53,6 +57,7 @@ function printHelp() {
|
|
|
53
57
|
` ${APP_NAME} doctor [--project] [--repo <path>]`,
|
|
54
58
|
` ${APP_NAME} repair [--project] [--overwrite] [--repo <path>]`,
|
|
55
59
|
` ${APP_NAME} uninstall [--project] [--repo <path>]`,
|
|
60
|
+
` ${APP_NAME} skills [list|info|add|remove|enable|disable|doctor|sync|upgrade|export|import]`,
|
|
56
61
|
"",
|
|
57
62
|
"Hook commands:",
|
|
58
63
|
` ${APP_NAME} session-log [--session-dir <path>]`,
|
|
@@ -62,7 +67,18 @@ function printHelp() {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
function parseArgs(argv) {
|
|
65
|
-
const valueFlags = new Set([
|
|
70
|
+
const valueFlags = new Set([
|
|
71
|
+
"session-dir",
|
|
72
|
+
"repo",
|
|
73
|
+
"transcript-path",
|
|
74
|
+
"cwd",
|
|
75
|
+
"hook-source",
|
|
76
|
+
"scope",
|
|
77
|
+
"target",
|
|
78
|
+
"format",
|
|
79
|
+
"source",
|
|
80
|
+
"output",
|
|
81
|
+
]);
|
|
66
82
|
const options = {};
|
|
67
83
|
const positionals = [];
|
|
68
84
|
let command = null;
|
|
@@ -128,6 +144,7 @@ function handleInit(options, context) {
|
|
|
128
144
|
|
|
129
145
|
ensureDir(settings.claudeSkillsDir, options["dry-run"]);
|
|
130
146
|
ensureDir(settings.codexSkillsDir, options["dry-run"]);
|
|
147
|
+
ensureDir(settings.geminiSkillsDir, options["dry-run"]);
|
|
131
148
|
|
|
132
149
|
const manifest = {
|
|
133
150
|
name: APP_NAME,
|
|
@@ -138,16 +155,20 @@ function handleInit(options, context) {
|
|
|
138
155
|
links: {
|
|
139
156
|
claude: [],
|
|
140
157
|
codex: [],
|
|
158
|
+
gemini: [],
|
|
141
159
|
},
|
|
142
160
|
};
|
|
143
161
|
|
|
144
162
|
let claudeLinks = { created: [], skipped: [] };
|
|
145
163
|
let codexLinks = { created: [], skipped: [] };
|
|
164
|
+
let geminiLinks = { created: [], skipped: [] };
|
|
146
165
|
if (settings.skillsSource) {
|
|
147
166
|
claudeLinks = linkSkills(settings.skillsSource, settings.claudeSkillsDir, options, overwriteState);
|
|
148
167
|
codexLinks = linkSkills(settings.skillsSource, settings.codexSkillsDir, options, overwriteState);
|
|
168
|
+
geminiLinks = linkSkills(settings.skillsSource, settings.geminiSkillsDir, options, overwriteState);
|
|
149
169
|
manifest.links.claude = claudeLinks.created;
|
|
150
170
|
manifest.links.codex = codexLinks.created;
|
|
171
|
+
manifest.links.gemini = geminiLinks.created;
|
|
151
172
|
|
|
152
173
|
if (!options["dry-run"]) {
|
|
153
174
|
writeJson(path.join(settings.claudeSkillsDir, ".agent-playbook.json"), manifest);
|
|
@@ -164,7 +185,7 @@ function handleInit(options, context) {
|
|
|
164
185
|
|
|
165
186
|
updateCodexConfig(settings, options);
|
|
166
187
|
|
|
167
|
-
printInitSummary(settings, hooksEnabled, options, claudeLinks, codexLinks, warnings);
|
|
188
|
+
printInitSummary(settings, hooksEnabled, options, claudeLinks, codexLinks, geminiLinks, warnings);
|
|
168
189
|
return Promise.resolve();
|
|
169
190
|
}
|
|
170
191
|
|
|
@@ -200,6 +221,7 @@ function handleUninstall(options, context) {
|
|
|
200
221
|
if (manifest && manifest.links) {
|
|
201
222
|
removeLinks(manifest.links.claude || []);
|
|
202
223
|
removeLinks(manifest.links.codex || []);
|
|
224
|
+
removeLinks(manifest.links.gemini || []);
|
|
203
225
|
safeUnlink(manifestPath);
|
|
204
226
|
} else {
|
|
205
227
|
console.log("No manifest found. Skipping link removal.");
|
|
@@ -266,39 +288,84 @@ async function handleSelfImprove(options) {
|
|
|
266
288
|
console.error(`Self-improvement entry saved to ${entryPath}`);
|
|
267
289
|
}
|
|
268
290
|
|
|
291
|
+
function handleSkills(options, positionals, context) {
|
|
292
|
+
const settings = resolveSettings(options, context || {});
|
|
293
|
+
const subcommand = positionals[0] || "list";
|
|
294
|
+
const args = positionals.slice(1);
|
|
295
|
+
|
|
296
|
+
switch (subcommand) {
|
|
297
|
+
case "list":
|
|
298
|
+
return handleSkillsList(options, args, settings);
|
|
299
|
+
case "info":
|
|
300
|
+
return handleSkillsInfo(options, args, settings);
|
|
301
|
+
case "add":
|
|
302
|
+
return handleSkillsAdd(options, args, settings);
|
|
303
|
+
case "remove":
|
|
304
|
+
return handleSkillsRemove(options, args, settings);
|
|
305
|
+
case "enable":
|
|
306
|
+
return handleSkillsEnable(options, args, settings);
|
|
307
|
+
case "disable":
|
|
308
|
+
return handleSkillsDisable(options, args, settings);
|
|
309
|
+
case "doctor":
|
|
310
|
+
return handleSkillsDoctor(options, args, settings);
|
|
311
|
+
case "sync":
|
|
312
|
+
return handleSkillsSync(options, args, settings);
|
|
313
|
+
case "upgrade":
|
|
314
|
+
return handleSkillsUpgrade(options, args, settings);
|
|
315
|
+
case "export":
|
|
316
|
+
return handleSkillsExport(options, args, settings);
|
|
317
|
+
case "import":
|
|
318
|
+
return handleSkillsImport(options, args, settings);
|
|
319
|
+
default:
|
|
320
|
+
console.error(`Unknown skills subcommand: ${subcommand}`);
|
|
321
|
+
return Promise.resolve();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
269
325
|
function resolveSettings(options, context) {
|
|
270
326
|
const cwd = process.cwd();
|
|
271
|
-
const
|
|
327
|
+
const repoRootDetected = options.repo ? path.resolve(options.repo) : findRepoRoot(cwd);
|
|
272
328
|
const cliRoot =
|
|
273
329
|
context && context.cliPath ? path.resolve(path.dirname(context.cliPath), "..") : null;
|
|
274
|
-
const skillsSource = resolveSkillsSource([
|
|
330
|
+
const skillsSource = resolveSkillsSource([repoRootDetected || cwd, cliRoot]);
|
|
275
331
|
const projectMode = Boolean(options.project);
|
|
276
332
|
|
|
277
333
|
const envClaudeDir = process.env.AGENT_PLAYBOOK_CLAUDE_DIR;
|
|
278
334
|
const envCodexDir = process.env.AGENT_PLAYBOOK_CODEX_DIR;
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
335
|
+
const envGeminiDir = process.env.AGENT_PLAYBOOK_GEMINI_DIR;
|
|
336
|
+
const globalClaudeDir = envClaudeDir ? path.resolve(envClaudeDir) : path.join(os.homedir(), ".claude");
|
|
337
|
+
const globalCodexDir = envCodexDir ? path.resolve(envCodexDir) : path.join(os.homedir(), ".codex");
|
|
338
|
+
const globalGeminiDir = envGeminiDir ? path.resolve(envGeminiDir) : path.join(os.homedir(), ".gemini");
|
|
339
|
+
const projectRoot = repoRootDetected || cwd;
|
|
340
|
+
const projectClaudeDir = repoRootDetected ? path.join(repoRootDetected, ".claude") : null;
|
|
341
|
+
const projectCodexDir = repoRootDetected ? path.join(repoRootDetected, ".codex") : null;
|
|
342
|
+
const projectGeminiDir = repoRootDetected ? path.join(repoRootDetected, ".gemini") : null;
|
|
343
|
+
const claudeDir = projectMode ? path.join(projectRoot, ".claude") : globalClaudeDir;
|
|
344
|
+
const codexDir = projectMode ? path.join(projectRoot, ".codex") : globalCodexDir;
|
|
345
|
+
const geminiDir = projectMode ? path.join(projectRoot, ".gemini") : globalGeminiDir;
|
|
289
346
|
|
|
290
347
|
return {
|
|
291
348
|
cwd,
|
|
292
|
-
repoRoot:
|
|
349
|
+
repoRoot: repoRootDetected || cwd,
|
|
350
|
+
repoRootDetected,
|
|
293
351
|
skillsSource,
|
|
294
352
|
projectMode,
|
|
295
353
|
cliPath: context && context.cliPath ? context.cliPath : null,
|
|
296
354
|
claudeDir,
|
|
297
355
|
codexDir,
|
|
356
|
+
geminiDir,
|
|
357
|
+
globalClaudeDir,
|
|
358
|
+
globalCodexDir,
|
|
359
|
+
globalGeminiDir,
|
|
360
|
+
projectClaudeDir,
|
|
361
|
+
projectCodexDir,
|
|
362
|
+
projectGeminiDir,
|
|
298
363
|
claudeSkillsDir: path.join(claudeDir, SKILLS_DIR_NAME),
|
|
299
364
|
codexSkillsDir: path.join(codexDir, SKILLS_DIR_NAME),
|
|
365
|
+
geminiSkillsDir: path.join(geminiDir, SKILLS_DIR_NAME),
|
|
300
366
|
claudeSettingsPath: path.join(claudeDir, "settings.json"),
|
|
301
367
|
codexConfigPath: path.join(codexDir, "config.toml"),
|
|
368
|
+
statePath: path.join(globalClaudeDir, LOCAL_CLI_DIR, STATE_FILE_NAME),
|
|
302
369
|
};
|
|
303
370
|
}
|
|
304
371
|
|
|
@@ -384,29 +451,29 @@ function readLineSync() {
|
|
|
384
451
|
}
|
|
385
452
|
}
|
|
386
453
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
454
|
+
try {
|
|
455
|
+
while (true) {
|
|
456
|
+
let bytes = 0;
|
|
457
|
+
try {
|
|
458
|
+
bytes = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
if (bytes <= 0) {
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
input += buffer.toString("utf8", 0, bytes);
|
|
466
|
+
if (input.includes("\n")) {
|
|
467
|
+
break;
|
|
394
468
|
}
|
|
395
|
-
throw error;
|
|
396
|
-
}
|
|
397
|
-
if (bytes <= 0) {
|
|
398
|
-
break;
|
|
399
|
-
}
|
|
400
|
-
input += buffer.toString("utf8", 0, bytes);
|
|
401
|
-
if (input.includes("\n")) {
|
|
402
|
-
break;
|
|
403
469
|
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
470
|
+
} finally {
|
|
471
|
+
if (shouldClose) {
|
|
472
|
+
try {
|
|
473
|
+
fs.closeSync(fd);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
// Best-effort close; ignore failures.
|
|
476
|
+
}
|
|
410
477
|
}
|
|
411
478
|
}
|
|
412
479
|
return input.trim();
|
|
@@ -437,6 +504,7 @@ function linkSkills(sourceDir, targetDir, options, overwriteState) {
|
|
|
437
504
|
const skipped = [];
|
|
438
505
|
const overwritten = [];
|
|
439
506
|
const state = overwriteState || createOverwriteState(options);
|
|
507
|
+
const installMode = resolveInstallMode(options, "link");
|
|
440
508
|
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
441
509
|
|
|
442
510
|
entries.forEach((entry) => {
|
|
@@ -470,11 +538,11 @@ function linkSkills(sourceDir, targetDir, options, overwriteState) {
|
|
|
470
538
|
}
|
|
471
539
|
|
|
472
540
|
if (options["dry-run"]) {
|
|
473
|
-
created.push({ source: skillDir, target: targetPath, mode:
|
|
541
|
+
created.push({ source: skillDir, target: targetPath, mode: installMode, dryRun: true });
|
|
474
542
|
return;
|
|
475
543
|
}
|
|
476
544
|
|
|
477
|
-
if (
|
|
545
|
+
if (installMode === "copy") {
|
|
478
546
|
fs.cpSync(skillDir, targetPath, { recursive: true });
|
|
479
547
|
created.push({ source: skillDir, target: targetPath, mode: "copy" });
|
|
480
548
|
return;
|
|
@@ -493,6 +561,1171 @@ function linkSkills(sourceDir, targetDir, options, overwriteState) {
|
|
|
493
561
|
return { created, skipped, overwritten };
|
|
494
562
|
}
|
|
495
563
|
|
|
564
|
+
function buildSkillEnvironment(settings) {
|
|
565
|
+
const projectRoot = settings.repoRootDetected;
|
|
566
|
+
const scopeDirs = {
|
|
567
|
+
project: projectRoot
|
|
568
|
+
? {
|
|
569
|
+
claude: path.join(projectRoot, ".claude", SKILLS_DIR_NAME),
|
|
570
|
+
codex: path.join(projectRoot, ".codex", SKILLS_DIR_NAME),
|
|
571
|
+
gemini: path.join(projectRoot, ".gemini", SKILLS_DIR_NAME),
|
|
572
|
+
}
|
|
573
|
+
: null,
|
|
574
|
+
global: {
|
|
575
|
+
claude: path.join(settings.globalClaudeDir, SKILLS_DIR_NAME),
|
|
576
|
+
codex: path.join(settings.globalCodexDir, SKILLS_DIR_NAME),
|
|
577
|
+
gemini: path.join(settings.globalGeminiDir, SKILLS_DIR_NAME),
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
projectRoot,
|
|
583
|
+
scopeDirs,
|
|
584
|
+
statePath: settings.statePath,
|
|
585
|
+
skillsSource: settings.skillsSource,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function normalizeScopeList(scopeValue, projectRoot, defaultScope) {
|
|
590
|
+
const warnings = [];
|
|
591
|
+
const value = String(scopeValue || defaultScope || "both").toLowerCase();
|
|
592
|
+
let scopes = [];
|
|
593
|
+
if (value === "both" || value === "all") {
|
|
594
|
+
scopes = ["project", "global"];
|
|
595
|
+
} else if (value === "project" || value === "repo") {
|
|
596
|
+
scopes = ["project"];
|
|
597
|
+
} else if (value === "global") {
|
|
598
|
+
scopes = ["global"];
|
|
599
|
+
} else {
|
|
600
|
+
warnings.push(`Unknown scope "${scopeValue}", defaulting to both.`);
|
|
601
|
+
scopes = ["project", "global"];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!projectRoot) {
|
|
605
|
+
if (scopes.includes("project")) {
|
|
606
|
+
warnings.push("Project scope requested but no repo root detected; skipping project scope.");
|
|
607
|
+
}
|
|
608
|
+
scopes = scopes.filter((scope) => scope !== "project");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (!scopes.length) {
|
|
612
|
+
scopes = ["global"];
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return { scopes, warnings };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function normalizeTargetList(targetValue, defaultTarget) {
|
|
619
|
+
const warnings = [];
|
|
620
|
+
const value = String(targetValue || defaultTarget || "both").toLowerCase();
|
|
621
|
+
let targets = [];
|
|
622
|
+
if (value === "both" || value === "all") {
|
|
623
|
+
targets = ["claude", "codex", "gemini"];
|
|
624
|
+
} else if (value === "claude" || value === "codex" || value === "gemini") {
|
|
625
|
+
targets = [value];
|
|
626
|
+
} else {
|
|
627
|
+
warnings.push(`Unknown target "${targetValue}", defaulting to all.`);
|
|
628
|
+
targets = ["claude", "codex", "gemini"];
|
|
629
|
+
}
|
|
630
|
+
return { targets, warnings };
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function resolveInstallMode(options, fallbackMode) {
|
|
634
|
+
if (options && options.link) {
|
|
635
|
+
return "link";
|
|
636
|
+
}
|
|
637
|
+
if (options && options.copy) {
|
|
638
|
+
return "copy";
|
|
639
|
+
}
|
|
640
|
+
return fallbackMode || "link";
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function createEmptyState() {
|
|
644
|
+
return {
|
|
645
|
+
version: "1",
|
|
646
|
+
updated_at: new Date().toISOString(),
|
|
647
|
+
skills: [],
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function loadStateFile(statePath) {
|
|
652
|
+
if (!fs.existsSync(statePath)) {
|
|
653
|
+
return createEmptyState();
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
657
|
+
if (!parsed || typeof parsed !== "object") {
|
|
658
|
+
throw new Error("Invalid state file");
|
|
659
|
+
}
|
|
660
|
+
if (!Array.isArray(parsed.skills)) {
|
|
661
|
+
parsed.skills = [];
|
|
662
|
+
}
|
|
663
|
+
if (!parsed.version) {
|
|
664
|
+
parsed.version = "1";
|
|
665
|
+
}
|
|
666
|
+
if (!parsed.updated_at) {
|
|
667
|
+
parsed.updated_at = new Date().toISOString();
|
|
668
|
+
}
|
|
669
|
+
return parsed;
|
|
670
|
+
} catch (error) {
|
|
671
|
+
console.error("Warning: unable to parse state.json, recreating state file.");
|
|
672
|
+
return createEmptyState();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function saveStateFile(statePath, state, dryRun) {
|
|
677
|
+
if (dryRun) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
ensureDir(path.dirname(statePath), false);
|
|
681
|
+
fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function stateKey(name, scope, target) {
|
|
685
|
+
return `${target}:${scope}:${name}`;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function indexStateEntries(state) {
|
|
689
|
+
const map = new Map();
|
|
690
|
+
(state.skills || []).forEach((entry) => {
|
|
691
|
+
if (!entry || !entry.name) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
map.set(stateKey(entry.name, entry.scope, entry.target), entry);
|
|
695
|
+
});
|
|
696
|
+
return map;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function listBuiltInSkills(skillsSource) {
|
|
700
|
+
if (!skillsSource || !fs.existsSync(skillsSource)) {
|
|
701
|
+
return [];
|
|
702
|
+
}
|
|
703
|
+
return fs
|
|
704
|
+
.readdirSync(skillsSource, { withFileTypes: true })
|
|
705
|
+
.filter((entry) => entry.isDirectory())
|
|
706
|
+
.map((entry) => entry.name)
|
|
707
|
+
.filter((name) => fs.existsSync(path.join(skillsSource, name, "SKILL.md")))
|
|
708
|
+
.sort();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function resolveSkillInput(input, env) {
|
|
712
|
+
if (!input) {
|
|
713
|
+
return { error: "Missing skill name or path." };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const resolvedPath = path.resolve(input);
|
|
717
|
+
if (fs.existsSync(resolvedPath)) {
|
|
718
|
+
const stat = fs.statSync(resolvedPath);
|
|
719
|
+
const skillDir = stat.isDirectory() ? resolvedPath : path.dirname(resolvedPath);
|
|
720
|
+
const skillFile = path.join(skillDir, "SKILL.md");
|
|
721
|
+
if (!fs.existsSync(skillFile)) {
|
|
722
|
+
return { error: `SKILL.md not found in ${skillDir}` };
|
|
723
|
+
}
|
|
724
|
+
return { name: path.basename(skillDir), sourceDir: skillDir, kind: "path" };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const skillsSource = env.skillsSource;
|
|
728
|
+
if (!skillsSource) {
|
|
729
|
+
return { error: "No bundled skills directory found. Use a local path instead." };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const candidate = path.join(skillsSource, input);
|
|
733
|
+
if (!fs.existsSync(path.join(candidate, "SKILL.md"))) {
|
|
734
|
+
const available = listBuiltInSkills(skillsSource);
|
|
735
|
+
const sample = available.length ? ` Available: ${available.join(", ")}` : "";
|
|
736
|
+
return { error: `Skill "${input}" not found in bundled skills.${sample}` };
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return { name: input, sourceDir: candidate, kind: "name" };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function scanSkills(scopeDirs, scopes, targets, stateIndex) {
|
|
743
|
+
const records = [];
|
|
744
|
+
const warnings = [];
|
|
745
|
+
|
|
746
|
+
scopes.forEach((scope) => {
|
|
747
|
+
const dirs = scopeDirs[scope];
|
|
748
|
+
if (!dirs) {
|
|
749
|
+
warnings.push(`Scope "${scope}" not available.`);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
targets.forEach((target) => {
|
|
753
|
+
const dirPath = dirs[target];
|
|
754
|
+
if (!dirPath) {
|
|
755
|
+
warnings.push(`Target "${target}" not available for scope "${scope}".`);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
records.push(...scanSkillDir(dirPath, scope, target, stateIndex));
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
markDuplicates(records);
|
|
763
|
+
return { records, warnings };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function scanSkillDir(dirPath, scope, target, stateIndex) {
|
|
767
|
+
if (!dirPath || !fs.existsSync(dirPath)) {
|
|
768
|
+
return [];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const records = [];
|
|
772
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
773
|
+
const disabledDir = path.join(dirPath, DISABLED_DIR_NAME);
|
|
774
|
+
|
|
775
|
+
entries.forEach((entry) => {
|
|
776
|
+
if (entry.name.startsWith(".") || entry.name === DISABLED_DIR_NAME) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
records.push(buildSkillRecord(entry.name, path.join(dirPath, entry.name), scope, target, false, stateIndex));
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
if (fs.existsSync(disabledDir)) {
|
|
786
|
+
const disabledEntries = fs.readdirSync(disabledDir, { withFileTypes: true });
|
|
787
|
+
disabledEntries.forEach((entry) => {
|
|
788
|
+
if (entry.name.startsWith(".")) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
records.push(
|
|
795
|
+
buildSkillRecord(entry.name, path.join(disabledDir, entry.name), scope, target, true, stateIndex)
|
|
796
|
+
);
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return records;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function buildSkillRecord(name, entryPath, scope, target, disabled, stateIndex) {
|
|
804
|
+
const record = {
|
|
805
|
+
name,
|
|
806
|
+
scope,
|
|
807
|
+
target,
|
|
808
|
+
path: entryPath,
|
|
809
|
+
mode: "unknown",
|
|
810
|
+
status: "ok",
|
|
811
|
+
disabled: Boolean(disabled),
|
|
812
|
+
managed: false,
|
|
813
|
+
source: "",
|
|
814
|
+
duplicate: false,
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
const stat = fs.lstatSync(entryPath);
|
|
819
|
+
if (stat.isSymbolicLink()) {
|
|
820
|
+
record.mode = "link";
|
|
821
|
+
try {
|
|
822
|
+
record.source = fs.realpathSync(entryPath);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
record.status = "broken";
|
|
825
|
+
}
|
|
826
|
+
} else if (stat.isDirectory()) {
|
|
827
|
+
record.mode = "copy";
|
|
828
|
+
} else {
|
|
829
|
+
record.status = "missing";
|
|
830
|
+
}
|
|
831
|
+
} catch (error) {
|
|
832
|
+
record.status = "missing";
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if (record.status === "ok") {
|
|
836
|
+
const skillFile = path.join(entryPath, "SKILL.md");
|
|
837
|
+
if (!fs.existsSync(skillFile)) {
|
|
838
|
+
record.status = "missing-skill-file";
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (record.disabled) {
|
|
843
|
+
record.status = "disabled";
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (stateIndex) {
|
|
847
|
+
const entry = stateIndex.get(stateKey(name, scope, target));
|
|
848
|
+
if (entry) {
|
|
849
|
+
record.managed = true;
|
|
850
|
+
if (entry.source && !record.source) {
|
|
851
|
+
record.source = entry.source;
|
|
852
|
+
}
|
|
853
|
+
if (entry.mode && record.mode === "unknown") {
|
|
854
|
+
record.mode = entry.mode;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return record;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function markDuplicates(records) {
|
|
863
|
+
const counts = new Map();
|
|
864
|
+
records.forEach((record) => {
|
|
865
|
+
const key = `${record.target}:${record.name}`;
|
|
866
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
867
|
+
});
|
|
868
|
+
records.forEach((record) => {
|
|
869
|
+
const key = `${record.target}:${record.name}`;
|
|
870
|
+
if (counts.get(key) > 1) {
|
|
871
|
+
record.duplicate = true;
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function formatSkillStatus(record) {
|
|
877
|
+
const tags = [];
|
|
878
|
+
if (record.status && record.status !== "ok") {
|
|
879
|
+
tags.push(record.status);
|
|
880
|
+
}
|
|
881
|
+
if (record.duplicate) {
|
|
882
|
+
tags.push("duplicate");
|
|
883
|
+
}
|
|
884
|
+
if (!tags.length) {
|
|
885
|
+
tags.push("ok");
|
|
886
|
+
}
|
|
887
|
+
return tags.join(",");
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function printSkillList(records, format) {
|
|
891
|
+
const isJson = String(format || "").toLowerCase() === "json";
|
|
892
|
+
if (!records.length) {
|
|
893
|
+
if (isJson) {
|
|
894
|
+
console.log("[]");
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
console.log("No skills found.");
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
const sorted = [...records].sort((a, b) => {
|
|
902
|
+
if (a.name !== b.name) {
|
|
903
|
+
return a.name.localeCompare(b.name);
|
|
904
|
+
}
|
|
905
|
+
if (a.target !== b.target) {
|
|
906
|
+
return a.target.localeCompare(b.target);
|
|
907
|
+
}
|
|
908
|
+
return a.scope.localeCompare(b.scope);
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
if (isJson) {
|
|
912
|
+
console.log(JSON.stringify(sorted, null, 2));
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const headers = ["Name", "Target", "Scope", "Mode", "Status", "Managed", "Source", "Path"];
|
|
917
|
+
const rows = sorted.map((record) => [
|
|
918
|
+
record.name,
|
|
919
|
+
record.target,
|
|
920
|
+
record.scope,
|
|
921
|
+
record.mode,
|
|
922
|
+
formatSkillStatus(record),
|
|
923
|
+
record.managed ? "yes" : "no",
|
|
924
|
+
record.source || "-",
|
|
925
|
+
record.path,
|
|
926
|
+
]);
|
|
927
|
+
const widths = headers.map((header, index) =>
|
|
928
|
+
Math.max(header.length, ...rows.map((row) => String(row[index]).length))
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
const formatRow = (row) =>
|
|
932
|
+
row
|
|
933
|
+
.map((cell, index) => {
|
|
934
|
+
const value = String(cell);
|
|
935
|
+
return index === row.length - 1 ? value : value.padEnd(widths[index]);
|
|
936
|
+
})
|
|
937
|
+
.join(" ");
|
|
938
|
+
|
|
939
|
+
console.log(formatRow(headers));
|
|
940
|
+
console.log(formatRow(headers.map((header) => "-".repeat(header.length))));
|
|
941
|
+
rows.forEach((row) => console.log(formatRow(row)));
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function resolveSkillMatches(name, options, env, stateIndex, defaultScope) {
|
|
945
|
+
const scopeInfo = normalizeScopeList(options.scope, env.projectRoot, defaultScope || "both");
|
|
946
|
+
const targetInfo = normalizeTargetList(options.target, "both");
|
|
947
|
+
const scan = scanSkills(env.scopeDirs, scopeInfo.scopes, targetInfo.targets, stateIndex);
|
|
948
|
+
const matches = scan.records.filter((record) => record.name === name);
|
|
949
|
+
return { matches, scopeInfo, targetInfo, warnings: [...scopeInfo.warnings, ...targetInfo.warnings] };
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function installSkill(sourceDir, targetPath, installOptions) {
|
|
953
|
+
const mode = installOptions && installOptions.mode ? installOptions.mode : "link";
|
|
954
|
+
const dryRun = installOptions && installOptions.dryRun;
|
|
955
|
+
const overwrite = installOptions && installOptions.overwrite;
|
|
956
|
+
|
|
957
|
+
if (overwrite && fs.existsSync(targetPath)) {
|
|
958
|
+
if (!dryRun) {
|
|
959
|
+
safeUnlink(targetPath);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (dryRun) {
|
|
964
|
+
return { mode, dryRun: true };
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
ensureDir(path.dirname(targetPath), false);
|
|
968
|
+
|
|
969
|
+
if (mode === "copy") {
|
|
970
|
+
fs.cpSync(sourceDir, targetPath, { recursive: true });
|
|
971
|
+
return { mode: "copy" };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
975
|
+
try {
|
|
976
|
+
fs.symlinkSync(sourceDir, targetPath, linkType);
|
|
977
|
+
return { mode: "link" };
|
|
978
|
+
} catch (error) {
|
|
979
|
+
fs.cpSync(sourceDir, targetPath, { recursive: true });
|
|
980
|
+
return { mode: "copy", fallback: "symlink_failed" };
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function handleSkillsList(options, args, settings) {
|
|
985
|
+
const env = buildSkillEnvironment(settings);
|
|
986
|
+
const scopeInfo = normalizeScopeList(options.scope, env.projectRoot, "both");
|
|
987
|
+
const targetInfo = normalizeTargetList(options.target, "both");
|
|
988
|
+
const state = loadStateFile(env.statePath);
|
|
989
|
+
const stateIndex = indexStateEntries(state);
|
|
990
|
+
|
|
991
|
+
const scan = scanSkills(env.scopeDirs, scopeInfo.scopes, targetInfo.targets, stateIndex);
|
|
992
|
+
[...scopeInfo.warnings, ...targetInfo.warnings, ...scan.warnings].forEach((warning) =>
|
|
993
|
+
console.error(`Warning: ${warning}`)
|
|
994
|
+
);
|
|
995
|
+
printSkillList(scan.records, options.format);
|
|
996
|
+
return Promise.resolve();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function handleSkillsInfo(options, args, settings) {
|
|
1000
|
+
const name = args[0];
|
|
1001
|
+
if (!name) {
|
|
1002
|
+
console.error("Usage: agent-playbook skills info <name>");
|
|
1003
|
+
process.exitCode = 1;
|
|
1004
|
+
return Promise.resolve();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const env = buildSkillEnvironment(settings);
|
|
1008
|
+
const scopeInfo = normalizeScopeList(options.scope, env.projectRoot, "both");
|
|
1009
|
+
const targetInfo = normalizeTargetList(options.target, "both");
|
|
1010
|
+
const state = loadStateFile(env.statePath);
|
|
1011
|
+
const stateIndex = indexStateEntries(state);
|
|
1012
|
+
const scan = scanSkills(env.scopeDirs, scopeInfo.scopes, targetInfo.targets, stateIndex);
|
|
1013
|
+
[...scopeInfo.warnings, ...targetInfo.warnings, ...scan.warnings].forEach((warning) =>
|
|
1014
|
+
console.error(`Warning: ${warning}`)
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
const matches = scan.records.filter((record) => record.name === name);
|
|
1018
|
+
if (!matches.length) {
|
|
1019
|
+
console.error(`Skill not found: ${name}`);
|
|
1020
|
+
process.exitCode = 1;
|
|
1021
|
+
return Promise.resolve();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
printSkillList(matches, options.format);
|
|
1025
|
+
return Promise.resolve();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function handleSkillsAdd(options, args, settings) {
|
|
1029
|
+
const input = args[0];
|
|
1030
|
+
if (!input) {
|
|
1031
|
+
console.error("Usage: agent-playbook skills add <name|path>");
|
|
1032
|
+
process.exitCode = 1;
|
|
1033
|
+
return Promise.resolve();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const env = buildSkillEnvironment(settings);
|
|
1037
|
+
const defaultScope = env.projectRoot ? "project" : "global";
|
|
1038
|
+
const scopeInfo = normalizeScopeList(options.scope, env.projectRoot, defaultScope);
|
|
1039
|
+
const targetInfo = normalizeTargetList(options.target, "both");
|
|
1040
|
+
const resolved = resolveSkillInput(input, env);
|
|
1041
|
+
const installMode = resolveInstallMode(options, "link");
|
|
1042
|
+
|
|
1043
|
+
if (resolved.error) {
|
|
1044
|
+
console.error(resolved.error);
|
|
1045
|
+
process.exitCode = 1;
|
|
1046
|
+
return Promise.resolve();
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
[...scopeInfo.warnings, ...targetInfo.warnings].forEach((warning) =>
|
|
1050
|
+
console.error(`Warning: ${warning}`)
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
const state = loadStateFile(env.statePath);
|
|
1054
|
+
const stateIndex = indexStateEntries(state);
|
|
1055
|
+
const overwriteState = createOverwriteState(options);
|
|
1056
|
+
const now = new Date().toISOString();
|
|
1057
|
+
const created = [];
|
|
1058
|
+
const skipped = [];
|
|
1059
|
+
|
|
1060
|
+
scopeInfo.scopes.forEach((scope) => {
|
|
1061
|
+
const dirs = env.scopeDirs[scope];
|
|
1062
|
+
if (!dirs) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
targetInfo.targets.forEach((target) => {
|
|
1066
|
+
const targetDir = dirs[target];
|
|
1067
|
+
if (!targetDir) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
ensureDir(targetDir, options["dry-run"]);
|
|
1071
|
+
const targetPath = path.join(targetDir, resolved.name);
|
|
1072
|
+
if (fs.existsSync(targetPath)) {
|
|
1073
|
+
if (!shouldOverwriteExisting(options, overwriteState, targetPath)) {
|
|
1074
|
+
skipped.push({ scope, target, path: targetPath });
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (!options["dry-run"]) {
|
|
1078
|
+
safeUnlink(targetPath);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const install = installSkill(resolved.sourceDir, targetPath, {
|
|
1083
|
+
mode: installMode,
|
|
1084
|
+
dryRun: options["dry-run"],
|
|
1085
|
+
});
|
|
1086
|
+
created.push({ scope, target, path: targetPath, mode: install.mode });
|
|
1087
|
+
|
|
1088
|
+
const key = stateKey(resolved.name, scope, target);
|
|
1089
|
+
const entry = stateIndex.get(key) || {
|
|
1090
|
+
name: resolved.name,
|
|
1091
|
+
scope,
|
|
1092
|
+
target,
|
|
1093
|
+
managed_by: "apb",
|
|
1094
|
+
installed_at: now,
|
|
1095
|
+
};
|
|
1096
|
+
entry.source = resolved.sourceDir;
|
|
1097
|
+
entry.mode = install.mode;
|
|
1098
|
+
entry.disabled = false;
|
|
1099
|
+
entry.updated_at = now;
|
|
1100
|
+
stateIndex.set(key, entry);
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
state.skills = Array.from(stateIndex.values());
|
|
1105
|
+
state.updated_at = now;
|
|
1106
|
+
saveStateFile(env.statePath, state, options["dry-run"]);
|
|
1107
|
+
|
|
1108
|
+
console.log(`Added skill "${resolved.name}".`);
|
|
1109
|
+
if (created.length) {
|
|
1110
|
+
created.forEach((item) =>
|
|
1111
|
+
console.log(`- ${item.scope}/${item.target}: ${item.path} (${item.mode})`)
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
if (skipped.length) {
|
|
1115
|
+
skipped.forEach((item) => console.log(`- Skipped ${item.scope}/${item.target}: ${item.path}`));
|
|
1116
|
+
}
|
|
1117
|
+
if (options["dry-run"]) {
|
|
1118
|
+
console.log("- Dry run: no changes written.");
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
return Promise.resolve();
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function handleSkillsRemove(options, args, settings) {
|
|
1125
|
+
const name = args[0];
|
|
1126
|
+
if (!name) {
|
|
1127
|
+
console.error("Usage: agent-playbook skills remove <name>");
|
|
1128
|
+
process.exitCode = 1;
|
|
1129
|
+
return Promise.resolve();
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
const env = buildSkillEnvironment(settings);
|
|
1133
|
+
const state = loadStateFile(env.statePath);
|
|
1134
|
+
const stateIndex = indexStateEntries(state);
|
|
1135
|
+
const matchesInfo = resolveSkillMatches(name, options, env, stateIndex, "both");
|
|
1136
|
+
matchesInfo.warnings.forEach((warning) => console.error(`Warning: ${warning}`));
|
|
1137
|
+
|
|
1138
|
+
const matches = matchesInfo.matches;
|
|
1139
|
+
const hasFilters = Boolean(options.scope || options.target);
|
|
1140
|
+
if (!matches.length) {
|
|
1141
|
+
const stateKeys = Array.from(stateIndex.keys()).filter((key) => key.endsWith(`:${name}`));
|
|
1142
|
+
if (stateKeys.length) {
|
|
1143
|
+
stateKeys.forEach((key) => stateIndex.delete(key));
|
|
1144
|
+
state.skills = Array.from(stateIndex.values());
|
|
1145
|
+
state.updated_at = new Date().toISOString();
|
|
1146
|
+
saveStateFile(env.statePath, state, options["dry-run"]);
|
|
1147
|
+
console.log(`Removed ${stateKeys.length} state entries for "${name}".`);
|
|
1148
|
+
return Promise.resolve();
|
|
1149
|
+
}
|
|
1150
|
+
console.error(`Skill not found: ${name}`);
|
|
1151
|
+
process.exitCode = 1;
|
|
1152
|
+
return Promise.resolve();
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (!hasFilters && matches.length > 1) {
|
|
1156
|
+
console.error(`Multiple matches for "${name}". Use --scope or --target to disambiguate.`);
|
|
1157
|
+
matches.forEach((match) =>
|
|
1158
|
+
console.error(`- ${match.scope}/${match.target}: ${match.path}`)
|
|
1159
|
+
);
|
|
1160
|
+
process.exitCode = 1;
|
|
1161
|
+
return Promise.resolve();
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const removed = [];
|
|
1165
|
+
const skipped = [];
|
|
1166
|
+
|
|
1167
|
+
matches.forEach((match) => {
|
|
1168
|
+
const key = stateKey(match.name, match.scope, match.target);
|
|
1169
|
+
const managed = stateIndex.has(key);
|
|
1170
|
+
if (!managed && !options.force) {
|
|
1171
|
+
skipped.push({ scope: match.scope, target: match.target, path: match.path });
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (!options["dry-run"]) {
|
|
1175
|
+
safeUnlink(match.path);
|
|
1176
|
+
}
|
|
1177
|
+
stateIndex.delete(key);
|
|
1178
|
+
removed.push({ scope: match.scope, target: match.target, path: match.path });
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
state.skills = Array.from(stateIndex.values());
|
|
1182
|
+
state.updated_at = new Date().toISOString();
|
|
1183
|
+
saveStateFile(env.statePath, state, options["dry-run"]);
|
|
1184
|
+
|
|
1185
|
+
if (removed.length) {
|
|
1186
|
+
removed.forEach((item) => console.log(`Removed ${item.scope}/${item.target}: ${item.path}`));
|
|
1187
|
+
}
|
|
1188
|
+
if (skipped.length) {
|
|
1189
|
+
skipped.forEach((item) =>
|
|
1190
|
+
console.log(`Skipped unmanaged ${item.scope}/${item.target}: ${item.path} (use --force)`)
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
if (options["dry-run"]) {
|
|
1194
|
+
console.log("- Dry run: no changes written.");
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return Promise.resolve();
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function handleSkillsDisable(options, args, settings) {
|
|
1201
|
+
const name = args[0];
|
|
1202
|
+
if (!name) {
|
|
1203
|
+
console.error("Usage: agent-playbook skills disable <name>");
|
|
1204
|
+
process.exitCode = 1;
|
|
1205
|
+
return Promise.resolve();
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const env = buildSkillEnvironment(settings);
|
|
1209
|
+
const state = loadStateFile(env.statePath);
|
|
1210
|
+
const stateIndex = indexStateEntries(state);
|
|
1211
|
+
const matchesInfo = resolveSkillMatches(name, options, env, stateIndex, "both");
|
|
1212
|
+
matchesInfo.warnings.forEach((warning) => console.error(`Warning: ${warning}`));
|
|
1213
|
+
|
|
1214
|
+
const candidates = matchesInfo.matches.filter((match) => !match.disabled);
|
|
1215
|
+
if (!candidates.length) {
|
|
1216
|
+
console.error(`No enabled skill found for "${name}".`);
|
|
1217
|
+
process.exitCode = 1;
|
|
1218
|
+
return Promise.resolve();
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const hasFilters = Boolean(options.scope || options.target);
|
|
1222
|
+
if (!hasFilters && candidates.length > 1) {
|
|
1223
|
+
console.error(`Multiple matches for "${name}". Use --scope or --target to disambiguate.`);
|
|
1224
|
+
candidates.forEach((match) =>
|
|
1225
|
+
console.error(`- ${match.scope}/${match.target}: ${match.path}`)
|
|
1226
|
+
);
|
|
1227
|
+
process.exitCode = 1;
|
|
1228
|
+
return Promise.resolve();
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const overwriteState = createOverwriteState(options);
|
|
1232
|
+
const now = new Date().toISOString();
|
|
1233
|
+
const disabled = [];
|
|
1234
|
+
|
|
1235
|
+
candidates.forEach((match) => {
|
|
1236
|
+
const skillsRoot = path.dirname(match.path);
|
|
1237
|
+
const disabledDir = path.join(skillsRoot, DISABLED_DIR_NAME);
|
|
1238
|
+
const disabledPath = path.join(disabledDir, match.name);
|
|
1239
|
+
if (fs.existsSync(disabledPath)) {
|
|
1240
|
+
if (!shouldOverwriteExisting(options, overwriteState, disabledPath)) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
if (!options["dry-run"]) {
|
|
1244
|
+
safeUnlink(disabledPath);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
ensureDir(disabledDir, options["dry-run"]);
|
|
1248
|
+
if (!options["dry-run"]) {
|
|
1249
|
+
fs.renameSync(match.path, disabledPath);
|
|
1250
|
+
}
|
|
1251
|
+
disabled.push({ scope: match.scope, target: match.target, path: disabledPath });
|
|
1252
|
+
|
|
1253
|
+
const key = stateKey(match.name, match.scope, match.target);
|
|
1254
|
+
const entry = stateIndex.get(key);
|
|
1255
|
+
if (entry) {
|
|
1256
|
+
entry.disabled = true;
|
|
1257
|
+
entry.updated_at = now;
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
state.skills = Array.from(stateIndex.values());
|
|
1262
|
+
state.updated_at = now;
|
|
1263
|
+
saveStateFile(env.statePath, state, options["dry-run"]);
|
|
1264
|
+
|
|
1265
|
+
disabled.forEach((item) => console.log(`Disabled ${item.scope}/${item.target}: ${item.path}`));
|
|
1266
|
+
if (options["dry-run"]) {
|
|
1267
|
+
console.log("- Dry run: no changes written.");
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
return Promise.resolve();
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function handleSkillsEnable(options, args, settings) {
|
|
1274
|
+
const name = args[0];
|
|
1275
|
+
if (!name) {
|
|
1276
|
+
console.error("Usage: agent-playbook skills enable <name>");
|
|
1277
|
+
process.exitCode = 1;
|
|
1278
|
+
return Promise.resolve();
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const env = buildSkillEnvironment(settings);
|
|
1282
|
+
const state = loadStateFile(env.statePath);
|
|
1283
|
+
const stateIndex = indexStateEntries(state);
|
|
1284
|
+
const matchesInfo = resolveSkillMatches(name, options, env, stateIndex, "both");
|
|
1285
|
+
matchesInfo.warnings.forEach((warning) => console.error(`Warning: ${warning}`));
|
|
1286
|
+
|
|
1287
|
+
const candidates = matchesInfo.matches.filter((match) => match.disabled);
|
|
1288
|
+
if (!candidates.length) {
|
|
1289
|
+
console.error(`No disabled skill found for "${name}".`);
|
|
1290
|
+
process.exitCode = 1;
|
|
1291
|
+
return Promise.resolve();
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const hasFilters = Boolean(options.scope || options.target);
|
|
1295
|
+
if (!hasFilters && candidates.length > 1) {
|
|
1296
|
+
console.error(`Multiple matches for "${name}". Use --scope or --target to disambiguate.`);
|
|
1297
|
+
candidates.forEach((match) =>
|
|
1298
|
+
console.error(`- ${match.scope}/${match.target}: ${match.path}`)
|
|
1299
|
+
);
|
|
1300
|
+
process.exitCode = 1;
|
|
1301
|
+
return Promise.resolve();
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
const overwriteState = createOverwriteState(options);
|
|
1305
|
+
const now = new Date().toISOString();
|
|
1306
|
+
const enabled = [];
|
|
1307
|
+
|
|
1308
|
+
candidates.forEach((match) => {
|
|
1309
|
+
const skillsRoot = path.dirname(path.dirname(match.path));
|
|
1310
|
+
const targetPath = path.join(skillsRoot, match.name);
|
|
1311
|
+
if (fs.existsSync(targetPath)) {
|
|
1312
|
+
if (!shouldOverwriteExisting(options, overwriteState, targetPath)) {
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (!options["dry-run"]) {
|
|
1316
|
+
safeUnlink(targetPath);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (!options["dry-run"]) {
|
|
1320
|
+
fs.renameSync(match.path, targetPath);
|
|
1321
|
+
}
|
|
1322
|
+
enabled.push({ scope: match.scope, target: match.target, path: targetPath });
|
|
1323
|
+
|
|
1324
|
+
const key = stateKey(match.name, match.scope, match.target);
|
|
1325
|
+
const entry = stateIndex.get(key);
|
|
1326
|
+
if (entry) {
|
|
1327
|
+
entry.disabled = false;
|
|
1328
|
+
entry.updated_at = now;
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
state.skills = Array.from(stateIndex.values());
|
|
1333
|
+
state.updated_at = now;
|
|
1334
|
+
saveStateFile(env.statePath, state, options["dry-run"]);
|
|
1335
|
+
|
|
1336
|
+
enabled.forEach((item) => console.log(`Enabled ${item.scope}/${item.target}: ${item.path}`));
|
|
1337
|
+
if (options["dry-run"]) {
|
|
1338
|
+
console.log("- Dry run: no changes written.");
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
return Promise.resolve();
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function checkSkillPath(targetPath) {
|
|
1345
|
+
if (!fs.existsSync(targetPath)) {
|
|
1346
|
+
return "missing";
|
|
1347
|
+
}
|
|
1348
|
+
try {
|
|
1349
|
+
const stat = fs.lstatSync(targetPath);
|
|
1350
|
+
if (stat.isSymbolicLink()) {
|
|
1351
|
+
try {
|
|
1352
|
+
fs.realpathSync(targetPath);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
return "broken";
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
const skillFile = path.join(targetPath, "SKILL.md");
|
|
1358
|
+
if (!fs.existsSync(skillFile)) {
|
|
1359
|
+
return "missing-skill-file";
|
|
1360
|
+
}
|
|
1361
|
+
} catch (error) {
|
|
1362
|
+
return "missing";
|
|
1363
|
+
}
|
|
1364
|
+
return "ok";
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function handleSkillsDoctor(options, args, settings) {
|
|
1368
|
+
const env = buildSkillEnvironment(settings);
|
|
1369
|
+
const scopeInfo = normalizeScopeList(options.scope, env.projectRoot, "both");
|
|
1370
|
+
const targetInfo = normalizeTargetList(options.target, "both");
|
|
1371
|
+
const state = loadStateFile(env.statePath);
|
|
1372
|
+
const stateIndex = indexStateEntries(state);
|
|
1373
|
+
const scan = scanSkills(env.scopeDirs, scopeInfo.scopes, targetInfo.targets, stateIndex);
|
|
1374
|
+
|
|
1375
|
+
const issues = [];
|
|
1376
|
+
const duplicateKeys = new Set();
|
|
1377
|
+
|
|
1378
|
+
scan.records.forEach((record) => {
|
|
1379
|
+
if (record.status === "broken" || record.status === "missing-skill-file") {
|
|
1380
|
+
issues.push(`${record.scope}/${record.target}/${record.name}: ${record.status}`);
|
|
1381
|
+
}
|
|
1382
|
+
if (record.duplicate) {
|
|
1383
|
+
const key = `${record.target}:${record.name}`;
|
|
1384
|
+
if (!duplicateKeys.has(key)) {
|
|
1385
|
+
duplicateKeys.add(key);
|
|
1386
|
+
issues.push(`${record.target}/${record.name}: duplicate across scopes`);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (!record.managed) {
|
|
1390
|
+
issues.push(`${record.scope}/${record.target}/${record.name}: unmanaged skill`);
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
state.skills.forEach((entry) => {
|
|
1395
|
+
const dirs = env.scopeDirs[entry.scope];
|
|
1396
|
+
if (!dirs) {
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const root = dirs[entry.target];
|
|
1400
|
+
if (!root) {
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const activePath = path.join(root, entry.name);
|
|
1404
|
+
const disabledPath = path.join(root, DISABLED_DIR_NAME, entry.name);
|
|
1405
|
+
const pathToCheck = entry.disabled ? disabledPath : activePath;
|
|
1406
|
+
const status = checkSkillPath(pathToCheck);
|
|
1407
|
+
if (status !== "ok") {
|
|
1408
|
+
issues.push(`${entry.scope}/${entry.target}/${entry.name}: managed entry ${status}`);
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
if (issues.length) {
|
|
1413
|
+
console.error("Issues detected:");
|
|
1414
|
+
issues.forEach((issue) => console.error(`- ${issue}`));
|
|
1415
|
+
process.exitCode = 1;
|
|
1416
|
+
} else {
|
|
1417
|
+
console.log("No critical issues detected.");
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
if (options.fix) {
|
|
1421
|
+
const now = new Date().toISOString();
|
|
1422
|
+
const overwrite = true;
|
|
1423
|
+
let fixedCount = 0;
|
|
1424
|
+
|
|
1425
|
+
state.skills.forEach((entry) => {
|
|
1426
|
+
const dirs = env.scopeDirs[entry.scope];
|
|
1427
|
+
if (!dirs) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const root = dirs[entry.target];
|
|
1431
|
+
if (!root) {
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
const activePath = path.join(root, entry.name);
|
|
1435
|
+
const disabledPath = path.join(root, DISABLED_DIR_NAME, entry.name);
|
|
1436
|
+
if (entry.disabled) {
|
|
1437
|
+
const activeStatus = checkSkillPath(activePath);
|
|
1438
|
+
const disabledStatus = checkSkillPath(disabledPath);
|
|
1439
|
+
if (activeStatus === "ok") {
|
|
1440
|
+
if (disabledStatus === "ok") {
|
|
1441
|
+
if (!options["dry-run"]) {
|
|
1442
|
+
safeUnlink(disabledPath);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
entry.disabled = false;
|
|
1446
|
+
entry.updated_at = now;
|
|
1447
|
+
fixedCount += 1;
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
if (disabledStatus === "ok") {
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
if (!fs.existsSync(path.dirname(disabledPath))) {
|
|
1454
|
+
ensureDir(path.dirname(disabledPath), options["dry-run"]);
|
|
1455
|
+
}
|
|
1456
|
+
if (entry.source && fs.existsSync(entry.source)) {
|
|
1457
|
+
installSkill(entry.source, disabledPath, {
|
|
1458
|
+
mode: entry.mode || "link",
|
|
1459
|
+
dryRun: options["dry-run"],
|
|
1460
|
+
overwrite,
|
|
1461
|
+
});
|
|
1462
|
+
entry.updated_at = now;
|
|
1463
|
+
fixedCount += 1;
|
|
1464
|
+
}
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const activeStatus = checkSkillPath(activePath);
|
|
1468
|
+
if (activeStatus === "ok") {
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
if (entry.source && fs.existsSync(entry.source)) {
|
|
1472
|
+
installSkill(entry.source, activePath, {
|
|
1473
|
+
mode: entry.mode || "link",
|
|
1474
|
+
dryRun: options["dry-run"],
|
|
1475
|
+
overwrite,
|
|
1476
|
+
});
|
|
1477
|
+
entry.updated_at = now;
|
|
1478
|
+
fixedCount += 1;
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
state.updated_at = now;
|
|
1483
|
+
saveStateFile(env.statePath, state, options["dry-run"]);
|
|
1484
|
+
console.log(`Fixed ${fixedCount} managed entries.`);
|
|
1485
|
+
if (options["dry-run"]) {
|
|
1486
|
+
console.log("- Dry run: no changes written.");
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
return Promise.resolve();
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function handleSkillsSync(options, args, settings) {
|
|
1494
|
+
const env = buildSkillEnvironment(settings);
|
|
1495
|
+
const state = loadStateFile(env.statePath);
|
|
1496
|
+
const now = new Date().toISOString();
|
|
1497
|
+
let changed = false;
|
|
1498
|
+
|
|
1499
|
+
const nextSkills = [];
|
|
1500
|
+
state.skills.forEach((entry) => {
|
|
1501
|
+
const dirs = env.scopeDirs[entry.scope];
|
|
1502
|
+
if (!dirs) {
|
|
1503
|
+
changed = true;
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const root = dirs[entry.target];
|
|
1507
|
+
if (!root) {
|
|
1508
|
+
changed = true;
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
const activePath = path.join(root, entry.name);
|
|
1512
|
+
const disabledPath = path.join(root, DISABLED_DIR_NAME, entry.name);
|
|
1513
|
+
if (entry.disabled) {
|
|
1514
|
+
if (fs.existsSync(disabledPath)) {
|
|
1515
|
+
nextSkills.push(entry);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
if (fs.existsSync(activePath)) {
|
|
1519
|
+
entry.disabled = false;
|
|
1520
|
+
entry.updated_at = now;
|
|
1521
|
+
nextSkills.push(entry);
|
|
1522
|
+
changed = true;
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
changed = true;
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (fs.existsSync(activePath)) {
|
|
1529
|
+
nextSkills.push(entry);
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
if (fs.existsSync(disabledPath)) {
|
|
1533
|
+
entry.disabled = true;
|
|
1534
|
+
entry.updated_at = now;
|
|
1535
|
+
nextSkills.push(entry);
|
|
1536
|
+
changed = true;
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
changed = true;
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
state.skills = nextSkills;
|
|
1543
|
+
state.updated_at = now;
|
|
1544
|
+
if (changed) {
|
|
1545
|
+
saveStateFile(env.statePath, state, options["dry-run"]);
|
|
1546
|
+
console.log("State synchronized.");
|
|
1547
|
+
} else {
|
|
1548
|
+
console.log("State already in sync.");
|
|
1549
|
+
}
|
|
1550
|
+
if (options["dry-run"]) {
|
|
1551
|
+
console.log("- Dry run: no changes written.");
|
|
1552
|
+
}
|
|
1553
|
+
return Promise.resolve();
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function handleSkillsUpgrade(options, args, settings) {
|
|
1557
|
+
const env = buildSkillEnvironment(settings);
|
|
1558
|
+
const state = loadStateFile(env.statePath);
|
|
1559
|
+
const overwrite = true;
|
|
1560
|
+
const now = new Date().toISOString();
|
|
1561
|
+
const sourceRoot = options.source ? path.resolve(options.source) : env.skillsSource;
|
|
1562
|
+
const defaultMode = resolveInstallMode(options, "link");
|
|
1563
|
+
let upgraded = 0;
|
|
1564
|
+
let skipped = 0;
|
|
1565
|
+
|
|
1566
|
+
state.skills.forEach((entry) => {
|
|
1567
|
+
if (entry.disabled) {
|
|
1568
|
+
skipped += 1;
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const dirs = env.scopeDirs[entry.scope];
|
|
1572
|
+
if (!dirs) {
|
|
1573
|
+
skipped += 1;
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
const root = dirs[entry.target];
|
|
1577
|
+
if (!root) {
|
|
1578
|
+
skipped += 1;
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
const activePath = path.join(root, entry.name);
|
|
1582
|
+
let sourceDir = entry.source;
|
|
1583
|
+
if (sourceRoot) {
|
|
1584
|
+
const candidate = path.join(sourceRoot, entry.name);
|
|
1585
|
+
if (fs.existsSync(path.join(candidate, "SKILL.md"))) {
|
|
1586
|
+
sourceDir = candidate;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
if (!sourceDir || !fs.existsSync(sourceDir)) {
|
|
1590
|
+
skipped += 1;
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
const install = installSkill(sourceDir, activePath, {
|
|
1595
|
+
mode: entry.mode || defaultMode,
|
|
1596
|
+
dryRun: options["dry-run"],
|
|
1597
|
+
overwrite,
|
|
1598
|
+
});
|
|
1599
|
+
entry.source = sourceDir;
|
|
1600
|
+
entry.mode = install.mode;
|
|
1601
|
+
entry.updated_at = now;
|
|
1602
|
+
entry.installed_at = now;
|
|
1603
|
+
upgraded += 1;
|
|
1604
|
+
});
|
|
1605
|
+
|
|
1606
|
+
state.updated_at = now;
|
|
1607
|
+
saveStateFile(env.statePath, state, options["dry-run"]);
|
|
1608
|
+
console.log(`Upgraded ${upgraded} managed skills. Skipped ${skipped}.`);
|
|
1609
|
+
if (options["dry-run"]) {
|
|
1610
|
+
console.log("- Dry run: no changes written.");
|
|
1611
|
+
}
|
|
1612
|
+
return Promise.resolve();
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
function handleSkillsExport(options, args, settings) {
|
|
1616
|
+
const outputPath = options.output;
|
|
1617
|
+
if (!outputPath) {
|
|
1618
|
+
console.error("Usage: agent-playbook skills export --output <file>");
|
|
1619
|
+
process.exitCode = 1;
|
|
1620
|
+
return Promise.resolve();
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
const env = buildSkillEnvironment(settings);
|
|
1624
|
+
const state = loadStateFile(env.statePath);
|
|
1625
|
+
const resolved = path.resolve(outputPath);
|
|
1626
|
+
ensureDir(path.dirname(resolved), options["dry-run"]);
|
|
1627
|
+
if (!options["dry-run"]) {
|
|
1628
|
+
fs.writeFileSync(resolved, JSON.stringify(state, null, 2));
|
|
1629
|
+
}
|
|
1630
|
+
console.log(`Exported state to ${resolved}`);
|
|
1631
|
+
if (options["dry-run"]) {
|
|
1632
|
+
console.log("- Dry run: no changes written.");
|
|
1633
|
+
}
|
|
1634
|
+
return Promise.resolve();
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function handleSkillsImport(options, args, settings) {
|
|
1638
|
+
const inputPath = args[0];
|
|
1639
|
+
if (!inputPath) {
|
|
1640
|
+
console.error("Usage: agent-playbook skills import <file>");
|
|
1641
|
+
process.exitCode = 1;
|
|
1642
|
+
return Promise.resolve();
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const resolved = path.resolve(inputPath);
|
|
1646
|
+
if (!fs.existsSync(resolved)) {
|
|
1647
|
+
console.error(`Import file not found: ${resolved}`);
|
|
1648
|
+
process.exitCode = 1;
|
|
1649
|
+
return Promise.resolve();
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
let imported;
|
|
1653
|
+
try {
|
|
1654
|
+
imported = JSON.parse(fs.readFileSync(resolved, "utf8"));
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
console.error("Invalid import file.");
|
|
1657
|
+
process.exitCode = 1;
|
|
1658
|
+
return Promise.resolve();
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const env = buildSkillEnvironment(settings);
|
|
1662
|
+
const overwriteState = createOverwriteState(options);
|
|
1663
|
+
const now = new Date().toISOString();
|
|
1664
|
+
const defaultMode = resolveInstallMode(options, "link");
|
|
1665
|
+
const skills = Array.isArray(imported.skills) ? imported.skills : [];
|
|
1666
|
+
const state = {
|
|
1667
|
+
version: imported.version || "1",
|
|
1668
|
+
updated_at: now,
|
|
1669
|
+
skills: skills,
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
let applied = 0;
|
|
1673
|
+
let skipped = 0;
|
|
1674
|
+
skills.forEach((entry) => {
|
|
1675
|
+
if (!entry || !entry.name || !entry.scope || !entry.target) {
|
|
1676
|
+
skipped += 1;
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
if (entry.disabled) {
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
const dirs = env.scopeDirs[entry.scope];
|
|
1683
|
+
if (!dirs) {
|
|
1684
|
+
skipped += 1;
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
const root = dirs[entry.target];
|
|
1688
|
+
if (!root) {
|
|
1689
|
+
skipped += 1;
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
const targetPath = path.join(root, entry.name);
|
|
1693
|
+
let sourceDir = entry.source;
|
|
1694
|
+
if (options.source) {
|
|
1695
|
+
const candidate = path.join(path.resolve(options.source), entry.name);
|
|
1696
|
+
if (fs.existsSync(path.join(candidate, "SKILL.md"))) {
|
|
1697
|
+
sourceDir = candidate;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
if (!sourceDir || !fs.existsSync(sourceDir)) {
|
|
1701
|
+
skipped += 1;
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (fs.existsSync(targetPath)) {
|
|
1705
|
+
if (!shouldOverwriteExisting(options, overwriteState, targetPath)) {
|
|
1706
|
+
skipped += 1;
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
if (!options["dry-run"]) {
|
|
1710
|
+
safeUnlink(targetPath);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
installSkill(sourceDir, targetPath, {
|
|
1714
|
+
mode: entry.mode || defaultMode,
|
|
1715
|
+
dryRun: options["dry-run"],
|
|
1716
|
+
});
|
|
1717
|
+
entry.updated_at = now;
|
|
1718
|
+
applied += 1;
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
saveStateFile(env.statePath, state, options["dry-run"]);
|
|
1722
|
+
console.log(`Imported state (${applied} applied, ${skipped} skipped).`);
|
|
1723
|
+
if (options["dry-run"]) {
|
|
1724
|
+
console.log("- Dry run: no changes written.");
|
|
1725
|
+
}
|
|
1726
|
+
return Promise.resolve();
|
|
1727
|
+
}
|
|
1728
|
+
|
|
496
1729
|
function ensureLocalCli(settings, context, options) {
|
|
497
1730
|
const baseDir = settings.projectMode ? settings.claudeDir : settings.claudeDir;
|
|
498
1731
|
const cliRoot = path.join(baseDir, LOCAL_CLI_DIR);
|
|
@@ -522,6 +1755,9 @@ function updateClaudeSettings(settings, cliPath, options) {
|
|
|
522
1755
|
const data = existing || {};
|
|
523
1756
|
|
|
524
1757
|
data.hooks = data.hooks || {};
|
|
1758
|
+
const marker = `--hook-source ${HOOK_SOURCE_VALUE}`;
|
|
1759
|
+
data.hooks = removeHookCommand(data.hooks, "SessionEnd", marker);
|
|
1760
|
+
data.hooks = removeHookCommand(data.hooks, "PostToolUse", marker);
|
|
525
1761
|
|
|
526
1762
|
let sessionCommand = buildHookCommand(cliPath, "session-log");
|
|
527
1763
|
sessionCommand = `${sessionCommand} --hook-source ${HOOK_SOURCE_VALUE}`;
|
|
@@ -658,8 +1894,11 @@ function upsertCodexBlock(content, values) {
|
|
|
658
1894
|
}
|
|
659
1895
|
|
|
660
1896
|
function removeCodexBlock(content) {
|
|
661
|
-
const pattern = /^\[agent_playbook\][\s\S]*?(?=^\[|\s*$)/
|
|
662
|
-
|
|
1897
|
+
const pattern = /^\[agent_playbook\][\s\S]*?(?=^\[|\s*$)/gm;
|
|
1898
|
+
const cleaned = content.replace(pattern, "");
|
|
1899
|
+
const legacyPattern =
|
|
1900
|
+
/(?:\n\s*version\s*=\s*\"[^\"]*\"\s*\n\s*installed_at\s*=\s*\"[^\"]*\"\s*)+$/;
|
|
1901
|
+
return cleaned.replace(legacyPattern, "\n").trimEnd();
|
|
663
1902
|
}
|
|
664
1903
|
|
|
665
1904
|
function buildHookCommand(cliPath, subcommand) {
|
|
@@ -941,6 +2180,7 @@ function collectStatus(settings) {
|
|
|
941
2180
|
codexConfigPath: settings.codexConfigPath,
|
|
942
2181
|
claudeSkillsDir: settings.claudeSkillsDir,
|
|
943
2182
|
codexSkillsDir: settings.codexSkillsDir,
|
|
2183
|
+
geminiSkillsDir: settings.geminiSkillsDir,
|
|
944
2184
|
claudeSettingsReadable: claudeSettings !== null || !fs.existsSync(settings.claudeSettingsPath),
|
|
945
2185
|
codexBlockPresent: hasCodexBlock(settings.codexConfigPath),
|
|
946
2186
|
hooksInstalled: hasHooks(settings.claudeSettingsPath),
|
|
@@ -948,6 +2188,7 @@ function collectStatus(settings) {
|
|
|
948
2188
|
localCliPresent: fs.existsSync(path.join(settings.claudeDir, LOCAL_CLI_DIR, "bin", "agent-playbook.js")),
|
|
949
2189
|
claudeSkillCount: countSkills(settings.claudeSkillsDir),
|
|
950
2190
|
codexSkillCount: countSkills(settings.codexSkillsDir),
|
|
2191
|
+
geminiSkillCount: countSkills(settings.geminiSkillsDir),
|
|
951
2192
|
};
|
|
952
2193
|
}
|
|
953
2194
|
|
|
@@ -995,27 +2236,37 @@ function printStatus(status) {
|
|
|
995
2236
|
console.log(`- Codex config: ${status.codexConfigPath}`);
|
|
996
2237
|
console.log(`- Claude skills: ${status.claudeSkillsDir}`);
|
|
997
2238
|
console.log(`- Codex skills: ${status.codexSkillsDir}`);
|
|
2239
|
+
console.log(`- Gemini skills: ${status.geminiSkillsDir}`);
|
|
998
2240
|
console.log(`- Claude skills count: ${status.claudeSkillCount}`);
|
|
999
2241
|
console.log(`- Codex skills count: ${status.codexSkillCount}`);
|
|
2242
|
+
console.log(`- Gemini skills count: ${status.geminiSkillCount}`);
|
|
1000
2243
|
console.log(`- Hooks installed: ${status.hooksInstalled ? "yes" : "no"}`);
|
|
1001
2244
|
console.log(`- Manifest present: ${status.manifestPresent ? "yes" : "no"}`);
|
|
1002
2245
|
console.log(`- Local CLI present: ${status.localCliPresent ? "yes" : "no"}`);
|
|
1003
2246
|
console.log(`- Codex config block: ${status.codexBlockPresent ? "yes" : "no"}`);
|
|
1004
2247
|
}
|
|
1005
2248
|
|
|
1006
|
-
function printInitSummary(settings, hooksEnabled, options, claudeLinks, codexLinks, warnings) {
|
|
2249
|
+
function printInitSummary(settings, hooksEnabled, options, claudeLinks, codexLinks, geminiLinks, warnings) {
|
|
1007
2250
|
console.log("Init complete.");
|
|
1008
2251
|
console.log(`- Claude skills: ${settings.claudeSkillsDir}`);
|
|
1009
2252
|
console.log(`- Codex skills: ${settings.codexSkillsDir}`);
|
|
2253
|
+
console.log(`- Gemini skills: ${settings.geminiSkillsDir}`);
|
|
1010
2254
|
console.log(`- Hooks: ${hooksEnabled ? "enabled" : "disabled"}`);
|
|
1011
|
-
|
|
2255
|
+
const linkedCount =
|
|
2256
|
+
claudeLinks.created.length + codexLinks.created.length + (geminiLinks ? geminiLinks.created.length : 0);
|
|
2257
|
+
console.log(`- Linked skills: ${linkedCount}`);
|
|
1012
2258
|
const overwrittenCount =
|
|
1013
2259
|
(claudeLinks.overwritten ? claudeLinks.overwritten.length : 0) +
|
|
1014
|
-
(codexLinks.overwritten ? codexLinks.overwritten.length : 0)
|
|
2260
|
+
(codexLinks.overwritten ? codexLinks.overwritten.length : 0) +
|
|
2261
|
+
(geminiLinks && geminiLinks.overwritten ? geminiLinks.overwritten.length : 0);
|
|
1015
2262
|
if (overwrittenCount) {
|
|
1016
2263
|
console.log(`- Overwritten skills: ${overwrittenCount}`);
|
|
1017
2264
|
}
|
|
1018
|
-
if (
|
|
2265
|
+
if (
|
|
2266
|
+
claudeLinks.skipped.length ||
|
|
2267
|
+
codexLinks.skipped.length ||
|
|
2268
|
+
(geminiLinks && geminiLinks.skipped.length)
|
|
2269
|
+
) {
|
|
1019
2270
|
console.log("- Some skills were skipped due to existing paths.");
|
|
1020
2271
|
}
|
|
1021
2272
|
if (warnings && warnings.length) {
|
|
@@ -1181,6 +2432,7 @@ function handleRepair(options, context) {
|
|
|
1181
2432
|
if (!options["dry-run"]) {
|
|
1182
2433
|
ensureDir(settings.claudeSkillsDir, false);
|
|
1183
2434
|
ensureDir(settings.codexSkillsDir, false);
|
|
2435
|
+
ensureDir(settings.geminiSkillsDir, false);
|
|
1184
2436
|
}
|
|
1185
2437
|
|
|
1186
2438
|
if (!status.localCliPresent) {
|
|
@@ -1205,6 +2457,7 @@ function handleRepair(options, context) {
|
|
|
1205
2457
|
if (settings.skillsSource) {
|
|
1206
2458
|
linkSkills(settings.skillsSource, settings.claudeSkillsDir, options, overwriteState);
|
|
1207
2459
|
linkSkills(settings.skillsSource, settings.codexSkillsDir, options, overwriteState);
|
|
2460
|
+
linkSkills(settings.skillsSource, settings.geminiSkillsDir, options, overwriteState);
|
|
1208
2461
|
if (!options["dry-run"]) {
|
|
1209
2462
|
const manifestPath = path.join(settings.claudeSkillsDir, ".agent-playbook.json");
|
|
1210
2463
|
if (!fs.existsSync(manifestPath)) {
|
|
@@ -1214,12 +2467,20 @@ function handleRepair(options, context) {
|
|
|
1214
2467
|
installedAt: new Date().toISOString(),
|
|
1215
2468
|
repairedAt: new Date().toISOString(),
|
|
1216
2469
|
repoRoot: settings.repoRoot,
|
|
1217
|
-
links: { claude: [], codex: [] },
|
|
2470
|
+
links: { claude: [], codex: [], gemini: [] },
|
|
1218
2471
|
});
|
|
1219
2472
|
}
|
|
1220
2473
|
}
|
|
1221
2474
|
}
|
|
1222
2475
|
|
|
1223
|
-
printInitSummary(
|
|
2476
|
+
printInitSummary(
|
|
2477
|
+
settings,
|
|
2478
|
+
true,
|
|
2479
|
+
options,
|
|
2480
|
+
{ created: [], skipped: [] },
|
|
2481
|
+
{ created: [], skipped: [] },
|
|
2482
|
+
{ created: [], skipped: [] },
|
|
2483
|
+
warnings
|
|
2484
|
+
);
|
|
1224
2485
|
return Promise.resolve();
|
|
1225
2486
|
}
|