shoplazza-ai-dev-cli 0.1.0 → 0.1.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/dist/cli.mjs +1920 -353
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -16,13 +16,13 @@ import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep
|
|
|
16
16
|
import { homedir, platform, tmpdir } from "os";
|
|
17
17
|
import { URL as URL$1, fileURLToPath } from "url";
|
|
18
18
|
import { stripVTControlCharacters } from "node:util";
|
|
19
|
+
import { access, chmod, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
|
|
19
20
|
import { gunzipSync } from "node:zlib";
|
|
20
21
|
import { mkdirSync as mkdirSync$1, writeFileSync as writeFileSync$1 } from "node:fs";
|
|
21
22
|
import { dirname as dirname$1, normalize as normalize$1, resolve as resolve$1, sep as sep$1 } from "node:path";
|
|
23
|
+
import { parse } from "yaml";
|
|
22
24
|
import * as readline from "readline";
|
|
23
25
|
import { Writable } from "stream";
|
|
24
|
-
import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, symlink, writeFile } from "fs/promises";
|
|
25
|
-
import { parse } from "yaml";
|
|
26
26
|
import { createHash } from "crypto";
|
|
27
27
|
import { createServer } from "http";
|
|
28
28
|
var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
|
|
@@ -284,6 +284,95 @@ function isWellKnownUrl(input) {
|
|
|
284
284
|
return false;
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
+
const FORGE_STORE_DIRNAME = ".ai-dev-cli";
|
|
288
|
+
const home = homedir();
|
|
289
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
290
|
+
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
|
|
291
|
+
const cursorHome = join(home, ".cursor");
|
|
292
|
+
function getAgentHome(type) {
|
|
293
|
+
switch (type) {
|
|
294
|
+
case "claude-code": return claudeHome;
|
|
295
|
+
case "codex": return codexHome;
|
|
296
|
+
case "cursor": return cursorHome;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const agents = {
|
|
300
|
+
"claude-code": {
|
|
301
|
+
name: "claude-code",
|
|
302
|
+
displayName: "Claude Code",
|
|
303
|
+
skillsDir: ".claude/skills",
|
|
304
|
+
globalSkillsDir: join(claudeHome, "skills"),
|
|
305
|
+
detectInstalled: async () => {
|
|
306
|
+
return existsSync(claudeHome);
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
codex: {
|
|
310
|
+
name: "codex",
|
|
311
|
+
displayName: "Codex",
|
|
312
|
+
skillsDir: ".agents/skills",
|
|
313
|
+
globalSkillsDir: join(codexHome, "skills"),
|
|
314
|
+
detectInstalled: async () => {
|
|
315
|
+
return existsSync(codexHome) || existsSync("/etc/codex");
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
cursor: {
|
|
319
|
+
name: "cursor",
|
|
320
|
+
displayName: "Cursor",
|
|
321
|
+
skillsDir: ".agents/skills",
|
|
322
|
+
globalSkillsDir: join(home, ".cursor/skills"),
|
|
323
|
+
detectInstalled: async () => {
|
|
324
|
+
return existsSync(join(home, ".cursor"));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
async function detectInstalledAgents() {
|
|
329
|
+
return (await Promise.all(Object.entries(agents).map(async ([type, config]) => ({
|
|
330
|
+
type,
|
|
331
|
+
installed: await config.detectInstalled()
|
|
332
|
+
})))).filter((r) => r.installed).map((r) => r.type);
|
|
333
|
+
}
|
|
334
|
+
function getUniversalAgents() {
|
|
335
|
+
return Object.entries(agents).filter(([_, config]) => config.skillsDir === ".agents/skills" && config.showInUniversalList !== false).map(([type]) => type);
|
|
336
|
+
}
|
|
337
|
+
function isUniversalAgent(type) {
|
|
338
|
+
return agents[type].skillsDir === ".agents/skills";
|
|
339
|
+
}
|
|
340
|
+
function getStoreRoot(_scope, _cwd) {
|
|
341
|
+
return join(home, FORGE_STORE_DIRNAME, "store");
|
|
342
|
+
}
|
|
343
|
+
function getAgentEntryRoot(agent, scope, cwd) {
|
|
344
|
+
if (scope === "global") return getAgentHome(agent);
|
|
345
|
+
return cwd ?? process.cwd();
|
|
346
|
+
}
|
|
347
|
+
function getInstallTargets(agent, component, scope, cwd) {
|
|
348
|
+
const entryRoot = getAgentEntryRoot(agent, scope, cwd);
|
|
349
|
+
switch (component.kind) {
|
|
350
|
+
case "plugin-skill": return {
|
|
351
|
+
canonical: join(component.pluginCanonical, "skills", component.skillName),
|
|
352
|
+
entry: agent === "claude-code" ? join(entryRoot, ".claude", "skills", component.skillName) : agent === "cursor" ? join(entryRoot, ".cursor", "skills", component.skillName) : join(entryRoot, ".agents", "skills", component.skillName)
|
|
353
|
+
};
|
|
354
|
+
case "plugin-agent-manifest": return {
|
|
355
|
+
canonical: join(component.pluginCanonical, "agents", component.filename),
|
|
356
|
+
entry: agent === "claude-code" ? join(entryRoot, ".claude", "agents", component.filename) : agent === "cursor" ? join(entryRoot, ".cursor", "agents", component.filename) : join(entryRoot, ".codex", "agents", component.filename)
|
|
357
|
+
};
|
|
358
|
+
case "plugin-agent-deps-dir": return {
|
|
359
|
+
canonical: join(component.pluginCanonical, "agents", component.dirname),
|
|
360
|
+
entry: agent === "claude-code" ? join(entryRoot, ".claude", "agents", component.dirname) : agent === "cursor" ? join(entryRoot, ".cursor", "agents", component.dirname) : join(entryRoot, ".codex", "agents", component.dirname)
|
|
361
|
+
};
|
|
362
|
+
case "plugin-rule-manifest": {
|
|
363
|
+
const canonical = join(component.pluginCanonical, "rules", component.filename);
|
|
364
|
+
const cursorName = component.filename.endsWith(".md") ? component.filename.slice(0, -3) + ".mdc" : component.filename;
|
|
365
|
+
return {
|
|
366
|
+
canonical,
|
|
367
|
+
entry: agent === "claude-code" ? join(entryRoot, ".claude", "rules", component.filename) : agent === "cursor" ? join(entryRoot, ".cursor", "rules", cursorName) : ""
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
case "plugin-rule-deps-dir": return {
|
|
371
|
+
canonical: join(component.pluginCanonical, "rules", component.dirname),
|
|
372
|
+
entry: agent === "claude-code" ? join(entryRoot, ".claude", "rules", component.dirname) : agent === "cursor" ? join(entryRoot, ".cursor", "rules", component.dirname) : ""
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
287
376
|
const CONFIG_DIR = join(homedir(), ".ai-dev-cli");
|
|
288
377
|
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
289
378
|
const DEFAULT_PORTAL = "https://forge.shoplazza.site";
|
|
@@ -401,6 +490,268 @@ function configCommand(args) {
|
|
|
401
490
|
console.error("Available: get | set | unset | list | path");
|
|
402
491
|
process.exit(2);
|
|
403
492
|
}
|
|
493
|
+
const FORGE_HOOK_BASENAME$1 = "track-skill-read.mjs";
|
|
494
|
+
async function upsertCursorHooks(hooksJsonPath, scriptPath) {
|
|
495
|
+
let raw = "";
|
|
496
|
+
try {
|
|
497
|
+
raw = await readFile(hooksJsonPath, "utf-8");
|
|
498
|
+
} catch {}
|
|
499
|
+
const cfg = raw.trim() ? JSON.parse(raw) : { version: 1 };
|
|
500
|
+
if (!cfg.hooks) cfg.hooks = {};
|
|
501
|
+
if (!Array.isArray(cfg.hooks.postToolUse)) cfg.hooks.postToolUse = [];
|
|
502
|
+
let readMatcher = cfg.hooks.postToolUse.find((m) => m.matcher === "Read");
|
|
503
|
+
if (!readMatcher) {
|
|
504
|
+
readMatcher = {
|
|
505
|
+
matcher: "Read",
|
|
506
|
+
hooks: []
|
|
507
|
+
};
|
|
508
|
+
cfg.hooks.postToolUse.push(readMatcher);
|
|
509
|
+
}
|
|
510
|
+
if (!Array.isArray(readMatcher.hooks)) readMatcher.hooks = [];
|
|
511
|
+
const command = `node ${shellEscape$2(scriptPath)}`;
|
|
512
|
+
const ourEntry = readMatcher.hooks.find((h) => h.command.includes(FORGE_HOOK_BASENAME$1));
|
|
513
|
+
if (ourEntry) {
|
|
514
|
+
if (ourEntry.command !== command) ourEntry.command = command;
|
|
515
|
+
} else readMatcher.hooks.push({
|
|
516
|
+
type: "command",
|
|
517
|
+
command
|
|
518
|
+
});
|
|
519
|
+
await mkdir(dirname(hooksJsonPath), { recursive: true });
|
|
520
|
+
await writeFile(hooksJsonPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
521
|
+
}
|
|
522
|
+
function shellEscape$2(p) {
|
|
523
|
+
if (!/[\s"'$`\\]/.test(p)) return p;
|
|
524
|
+
return `'${p.replace(/'/g, `'\\''`)}'`;
|
|
525
|
+
}
|
|
526
|
+
function upsertCodexAgent(toml, entry) {
|
|
527
|
+
const header = `[agents.${entry.id}]`;
|
|
528
|
+
return upsertSection(toml, header, `${header}\nconfig_file = ${jsonString(entry.configFile)}\n`);
|
|
529
|
+
}
|
|
530
|
+
function removeCodexAgent(toml, id) {
|
|
531
|
+
return removeSection(toml, `[agents.${id}]`);
|
|
532
|
+
}
|
|
533
|
+
function ensureCodexHooksFeature(toml) {
|
|
534
|
+
const lines = toml.split("\n");
|
|
535
|
+
let inFeatures = false;
|
|
536
|
+
let hasFlag = false;
|
|
537
|
+
let featuresLineIdx = -1;
|
|
538
|
+
for (const [i, line] of lines.entries()) {
|
|
539
|
+
const trimmed = line.trim();
|
|
540
|
+
if (trimmed === "[features]") {
|
|
541
|
+
inFeatures = true;
|
|
542
|
+
featuresLineIdx = i;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]") && trimmed !== "[features]") {
|
|
546
|
+
inFeatures = false;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
if (inFeatures && /^codex_hooks\s*=/.test(trimmed)) {
|
|
550
|
+
hasFlag = true;
|
|
551
|
+
lines[i] = "codex_hooks = true";
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (featuresLineIdx === -1) return toml + (toml.length > 0 && !toml.endsWith("\n") ? "\n" : "") + "\n[features]\ncodex_hooks = true\n";
|
|
555
|
+
if (!hasFlag) lines.splice(featuresLineIdx + 1, 0, "codex_hooks = true");
|
|
556
|
+
return lines.join("\n");
|
|
557
|
+
}
|
|
558
|
+
function upsertSection(toml, header, newBlock) {
|
|
559
|
+
const idx = toml.indexOf(header);
|
|
560
|
+
if (idx < 0) return toml + (toml.length > 0 && !toml.endsWith("\n") ? "\n" : "") + "\n" + newBlock;
|
|
561
|
+
const after = toml.indexOf("\n[", idx + header.length);
|
|
562
|
+
const blockEnd = after < 0 ? toml.length : after + 1;
|
|
563
|
+
return toml.slice(0, idx) + newBlock + toml.slice(blockEnd);
|
|
564
|
+
}
|
|
565
|
+
function removeSection(toml, header) {
|
|
566
|
+
const idx = toml.indexOf(header);
|
|
567
|
+
if (idx < 0) return toml;
|
|
568
|
+
const after = toml.indexOf("\n[", idx + header.length);
|
|
569
|
+
const blockEnd = after < 0 ? toml.length : after + 1;
|
|
570
|
+
let start = idx;
|
|
571
|
+
while (start > 0 && toml[start - 1] === "\n" && (start - 2 < 0 || toml[start - 2] === "\n")) start--;
|
|
572
|
+
return toml.slice(0, start) + toml.slice(blockEnd);
|
|
573
|
+
}
|
|
574
|
+
function jsonString(s) {
|
|
575
|
+
return JSON.stringify(s);
|
|
576
|
+
}
|
|
577
|
+
const FORGE_HOOK_BASENAME = "track-skill-read.mjs";
|
|
578
|
+
async function upsertCodexHooks(hooksJsonPath, configTomlPath, scriptPath) {
|
|
579
|
+
let raw = "";
|
|
580
|
+
try {
|
|
581
|
+
raw = await readFile(hooksJsonPath, "utf-8");
|
|
582
|
+
} catch {}
|
|
583
|
+
const cfg = raw.trim() ? JSON.parse(raw) : {};
|
|
584
|
+
if (!Array.isArray(cfg.UserPromptSubmit)) cfg.UserPromptSubmit = [];
|
|
585
|
+
let m = cfg.UserPromptSubmit.find((x) => x.matcher === ".*");
|
|
586
|
+
if (!m) {
|
|
587
|
+
m = {
|
|
588
|
+
matcher: ".*",
|
|
589
|
+
hooks: []
|
|
590
|
+
};
|
|
591
|
+
cfg.UserPromptSubmit.push(m);
|
|
592
|
+
}
|
|
593
|
+
if (!Array.isArray(m.hooks)) m.hooks = [];
|
|
594
|
+
const command = `node ${shellEscape$1(scriptPath)} --codex-mode`;
|
|
595
|
+
const our = m.hooks.find((h) => h.command.includes(FORGE_HOOK_BASENAME));
|
|
596
|
+
if (our) {
|
|
597
|
+
if (our.command !== command) our.command = command;
|
|
598
|
+
} else m.hooks.push({
|
|
599
|
+
type: "command",
|
|
600
|
+
command
|
|
601
|
+
});
|
|
602
|
+
await mkdir(dirname(hooksJsonPath), { recursive: true });
|
|
603
|
+
await writeFile(hooksJsonPath, JSON.stringify(cfg, null, 2) + "\n", "utf-8");
|
|
604
|
+
let configRaw = "";
|
|
605
|
+
try {
|
|
606
|
+
configRaw = await readFile(configTomlPath, "utf-8");
|
|
607
|
+
} catch {}
|
|
608
|
+
const updated = ensureCodexHooksFeature(configRaw);
|
|
609
|
+
if (updated !== configRaw) {
|
|
610
|
+
await mkdir(dirname(configTomlPath), { recursive: true });
|
|
611
|
+
await writeFile(configTomlPath, updated, "utf-8");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function shellEscape$1(p) {
|
|
615
|
+
if (!/[\s"'$`\\]/.test(p)) return p;
|
|
616
|
+
return `'${p.replace(/'/g, `'\\''`)}'`;
|
|
617
|
+
}
|
|
618
|
+
const HOOK_SCRIPT_BASENAME = "track-skill-read.mjs";
|
|
619
|
+
const HOOK_DIRNAME = "forge-hooks";
|
|
620
|
+
const FORGE_HOOK_TAG = "forge:track-skill-read";
|
|
621
|
+
async function writeForgeSourceManifest(skillDir, manifest) {
|
|
622
|
+
const target = join(skillDir, ".forge-source.json");
|
|
623
|
+
await mkdir(skillDir, { recursive: true });
|
|
624
|
+
await writeFile(target, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
625
|
+
}
|
|
626
|
+
function getForgeHookScript(portalUrl) {
|
|
627
|
+
const portal = portalUrl.replace(/\/$/, "");
|
|
628
|
+
return `#!/usr/bin/env node
|
|
629
|
+
// ${FORGE_HOOK_TAG}
|
|
630
|
+
// Auto-generated by ai-dev-cli. Do not edit; run \`ai-dev-cli add\` to refresh.
|
|
631
|
+
// Reports {type:'skill_invoked', asset_ref, install_mode} when the agent reads
|
|
632
|
+
// SKILL.md from a forge-installed skill (identified by .forge-source.json).
|
|
633
|
+
// Anonymous: /events accepts unauthenticated posts.
|
|
634
|
+
import fs from 'node:fs';
|
|
635
|
+
import path from 'node:path';
|
|
636
|
+
|
|
637
|
+
const PORTAL = ${JSON.stringify(portal)};
|
|
638
|
+
|
|
639
|
+
setTimeout(() => process.exit(0), 5000);
|
|
640
|
+
|
|
641
|
+
let buf = '';
|
|
642
|
+
process.stdin.on('data', (c) => (buf += c));
|
|
643
|
+
process.stdin.on('end', () => {
|
|
644
|
+
try {
|
|
645
|
+
const ev = JSON.parse(buf);
|
|
646
|
+
const filePath = ev.tool_input?.file_path || '';
|
|
647
|
+
if (!filePath.endsWith(path.sep + 'SKILL.md') && !filePath.endsWith('/SKILL.md')) return;
|
|
648
|
+
|
|
649
|
+
let dir = path.dirname(filePath);
|
|
650
|
+
let manifest = null;
|
|
651
|
+
for (let i = 0; i < 8 && dir && dir !== path.dirname(dir); i++) {
|
|
652
|
+
const p = path.join(dir, '.forge-source.json');
|
|
653
|
+
if (fs.existsSync(p)) {
|
|
654
|
+
manifest = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
dir = path.dirname(dir);
|
|
658
|
+
}
|
|
659
|
+
if (!manifest?.ref) return;
|
|
660
|
+
|
|
661
|
+
fetch(PORTAL + '/events', {
|
|
662
|
+
method: 'POST',
|
|
663
|
+
headers: { 'Content-Type': 'application/json' },
|
|
664
|
+
body: JSON.stringify({
|
|
665
|
+
event_type: 'skill_invoked',
|
|
666
|
+
asset_ref: manifest.ref,
|
|
667
|
+
install_mode: manifest.install_mode || 'claude-code',
|
|
668
|
+
metadata: {
|
|
669
|
+
parent_plugin_ref: manifest.parent_plugin_ref || null,
|
|
670
|
+
},
|
|
671
|
+
}),
|
|
672
|
+
signal: AbortSignal.timeout(2000),
|
|
673
|
+
}).catch(() => {});
|
|
674
|
+
} catch {
|
|
675
|
+
/* swallow */
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
`;
|
|
679
|
+
}
|
|
680
|
+
async function ensureForgeHooks(agent) {
|
|
681
|
+
const warnings = [];
|
|
682
|
+
try {
|
|
683
|
+
const portal = loadConfig().portal || DEFAULT_PORTAL;
|
|
684
|
+
const home = getAgentHome(agent);
|
|
685
|
+
const scriptPath = join(home, HOOK_DIRNAME, HOOK_SCRIPT_BASENAME);
|
|
686
|
+
await writeHookScript(scriptPath, portal);
|
|
687
|
+
if (agent === "claude-code") await upsertClaudeCodeSettings(join(home, "settings.json"), scriptPath);
|
|
688
|
+
else if (agent === "cursor") await upsertCursorHooks(join(home, "hooks.json"), scriptPath);
|
|
689
|
+
else if (agent === "codex") await upsertCodexHooks(join(home, "hooks.json"), join(home, "config.toml"), scriptPath);
|
|
690
|
+
} catch (err) {
|
|
691
|
+
warnings.push(`Failed to install forge hooks for ${agent}: ${err instanceof Error ? err.message : String(err)}`);
|
|
692
|
+
}
|
|
693
|
+
return { warnings };
|
|
694
|
+
}
|
|
695
|
+
async function writeHookScript(scriptPath, portalUrl) {
|
|
696
|
+
const next = getForgeHookScript(portalUrl);
|
|
697
|
+
let prev = null;
|
|
698
|
+
if (existsSync(scriptPath)) try {
|
|
699
|
+
prev = await readFile(scriptPath, "utf-8");
|
|
700
|
+
} catch {
|
|
701
|
+
prev = null;
|
|
702
|
+
}
|
|
703
|
+
if (prev === next) return;
|
|
704
|
+
await mkdir(dirname(scriptPath), { recursive: true });
|
|
705
|
+
await writeFile(scriptPath, next, {
|
|
706
|
+
encoding: "utf-8",
|
|
707
|
+
mode: 493
|
|
708
|
+
});
|
|
709
|
+
try {
|
|
710
|
+
await chmod(scriptPath, 493);
|
|
711
|
+
} catch {}
|
|
712
|
+
}
|
|
713
|
+
async function upsertClaudeCodeSettings(settingsPath, scriptPath) {
|
|
714
|
+
const command = `node ${shellEscape(scriptPath)}`;
|
|
715
|
+
let raw = "";
|
|
716
|
+
try {
|
|
717
|
+
raw = await readFile(settingsPath, "utf-8");
|
|
718
|
+
} catch {
|
|
719
|
+
raw = "";
|
|
720
|
+
}
|
|
721
|
+
let settings = {};
|
|
722
|
+
if (raw.trim().length > 0) try {
|
|
723
|
+
settings = JSON.parse(raw);
|
|
724
|
+
} catch (err) {
|
|
725
|
+
throw new Error(`Refusing to overwrite invalid JSON at ${settingsPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
726
|
+
}
|
|
727
|
+
if (!settings.hooks || typeof settings.hooks !== "object") settings.hooks = {};
|
|
728
|
+
if (!Array.isArray(settings.hooks.PostToolUse)) settings.hooks.PostToolUse = [];
|
|
729
|
+
const matcherList = settings.hooks.PostToolUse;
|
|
730
|
+
let readMatcher = matcherList.find((m) => m && m.matcher === "Read");
|
|
731
|
+
if (!readMatcher) {
|
|
732
|
+
readMatcher = {
|
|
733
|
+
matcher: "Read",
|
|
734
|
+
hooks: []
|
|
735
|
+
};
|
|
736
|
+
matcherList.push(readMatcher);
|
|
737
|
+
}
|
|
738
|
+
if (!Array.isArray(readMatcher.hooks)) readMatcher.hooks = [];
|
|
739
|
+
const ourEntry = readMatcher.hooks.find((h) => typeof h?.command === "string" && h.command.includes(HOOK_SCRIPT_BASENAME));
|
|
740
|
+
if (ourEntry) {
|
|
741
|
+
if (ourEntry.command === command && ourEntry.type === "command") return;
|
|
742
|
+
ourEntry.type = "command";
|
|
743
|
+
ourEntry.command = command;
|
|
744
|
+
} else readMatcher.hooks.push({
|
|
745
|
+
type: "command",
|
|
746
|
+
command
|
|
747
|
+
});
|
|
748
|
+
await mkdir(dirname(settingsPath), { recursive: true });
|
|
749
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
750
|
+
}
|
|
751
|
+
function shellEscape(p) {
|
|
752
|
+
if (!/[\s"'$`\\]/.test(p)) return p;
|
|
753
|
+
return `'${p.replace(/'/g, `'\\''`)}'`;
|
|
754
|
+
}
|
|
404
755
|
async function fetchSkillArchive(ref, portalUrl, bearerToken) {
|
|
405
756
|
const url = `${portalUrl.replace(/\/$/, "")}/api/cli/skills/archive?ref=${encodeURIComponent(ref)}`;
|
|
406
757
|
const headers = { Accept: "application/gzip" };
|
|
@@ -421,6 +772,48 @@ async function fetchSkillArchive(ref, portalUrl, bearerToken) {
|
|
|
421
772
|
}
|
|
422
773
|
return Buffer.from(await resp.arrayBuffer());
|
|
423
774
|
}
|
|
775
|
+
async function fetchTeamAssets(teamScope, portalUrl, bearerToken) {
|
|
776
|
+
const url = `${portalUrl.replace(/\/$/, "")}/api/cli/teams/${encodeURIComponent(teamScope)}/assets`;
|
|
777
|
+
const headers = {
|
|
778
|
+
Accept: "application/json",
|
|
779
|
+
Authorization: `Bearer ${bearerToken}`
|
|
780
|
+
};
|
|
781
|
+
let resp;
|
|
782
|
+
try {
|
|
783
|
+
resp = await fetch(url, { headers });
|
|
784
|
+
} catch (err) {
|
|
785
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
786
|
+
throw new Error(`Failed to reach portal at ${portalUrl}: ${msg}`);
|
|
787
|
+
}
|
|
788
|
+
if (resp.status === 401) throw new Error("Login required: run `shoplazza-ai-dev-cli login` first");
|
|
789
|
+
if (resp.status === 403) throw new Error(`Not a member of team "${teamScope}"`);
|
|
790
|
+
if (resp.status === 404) throw new Error(`Team not found: ${teamScope}`);
|
|
791
|
+
if (!resp.ok) {
|
|
792
|
+
const body = await resp.text().catch(() => "");
|
|
793
|
+
throw new Error(`Portal returned ${resp.status} ${resp.statusText}${body ? `: ${body}` : ""}`);
|
|
794
|
+
}
|
|
795
|
+
return (await resp.json()).items ?? [];
|
|
796
|
+
}
|
|
797
|
+
async function fetchPluginArchive(ref, portalUrl, bearerToken) {
|
|
798
|
+
const url = `${portalUrl.replace(/\/$/, "")}/api/cli/plugins/archive?ref=${encodeURIComponent(ref)}`;
|
|
799
|
+
const headers = { Accept: "application/gzip" };
|
|
800
|
+
if (bearerToken) headers.Authorization = `Bearer ${bearerToken}`;
|
|
801
|
+
let resp;
|
|
802
|
+
try {
|
|
803
|
+
resp = await fetch(url, { headers });
|
|
804
|
+
} catch (err) {
|
|
805
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
806
|
+
throw new Error(`Failed to reach portal at ${portalUrl}: ${msg}`);
|
|
807
|
+
}
|
|
808
|
+
if (resp.status === 401) throw new Error("Login required: run `ai-dev-cli login` first");
|
|
809
|
+
if (resp.status === 403) throw new Error(`Permission denied for ${ref}`);
|
|
810
|
+
if (resp.status === 404) throw new Error(`Plugin not found: ${ref}`);
|
|
811
|
+
if (!resp.ok) {
|
|
812
|
+
const body = await resp.text().catch(() => "");
|
|
813
|
+
throw new Error(`Portal returned ${resp.status} ${resp.statusText}${body ? `: ${body}` : ""}`);
|
|
814
|
+
}
|
|
815
|
+
return Buffer.from(await resp.arrayBuffer());
|
|
816
|
+
}
|
|
424
817
|
function extractTarGz(gz, dest) {
|
|
425
818
|
const tar = gunzipSync(gz);
|
|
426
819
|
const destAbs = resolve$1(dest);
|
|
@@ -456,6 +849,117 @@ function readCStr(buf, start, len) {
|
|
|
456
849
|
while (end < slice.length && slice[end] !== 0) end++;
|
|
457
850
|
return slice.subarray(0, end).toString("utf-8");
|
|
458
851
|
}
|
|
852
|
+
const FORGE_SIDECARS = [".forge-source.json", ".forge-plugin.json"];
|
|
853
|
+
async function assertEntryReplaceable(entryPath, ctx) {
|
|
854
|
+
let st;
|
|
855
|
+
try {
|
|
856
|
+
st = await lstat(entryPath);
|
|
857
|
+
} catch (e) {
|
|
858
|
+
if (e.code === "ENOENT") return { kind: "absent" };
|
|
859
|
+
throw e;
|
|
860
|
+
}
|
|
861
|
+
if (st.isSymbolicLink()) {
|
|
862
|
+
const target = await readlink(entryPath);
|
|
863
|
+
const absTarget = isAbsolute(target) ? target : resolve(dirname(entryPath), target);
|
|
864
|
+
if (!pathInside(ctx.storeRoot, absTarget)) return {
|
|
865
|
+
kind: "foreign-symlink",
|
|
866
|
+
realPath: absTarget
|
|
867
|
+
};
|
|
868
|
+
const ref = await readForgeRef(absTarget);
|
|
869
|
+
if (ref === ctx.intendedRef || pathsEqual(absTarget, ctx.intendedCanonical)) return { kind: "forge-self" };
|
|
870
|
+
return {
|
|
871
|
+
kind: "forge-other",
|
|
872
|
+
otherRef: ref ?? "<unknown>"
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
if (st.isDirectory()) {
|
|
876
|
+
const ref = await readForgeRef(entryPath);
|
|
877
|
+
if (ref === ctx.intendedRef) return { kind: "forge-self" };
|
|
878
|
+
if (ref) return {
|
|
879
|
+
kind: "forge-other",
|
|
880
|
+
otherRef: ref
|
|
881
|
+
};
|
|
882
|
+
return {
|
|
883
|
+
kind: "foreign-dir",
|
|
884
|
+
realPath: entryPath
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
return {
|
|
888
|
+
kind: "foreign-file",
|
|
889
|
+
realPath: entryPath
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function pathInside(root, p) {
|
|
893
|
+
const r = relative(resolve(root), resolve(p));
|
|
894
|
+
return r === "" || !r.startsWith("..") && !isAbsolute(r);
|
|
895
|
+
}
|
|
896
|
+
function pathsEqual(a, b) {
|
|
897
|
+
return resolve(a) === resolve(b);
|
|
898
|
+
}
|
|
899
|
+
async function readForgeRef(p) {
|
|
900
|
+
for (const name of FORGE_SIDECARS) try {
|
|
901
|
+
const buf = await readFile(join(p, name), "utf-8");
|
|
902
|
+
const json = JSON.parse(buf);
|
|
903
|
+
if (typeof json.ref === "string") return json.ref;
|
|
904
|
+
} catch {}
|
|
905
|
+
const parent = dirname(p);
|
|
906
|
+
if (parent === p) return null;
|
|
907
|
+
for (const name of FORGE_SIDECARS) try {
|
|
908
|
+
const buf = await readFile(join(parent, name), "utf-8");
|
|
909
|
+
const json = JSON.parse(buf);
|
|
910
|
+
if (typeof json.ref === "string") return json.ref;
|
|
911
|
+
} catch {}
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
async function promptForeignOverwrite(conflicts, opts = {}) {
|
|
915
|
+
if (conflicts.length === 0) return true;
|
|
916
|
+
if (opts.yes || opts.force) return true;
|
|
917
|
+
if (!process.stdin.isTTY) return false;
|
|
918
|
+
console.log();
|
|
919
|
+
console.log(import_picocolors.default.yellow(`⚠ Found ${conflicts.length} conflicts with user-owned content:`));
|
|
920
|
+
console.log();
|
|
921
|
+
for (const c of conflicts) console.log(` ${c.entryPath} (${c.kind})`);
|
|
922
|
+
console.log();
|
|
923
|
+
console.log("These are not managed by forge. Overwriting will replace your content.");
|
|
924
|
+
const choice = await ve({
|
|
925
|
+
message: "Force overwrite all?",
|
|
926
|
+
options: [
|
|
927
|
+
{
|
|
928
|
+
value: "no",
|
|
929
|
+
label: "no — abort install (default)"
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
value: "yes",
|
|
933
|
+
label: "yes — replace everything (existing content will be lost)"
|
|
934
|
+
},
|
|
935
|
+
{
|
|
936
|
+
value: "detail",
|
|
937
|
+
label: "show details (file sizes / dir trees)"
|
|
938
|
+
}
|
|
939
|
+
],
|
|
940
|
+
initialValue: "no"
|
|
941
|
+
});
|
|
942
|
+
if (pD(choice)) return false;
|
|
943
|
+
if (choice === "detail") {
|
|
944
|
+
console.log();
|
|
945
|
+
for (const c of conflicts) console.log(` ${c.entryPath} → ${c.kind}`);
|
|
946
|
+
return promptForeignOverwrite(conflicts, opts);
|
|
947
|
+
}
|
|
948
|
+
return choice === "yes";
|
|
949
|
+
}
|
|
950
|
+
const AGENTS_DIR$2 = ".agents";
|
|
951
|
+
const SKILLS_SUBDIR = "skills";
|
|
952
|
+
function parseFrontmatter(raw) {
|
|
953
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
954
|
+
if (!match) return {
|
|
955
|
+
data: {},
|
|
956
|
+
content: raw
|
|
957
|
+
};
|
|
958
|
+
return {
|
|
959
|
+
data: parse(match[1]) ?? {},
|
|
960
|
+
content: match[2] ?? ""
|
|
961
|
+
};
|
|
962
|
+
}
|
|
459
963
|
const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
|
|
460
964
|
const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
|
|
461
965
|
const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
|
|
@@ -468,298 +972,40 @@ function stripTerminalEscapes(str) {
|
|
|
468
972
|
function sanitizeMetadata(str) {
|
|
469
973
|
return stripTerminalEscapes(str).replace(/[\r\n]+/g, " ").trim();
|
|
470
974
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const S_STEP_CANCEL = import_picocolors.default.red("■");
|
|
476
|
-
const S_STEP_SUBMIT = import_picocolors.default.green("◇");
|
|
477
|
-
const S_RADIO_ACTIVE = import_picocolors.default.green("●");
|
|
478
|
-
const S_RADIO_INACTIVE = import_picocolors.default.dim("○");
|
|
479
|
-
import_picocolors.default.green("✓");
|
|
480
|
-
const S_BULLET = import_picocolors.default.green("•");
|
|
481
|
-
const S_BAR = import_picocolors.default.dim("│");
|
|
482
|
-
const S_BAR_H = import_picocolors.default.dim("─");
|
|
483
|
-
const cancelSymbol = Symbol("cancel");
|
|
484
|
-
function approxStringWidth(plain) {
|
|
485
|
-
let width = 0;
|
|
486
|
-
for (const ch of plain) {
|
|
487
|
-
const code = ch.codePointAt(0);
|
|
488
|
-
if (code === 0) continue;
|
|
489
|
-
width += code >= 4352 && code <= 4447 || code >= 8986 && code <= 8987 || code >= 9001 && code <= 9002 || code >= 9193 && code <= 9196 || code === 9200 || code === 9203 || code >= 9725 && code <= 9726 || code >= 9748 && code <= 9749 || code >= 9800 && code <= 9811 || code >= 9855 && code <= 9855 || code >= 9875 && code <= 9875 || code >= 9889 && code <= 9889 || code >= 9898 && code <= 9899 || code >= 9917 && code <= 9918 || code >= 9924 && code <= 9925 || code >= 9934 && code <= 9934 || code >= 9940 && code <= 9940 || code >= 9962 && code <= 9962 || code >= 9970 && code <= 9971 || code >= 9973 && code <= 9973 || code >= 9978 && code <= 9978 || code >= 9981 && code <= 9981 || code >= 9989 && code <= 9989 || code >= 9994 && code <= 9995 || code >= 10024 && code <= 10024 || code >= 10060 && code <= 10060 || code >= 10062 && code <= 10062 || code >= 10067 && code <= 10069 || code >= 10071 && code <= 10071 || code >= 10133 && code <= 10135 || code >= 10160 && code <= 10160 || code >= 10175 && code <= 10175 || code >= 11035 && code <= 11036 || code >= 11088 && code <= 11088 || code >= 11093 && code <= 11093 || code >= 11904 && code <= 42191 && code !== 12351 || code >= 43360 && code <= 43388 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65049 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 126976 && code <= 129535 ? 2 : 1;
|
|
490
|
-
}
|
|
491
|
-
return width;
|
|
975
|
+
function isContainedIn(targetPath, basePath) {
|
|
976
|
+
const normalizedBase = normalize(resolve(basePath));
|
|
977
|
+
const normalizedTarget = normalize(resolve(targetPath));
|
|
978
|
+
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
492
979
|
}
|
|
493
|
-
function
|
|
494
|
-
|
|
495
|
-
const cols = Math.max(1, columns);
|
|
496
|
-
const w = approxStringWidth(plain);
|
|
497
|
-
return Math.max(1, Math.ceil(w / cols));
|
|
980
|
+
function isValidRelativePath(path) {
|
|
981
|
+
return path.startsWith("./");
|
|
498
982
|
}
|
|
499
|
-
function
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
};
|
|
526
|
-
const clearRender = () => {
|
|
527
|
-
if (lastRenderHeight > 0) {
|
|
528
|
-
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
529
|
-
for (let i = 0; i < lastRenderHeight; i++) process.stdout.write("\x1B[2K\x1B[1B");
|
|
530
|
-
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
531
|
-
}
|
|
532
|
-
};
|
|
533
|
-
const render = (state = "active") => {
|
|
534
|
-
clearRender();
|
|
535
|
-
const lines = [];
|
|
536
|
-
const filtered = getFiltered();
|
|
537
|
-
const icon = state === "active" ? S_STEP_ACTIVE : state === "cancel" ? S_STEP_CANCEL : S_STEP_SUBMIT;
|
|
538
|
-
lines.push(`${icon} ${import_picocolors.default.bold(message)}`);
|
|
539
|
-
if (state === "active") {
|
|
540
|
-
if (lockedSection && lockedSection.items.length > 0) {
|
|
541
|
-
lines.push(`${S_BAR}`);
|
|
542
|
-
const lockedTitle = `${import_picocolors.default.bold(lockedSection.title)} ${import_picocolors.default.dim("── always included")}`;
|
|
543
|
-
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`);
|
|
544
|
-
for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_BULLET} ${import_picocolors.default.bold(item.label)}`);
|
|
545
|
-
lines.push(`${S_BAR}`);
|
|
546
|
-
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`);
|
|
547
|
-
}
|
|
548
|
-
const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
|
|
549
|
-
lines.push(searchLine);
|
|
550
|
-
lines.push(`${S_BAR} ${import_picocolors.default.dim("↑↓ move, space select, enter confirm")}`);
|
|
551
|
-
lines.push(`${S_BAR}`);
|
|
552
|
-
const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible));
|
|
553
|
-
const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible);
|
|
554
|
-
const visibleItems = filtered.slice(visibleStart, visibleEnd);
|
|
555
|
-
if (filtered.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("No matches found")}`);
|
|
556
|
-
else {
|
|
557
|
-
for (let i = 0; i < visibleItems.length; i++) {
|
|
558
|
-
const item = visibleItems[i];
|
|
559
|
-
const actualIndex = visibleStart + i;
|
|
560
|
-
const isSelected = selected.has(item.value);
|
|
561
|
-
const isCursor = actualIndex === cursor;
|
|
562
|
-
const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
|
|
563
|
-
const label = isCursor ? import_picocolors.default.underline(item.label) : item.label;
|
|
564
|
-
const hint = item.hint ? import_picocolors.default.dim(` (${item.hint})`) : "";
|
|
565
|
-
const prefix = isCursor ? import_picocolors.default.cyan("❯") : " ";
|
|
566
|
-
lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`);
|
|
567
|
-
}
|
|
568
|
-
const hiddenBefore = visibleStart;
|
|
569
|
-
const hiddenAfter = filtered.length - visibleEnd;
|
|
570
|
-
if (hiddenBefore > 0 || hiddenAfter > 0) {
|
|
571
|
-
const parts = [];
|
|
572
|
-
if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`);
|
|
573
|
-
if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`);
|
|
574
|
-
lines.push(`${S_BAR} ${import_picocolors.default.dim(parts.join(" "))}`);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
lines.push(`${S_BAR}`);
|
|
578
|
-
const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
|
|
579
|
-
if (allSelectedLabels.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("Selected: (none)")}`);
|
|
580
|
-
else {
|
|
581
|
-
const summary = allSelectedLabels.length <= 3 ? allSelectedLabels.join(", ") : `${allSelectedLabels.slice(0, 3).join(", ")} +${allSelectedLabels.length - 3} more`;
|
|
582
|
-
lines.push(`${S_BAR} ${import_picocolors.default.green("Selected:")} ${summary}`);
|
|
583
|
-
}
|
|
584
|
-
lines.push(`${import_picocolors.default.dim("└")}`);
|
|
585
|
-
} else if (state === "submit") {
|
|
586
|
-
const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
|
|
587
|
-
lines.push(`${S_BAR} ${import_picocolors.default.dim(allSelectedLabels.join(", "))}`);
|
|
588
|
-
} else if (state === "cancel") lines.push(`${S_BAR} ${import_picocolors.default.strikethrough(import_picocolors.default.dim("Cancelled"))}`);
|
|
589
|
-
process.stdout.write(lines.join("\n") + "\n");
|
|
590
|
-
lastRenderHeight = countVisualRowsForLines(lines, process.stdout.columns);
|
|
591
|
-
};
|
|
592
|
-
const cleanup = () => {
|
|
593
|
-
process.stdin.removeListener("keypress", keypressHandler);
|
|
594
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
595
|
-
rl.close();
|
|
596
|
-
};
|
|
597
|
-
const submit = () => {
|
|
598
|
-
if (required && selected.size === 0 && lockedValues.length === 0) return;
|
|
599
|
-
render("submit");
|
|
600
|
-
cleanup();
|
|
601
|
-
resolve([...lockedValues, ...Array.from(selected)]);
|
|
602
|
-
};
|
|
603
|
-
const cancel = () => {
|
|
604
|
-
render("cancel");
|
|
605
|
-
cleanup();
|
|
606
|
-
resolve(cancelSymbol);
|
|
607
|
-
};
|
|
608
|
-
const keypressHandler = (_str, key) => {
|
|
609
|
-
if (!key) return;
|
|
610
|
-
const filtered = getFiltered();
|
|
611
|
-
if (key.name === "return") {
|
|
612
|
-
submit();
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
616
|
-
cancel();
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
if (key.name === "up") {
|
|
620
|
-
cursor = Math.max(0, cursor - 1);
|
|
621
|
-
render();
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
if (key.name === "down") {
|
|
625
|
-
cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
626
|
-
render();
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
if (key.name === "space") {
|
|
630
|
-
const item = filtered[cursor];
|
|
631
|
-
if (item) if (selected.has(item.value)) selected.delete(item.value);
|
|
632
|
-
else selected.add(item.value);
|
|
633
|
-
render();
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
if (key.name === "backspace") {
|
|
637
|
-
query = query.slice(0, -1);
|
|
638
|
-
cursor = 0;
|
|
639
|
-
render();
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
643
|
-
query += key.sequence;
|
|
644
|
-
cursor = 0;
|
|
645
|
-
render();
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
};
|
|
649
|
-
process.stdin.on("keypress", keypressHandler);
|
|
650
|
-
render();
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
const DEFAULT_CLONE_TIMEOUT_MS = 3e5;
|
|
654
|
-
const CLONE_TIMEOUT_MS = (() => {
|
|
655
|
-
const raw = process.env.SKILLS_CLONE_TIMEOUT_MS;
|
|
656
|
-
if (!raw) return DEFAULT_CLONE_TIMEOUT_MS;
|
|
657
|
-
const parsed = Number.parseInt(raw, 10);
|
|
658
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
|
|
659
|
-
})();
|
|
660
|
-
var GitCloneError = class extends Error {
|
|
661
|
-
url;
|
|
662
|
-
isTimeout;
|
|
663
|
-
isAuthError;
|
|
664
|
-
constructor(message, url, isTimeout = false, isAuthError = false) {
|
|
665
|
-
super(message);
|
|
666
|
-
this.name = "GitCloneError";
|
|
667
|
-
this.url = url;
|
|
668
|
-
this.isTimeout = isTimeout;
|
|
669
|
-
this.isAuthError = isAuthError;
|
|
670
|
-
}
|
|
671
|
-
};
|
|
672
|
-
async function cloneRepo(url, ref) {
|
|
673
|
-
const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
|
|
674
|
-
const git = esm_default({
|
|
675
|
-
timeout: { block: CLONE_TIMEOUT_MS },
|
|
676
|
-
config: [
|
|
677
|
-
"filter.lfs.required=false",
|
|
678
|
-
"filter.lfs.smudge=",
|
|
679
|
-
"filter.lfs.clean=",
|
|
680
|
-
"filter.lfs.process="
|
|
681
|
-
]
|
|
682
|
-
}).env({
|
|
683
|
-
...process.env,
|
|
684
|
-
GIT_TERMINAL_PROMPT: "0",
|
|
685
|
-
GIT_LFS_SKIP_SMUDGE: "1"
|
|
686
|
-
});
|
|
687
|
-
const cloneOptions = ref ? [
|
|
688
|
-
"--depth",
|
|
689
|
-
"1",
|
|
690
|
-
"--branch",
|
|
691
|
-
ref
|
|
692
|
-
] : ["--depth", "1"];
|
|
693
|
-
try {
|
|
694
|
-
await git.clone(url, tempDir, cloneOptions);
|
|
695
|
-
return tempDir;
|
|
696
|
-
} catch (error) {
|
|
697
|
-
await rm(tempDir, {
|
|
698
|
-
recursive: true,
|
|
699
|
-
force: true
|
|
700
|
-
}).catch(() => {});
|
|
701
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
702
|
-
const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
|
|
703
|
-
const isAuthError = errorMessage.includes("Authentication failed") || errorMessage.includes("could not read Username") || errorMessage.includes("Permission denied") || errorMessage.includes("Repository not found");
|
|
704
|
-
if (isTimeout) throw new GitCloneError(`Clone timed out after ${Math.round(CLONE_TIMEOUT_MS / 1e3)}s. Common causes:\n - Large repository: raise the timeout with SKILLS_CLONE_TIMEOUT_MS=600000 (10m)\n - Slow network: retry, or clone manually and pass the local path to 'skills add'\n - Private repo without credentials: ensure auth is configured\n - For SSH: ssh-add -l (to check loaded keys)\n - For HTTPS: gh auth status (if using GitHub CLI)`, url, true, false);
|
|
705
|
-
if (isAuthError) throw new GitCloneError(`Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`, url, false, true);
|
|
706
|
-
throw new GitCloneError(`Failed to clone ${url}: ${errorMessage}`, url, false, false);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
async function cleanupTempDir(dir) {
|
|
710
|
-
const normalizedDir = normalize(resolve(dir));
|
|
711
|
-
const normalizedTmpDir = normalize(resolve(tmpdir()));
|
|
712
|
-
if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
|
|
713
|
-
await rm(dir, {
|
|
714
|
-
recursive: true,
|
|
715
|
-
force: true
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
function parseFrontmatter(raw) {
|
|
719
|
-
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
720
|
-
if (!match) return {
|
|
721
|
-
data: {},
|
|
722
|
-
content: raw
|
|
723
|
-
};
|
|
724
|
-
return {
|
|
725
|
-
data: parse(match[1]) ?? {},
|
|
726
|
-
content: match[2] ?? ""
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
function isContainedIn(targetPath, basePath) {
|
|
730
|
-
const normalizedBase = normalize(resolve(basePath));
|
|
731
|
-
const normalizedTarget = normalize(resolve(targetPath));
|
|
732
|
-
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
733
|
-
}
|
|
734
|
-
function isValidRelativePath(path) {
|
|
735
|
-
return path.startsWith("./");
|
|
736
|
-
}
|
|
737
|
-
async function getPluginSkillPaths(basePath) {
|
|
738
|
-
const searchDirs = [];
|
|
739
|
-
const addPluginSkillPaths = (pluginBase, skills) => {
|
|
740
|
-
if (!isContainedIn(pluginBase, basePath)) return;
|
|
741
|
-
if (skills && skills.length > 0) for (const skillPath of skills) {
|
|
742
|
-
if (!isValidRelativePath(skillPath)) continue;
|
|
743
|
-
const skillDir = dirname(join(pluginBase, skillPath));
|
|
744
|
-
if (isContainedIn(skillDir, basePath)) searchDirs.push(skillDir);
|
|
745
|
-
}
|
|
746
|
-
searchDirs.push(join(pluginBase, "skills"));
|
|
747
|
-
};
|
|
748
|
-
try {
|
|
749
|
-
const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8");
|
|
750
|
-
const manifest = JSON.parse(content);
|
|
751
|
-
const pluginRoot = manifest.metadata?.pluginRoot;
|
|
752
|
-
if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) {
|
|
753
|
-
if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
|
|
754
|
-
if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
|
|
755
|
-
addPluginSkillPaths(join(basePath, pluginRoot ?? "", plugin.source ?? ""), plugin.skills);
|
|
756
|
-
}
|
|
757
|
-
} catch {}
|
|
758
|
-
try {
|
|
759
|
-
const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8");
|
|
760
|
-
addPluginSkillPaths(basePath, JSON.parse(content).skills);
|
|
761
|
-
} catch {}
|
|
762
|
-
return searchDirs;
|
|
983
|
+
async function getPluginSkillPaths(basePath) {
|
|
984
|
+
const searchDirs = [];
|
|
985
|
+
const addPluginSkillPaths = (pluginBase, skills) => {
|
|
986
|
+
if (!isContainedIn(pluginBase, basePath)) return;
|
|
987
|
+
if (skills && skills.length > 0) for (const skillPath of skills) {
|
|
988
|
+
if (!isValidRelativePath(skillPath)) continue;
|
|
989
|
+
const skillDir = dirname(join(pluginBase, skillPath));
|
|
990
|
+
if (isContainedIn(skillDir, basePath)) searchDirs.push(skillDir);
|
|
991
|
+
}
|
|
992
|
+
searchDirs.push(join(pluginBase, "skills"));
|
|
993
|
+
};
|
|
994
|
+
try {
|
|
995
|
+
const content = await readFile(join(basePath, ".claude-plugin/marketplace.json"), "utf-8");
|
|
996
|
+
const manifest = JSON.parse(content);
|
|
997
|
+
const pluginRoot = manifest.metadata?.pluginRoot;
|
|
998
|
+
if (pluginRoot === void 0 || isValidRelativePath(pluginRoot)) for (const plugin of manifest.plugins ?? []) {
|
|
999
|
+
if (typeof plugin.source !== "string" && plugin.source !== void 0) continue;
|
|
1000
|
+
if (plugin.source !== void 0 && !isValidRelativePath(plugin.source)) continue;
|
|
1001
|
+
addPluginSkillPaths(join(basePath, pluginRoot ?? "", plugin.source ?? ""), plugin.skills);
|
|
1002
|
+
}
|
|
1003
|
+
} catch {}
|
|
1004
|
+
try {
|
|
1005
|
+
const content = await readFile(join(basePath, ".claude-plugin/plugin.json"), "utf-8");
|
|
1006
|
+
addPluginSkillPaths(basePath, JSON.parse(content).skills);
|
|
1007
|
+
} catch {}
|
|
1008
|
+
return searchDirs;
|
|
763
1009
|
}
|
|
764
1010
|
async function getPluginGroupings(basePath) {
|
|
765
1011
|
const groupings = /* @__PURE__ */ new Map();
|
|
@@ -933,52 +1179,6 @@ function filterSkills(skills, inputNames) {
|
|
|
933
1179
|
return normalizedInputs.some((input) => input === name || input === displayName);
|
|
934
1180
|
});
|
|
935
1181
|
}
|
|
936
|
-
const home = homedir();
|
|
937
|
-
const codexHome = process.env.CODEX_HOME?.trim() || join(home, ".codex");
|
|
938
|
-
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude");
|
|
939
|
-
const agents = {
|
|
940
|
-
"claude-code": {
|
|
941
|
-
name: "claude-code",
|
|
942
|
-
displayName: "Claude Code",
|
|
943
|
-
skillsDir: ".claude/skills",
|
|
944
|
-
globalSkillsDir: join(claudeHome, "skills"),
|
|
945
|
-
detectInstalled: async () => {
|
|
946
|
-
return existsSync(claudeHome);
|
|
947
|
-
}
|
|
948
|
-
},
|
|
949
|
-
codex: {
|
|
950
|
-
name: "codex",
|
|
951
|
-
displayName: "Codex",
|
|
952
|
-
skillsDir: ".agents/skills",
|
|
953
|
-
globalSkillsDir: join(codexHome, "skills"),
|
|
954
|
-
detectInstalled: async () => {
|
|
955
|
-
return existsSync(codexHome) || existsSync("/etc/codex");
|
|
956
|
-
}
|
|
957
|
-
},
|
|
958
|
-
cursor: {
|
|
959
|
-
name: "cursor",
|
|
960
|
-
displayName: "Cursor",
|
|
961
|
-
skillsDir: ".agents/skills",
|
|
962
|
-
globalSkillsDir: join(home, ".cursor/skills"),
|
|
963
|
-
detectInstalled: async () => {
|
|
964
|
-
return existsSync(join(home, ".cursor"));
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
};
|
|
968
|
-
async function detectInstalledAgents() {
|
|
969
|
-
return (await Promise.all(Object.entries(agents).map(async ([type, config]) => ({
|
|
970
|
-
type,
|
|
971
|
-
installed: await config.detectInstalled()
|
|
972
|
-
})))).filter((r) => r.installed).map((r) => r.type);
|
|
973
|
-
}
|
|
974
|
-
function getUniversalAgents() {
|
|
975
|
-
return Object.entries(agents).filter(([_, config]) => config.skillsDir === ".agents/skills" && config.showInUniversalList !== false).map(([type]) => type);
|
|
976
|
-
}
|
|
977
|
-
function isUniversalAgent(type) {
|
|
978
|
-
return agents[type].skillsDir === ".agents/skills";
|
|
979
|
-
}
|
|
980
|
-
const AGENTS_DIR$2 = ".agents";
|
|
981
|
-
const SKILLS_SUBDIR = "skills";
|
|
982
1182
|
function sanitizeName(name) {
|
|
983
1183
|
return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill";
|
|
984
1184
|
}
|
|
@@ -1021,6 +1221,17 @@ async function cleanAndCreateDirectory(path) {
|
|
|
1021
1221
|
} catch {}
|
|
1022
1222
|
await mkdir(path, { recursive: true });
|
|
1023
1223
|
}
|
|
1224
|
+
async function cleanCanonicalDir(canonicalPath, storeRoot) {
|
|
1225
|
+
const rel = relative(resolve(storeRoot), resolve(canonicalPath));
|
|
1226
|
+
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error(`refusing to clean path outside forge store: ${canonicalPath}`);
|
|
1227
|
+
try {
|
|
1228
|
+
await rm(canonicalPath, {
|
|
1229
|
+
recursive: true,
|
|
1230
|
+
force: true
|
|
1231
|
+
});
|
|
1232
|
+
} catch {}
|
|
1233
|
+
await mkdir(canonicalPath, { recursive: true });
|
|
1234
|
+
}
|
|
1024
1235
|
async function resolveParentSymlinks(path) {
|
|
1025
1236
|
const resolved = resolve(path);
|
|
1026
1237
|
const dir = dirname(resolved);
|
|
@@ -1056,7 +1267,104 @@ async function createSymlink(target, linkPath) {
|
|
|
1056
1267
|
return false;
|
|
1057
1268
|
}
|
|
1058
1269
|
}
|
|
1059
|
-
async function installSkillForAgent(skill, agentType,
|
|
1270
|
+
async function installSkillForAgent(skill, agentType, opts) {
|
|
1271
|
+
if (!("ref" in opts && "scopeNamespace" in opts)) return _installSkillForAgentLegacy(skill, agentType, opts);
|
|
1272
|
+
const newOpts = opts;
|
|
1273
|
+
const scope = newOpts.scope ?? (newOpts.global ? "global" : "project");
|
|
1274
|
+
const cwd = newOpts.cwd ?? process.cwd();
|
|
1275
|
+
const skillName = sanitizeName(skill.name || basename(skill.path));
|
|
1276
|
+
const canonicalKey = `${newOpts.scopeNamespace}__${skillName}`;
|
|
1277
|
+
const storeRoot = getStoreRoot(scope, cwd);
|
|
1278
|
+
const canonicalDir = join(storeRoot, "skills", canonicalKey);
|
|
1279
|
+
if (!isPathSafe(join(storeRoot, "skills"), canonicalDir)) return {
|
|
1280
|
+
success: false,
|
|
1281
|
+
path: "",
|
|
1282
|
+
mode: "symlink",
|
|
1283
|
+
error: "Invalid skill name: path traversal detected",
|
|
1284
|
+
forgeOtherReplacements: []
|
|
1285
|
+
};
|
|
1286
|
+
const entryAgentRoot = scope === "global" ? getAgentHome(agentType) : cwd;
|
|
1287
|
+
const entry = agentType === "claude-code" ? join(entryAgentRoot, ".claude", "skills", skillName) : agentType === "cursor" ? join(entryAgentRoot, ".cursor", "skills", skillName) : join(entryAgentRoot, ".agents", "skills", skillName);
|
|
1288
|
+
try {
|
|
1289
|
+
const status = await assertEntryReplaceable(entry, {
|
|
1290
|
+
intendedRef: newOpts.ref,
|
|
1291
|
+
intendedCanonical: canonicalDir,
|
|
1292
|
+
storeRoot
|
|
1293
|
+
});
|
|
1294
|
+
const forgeOtherReplacements = [];
|
|
1295
|
+
const foreignConflicts = [];
|
|
1296
|
+
if (status.kind === "forge-other") forgeOtherReplacements.push({
|
|
1297
|
+
entryPath: entry,
|
|
1298
|
+
otherRef: status.otherRef
|
|
1299
|
+
});
|
|
1300
|
+
else if (status.kind === "foreign-file" || status.kind === "foreign-dir" || status.kind === "foreign-symlink") foreignConflicts.push({
|
|
1301
|
+
entryPath: entry,
|
|
1302
|
+
kind: status.kind
|
|
1303
|
+
});
|
|
1304
|
+
if (foreignConflicts.length > 0) {
|
|
1305
|
+
let proceed = newOpts.forceOverwrite ?? false;
|
|
1306
|
+
if (!proceed && newOpts.onConflictPrompt) proceed = await newOpts.onConflictPrompt(foreignConflicts);
|
|
1307
|
+
if (!proceed) return {
|
|
1308
|
+
success: false,
|
|
1309
|
+
path: entry,
|
|
1310
|
+
mode: "symlink",
|
|
1311
|
+
error: "aborted: user-owned content at entry path",
|
|
1312
|
+
forgeOtherReplacements: []
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
await cleanCanonicalDir(canonicalDir, storeRoot);
|
|
1316
|
+
await copyDirectory(skill.path, canonicalDir);
|
|
1317
|
+
await writeFile(join(canonicalDir, ".forge-source.json"), JSON.stringify({
|
|
1318
|
+
ref: newOpts.ref,
|
|
1319
|
+
scope: newOpts.scopeNamespace,
|
|
1320
|
+
type: "skill",
|
|
1321
|
+
name: skillName,
|
|
1322
|
+
install_mode: agentType,
|
|
1323
|
+
parent_plugin_ref: null,
|
|
1324
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1325
|
+
}, null, 2) + "\n");
|
|
1326
|
+
if (status.kind !== "absent" && status.kind !== "forge-self") try {
|
|
1327
|
+
await rm(entry, {
|
|
1328
|
+
recursive: true,
|
|
1329
|
+
force: true
|
|
1330
|
+
});
|
|
1331
|
+
} catch {}
|
|
1332
|
+
if (!await createSymlink(canonicalDir, entry)) {
|
|
1333
|
+
await mkdir(dirname(entry), { recursive: true });
|
|
1334
|
+
try {
|
|
1335
|
+
await rm(entry, {
|
|
1336
|
+
recursive: true,
|
|
1337
|
+
force: true
|
|
1338
|
+
});
|
|
1339
|
+
} catch {}
|
|
1340
|
+
await copyDirectory(canonicalDir, entry);
|
|
1341
|
+
return {
|
|
1342
|
+
success: true,
|
|
1343
|
+
path: entry,
|
|
1344
|
+
mode: "symlink",
|
|
1345
|
+
symlinkFailed: true,
|
|
1346
|
+
canonicalPath: canonicalDir,
|
|
1347
|
+
forgeOtherReplacements
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
return {
|
|
1351
|
+
success: true,
|
|
1352
|
+
path: entry,
|
|
1353
|
+
mode: "symlink",
|
|
1354
|
+
canonicalPath: canonicalDir,
|
|
1355
|
+
forgeOtherReplacements
|
|
1356
|
+
};
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
return {
|
|
1359
|
+
success: false,
|
|
1360
|
+
path: entry,
|
|
1361
|
+
mode: "symlink",
|
|
1362
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1363
|
+
forgeOtherReplacements: []
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
async function _installSkillForAgentLegacy(skill, agentType, options) {
|
|
1060
1368
|
const agent = agents[agentType];
|
|
1061
1369
|
const isGlobal = options.global ?? false;
|
|
1062
1370
|
const cwd = options.cwd || process.cwd();
|
|
@@ -1064,7 +1372,8 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1064
1372
|
success: false,
|
|
1065
1373
|
path: "",
|
|
1066
1374
|
mode: options.mode ?? "symlink",
|
|
1067
|
-
error: `${agent.displayName} does not support global skill installation
|
|
1375
|
+
error: `${agent.displayName} does not support global skill installation`,
|
|
1376
|
+
forgeOtherReplacements: []
|
|
1068
1377
|
};
|
|
1069
1378
|
const skillName = sanitizeName(skill.name || basename(skill.path));
|
|
1070
1379
|
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
@@ -1076,13 +1385,15 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1076
1385
|
success: false,
|
|
1077
1386
|
path: agentDir,
|
|
1078
1387
|
mode: installMode,
|
|
1079
|
-
error: "Invalid skill name: potential path traversal detected"
|
|
1388
|
+
error: "Invalid skill name: potential path traversal detected",
|
|
1389
|
+
forgeOtherReplacements: []
|
|
1080
1390
|
};
|
|
1081
1391
|
if (!isPathSafe(agentBase, agentDir)) return {
|
|
1082
1392
|
success: false,
|
|
1083
1393
|
path: agentDir,
|
|
1084
1394
|
mode: installMode,
|
|
1085
|
-
error: "Invalid skill name: potential path traversal detected"
|
|
1395
|
+
error: "Invalid skill name: potential path traversal detected",
|
|
1396
|
+
forgeOtherReplacements: []
|
|
1086
1397
|
};
|
|
1087
1398
|
try {
|
|
1088
1399
|
if (installMode === "copy") {
|
|
@@ -1091,7 +1402,8 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1091
1402
|
return {
|
|
1092
1403
|
success: true,
|
|
1093
1404
|
path: agentDir,
|
|
1094
|
-
mode: "copy"
|
|
1405
|
+
mode: "copy",
|
|
1406
|
+
forgeOtherReplacements: []
|
|
1095
1407
|
};
|
|
1096
1408
|
}
|
|
1097
1409
|
await cleanAndCreateDirectory(canonicalDir);
|
|
@@ -1100,7 +1412,8 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1100
1412
|
success: true,
|
|
1101
1413
|
path: canonicalDir,
|
|
1102
1414
|
canonicalPath: canonicalDir,
|
|
1103
|
-
mode: "symlink"
|
|
1415
|
+
mode: "symlink",
|
|
1416
|
+
forgeOtherReplacements: []
|
|
1104
1417
|
};
|
|
1105
1418
|
if (!await createSymlink(canonicalDir, agentDir)) {
|
|
1106
1419
|
await cleanAndCreateDirectory(agentDir);
|
|
@@ -1110,21 +1423,24 @@ async function installSkillForAgent(skill, agentType, options = {}) {
|
|
|
1110
1423
|
path: agentDir,
|
|
1111
1424
|
canonicalPath: canonicalDir,
|
|
1112
1425
|
mode: "symlink",
|
|
1113
|
-
symlinkFailed: true
|
|
1426
|
+
symlinkFailed: true,
|
|
1427
|
+
forgeOtherReplacements: []
|
|
1114
1428
|
};
|
|
1115
1429
|
}
|
|
1116
1430
|
return {
|
|
1117
1431
|
success: true,
|
|
1118
1432
|
path: agentDir,
|
|
1119
1433
|
canonicalPath: canonicalDir,
|
|
1120
|
-
mode: "symlink"
|
|
1434
|
+
mode: "symlink",
|
|
1435
|
+
forgeOtherReplacements: []
|
|
1121
1436
|
};
|
|
1122
1437
|
} catch (error) {
|
|
1123
1438
|
return {
|
|
1124
1439
|
success: false,
|
|
1125
1440
|
path: agentDir,
|
|
1126
1441
|
mode: installMode,
|
|
1127
|
-
error: error instanceof Error ? error.message : "Unknown error"
|
|
1442
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
1443
|
+
forgeOtherReplacements: []
|
|
1128
1444
|
};
|
|
1129
1445
|
}
|
|
1130
1446
|
}
|
|
@@ -1472,6 +1788,686 @@ async function listInstalledSkills(options = {}) {
|
|
|
1472
1788
|
} catch {}
|
|
1473
1789
|
return Array.from(skillsMap.values());
|
|
1474
1790
|
}
|
|
1791
|
+
const USER_START = "<!-- forge:user-content:start -->";
|
|
1792
|
+
const USER_END = "<!-- forge:user-content:end -->";
|
|
1793
|
+
function takeoverWithUserContent(existing) {
|
|
1794
|
+
if (existing.includes(USER_START)) return existing;
|
|
1795
|
+
return `${USER_START}\n${existing.trim()}\n${USER_END}\n`;
|
|
1796
|
+
}
|
|
1797
|
+
function injectPluginRules(agentsMd, plugin, rules) {
|
|
1798
|
+
const start = `<!-- forge-plugin:${plugin.key}:start ref=${plugin.ref} install_mode=codex -->`;
|
|
1799
|
+
const end = `<!-- forge-plugin:${plugin.key}:end -->`;
|
|
1800
|
+
const block = `${start}\n${rules.map((r) => {
|
|
1801
|
+
const rs = `<!-- forge-rule:${r.relativePath}:start -->`;
|
|
1802
|
+
const re = `<!-- forge-rule:${r.relativePath}:end -->`;
|
|
1803
|
+
return ` ${rs}\n${r.content.trimEnd()}\n ${re}`;
|
|
1804
|
+
}).join("\n")}\n${end}\n`;
|
|
1805
|
+
const existing = findPluginBlock(agentsMd, plugin.key);
|
|
1806
|
+
if (existing) return agentsMd.slice(0, existing.start) + block + agentsMd.slice(existing.end);
|
|
1807
|
+
return agentsMd + (agentsMd.endsWith("\n") ? "" : "\n") + "\n" + block;
|
|
1808
|
+
}
|
|
1809
|
+
function removePluginRules(agentsMd, pluginKey) {
|
|
1810
|
+
const existing = findPluginBlock(agentsMd, pluginKey);
|
|
1811
|
+
if (!existing) return agentsMd;
|
|
1812
|
+
return agentsMd.slice(0, existing.start) + agentsMd.slice(existing.end);
|
|
1813
|
+
}
|
|
1814
|
+
function findPluginBlock(agentsMd, pluginKey) {
|
|
1815
|
+
const startMarker = `<!-- forge-plugin:${pluginKey}:start`;
|
|
1816
|
+
const endMarker = `<!-- forge-plugin:${pluginKey}:end -->`;
|
|
1817
|
+
const startIdx = agentsMd.indexOf(startMarker);
|
|
1818
|
+
if (startIdx < 0) return null;
|
|
1819
|
+
const endIdx = agentsMd.indexOf(endMarker, startIdx);
|
|
1820
|
+
if (endIdx < 0) return null;
|
|
1821
|
+
let blockEnd = endIdx + endMarker.length;
|
|
1822
|
+
if (agentsMd[blockEnd] === "\n") blockEnd++;
|
|
1823
|
+
return {
|
|
1824
|
+
start: startIdx,
|
|
1825
|
+
end: blockEnd
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
async function installPlugin(ref, opts) {
|
|
1829
|
+
const cfg = loadConfig();
|
|
1830
|
+
const tarball = await fetchPluginArchive(ref, cfg.portal, cfg.token);
|
|
1831
|
+
const tmp = await mkdtemp(join(tmpdir(), "plugin-"));
|
|
1832
|
+
try {
|
|
1833
|
+
extractTarGz(tarball, tmp);
|
|
1834
|
+
return await installFromExtracted(ref, await findPluginRoot(tmp), opts);
|
|
1835
|
+
} finally {
|
|
1836
|
+
await rm(tmp, {
|
|
1837
|
+
recursive: true,
|
|
1838
|
+
force: true
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
async function findPluginRoot(start) {
|
|
1843
|
+
const queue = [start];
|
|
1844
|
+
while (queue.length > 0) {
|
|
1845
|
+
const dir = queue.shift();
|
|
1846
|
+
if (existsSync(join(dir, "plugin.json"))) return dir;
|
|
1847
|
+
if (existsSync(join(dir, ".claude-plugin", "plugin.json"))) return dir;
|
|
1848
|
+
let entries;
|
|
1849
|
+
try {
|
|
1850
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
1851
|
+
} catch {
|
|
1852
|
+
continue;
|
|
1853
|
+
}
|
|
1854
|
+
for (const e of entries) if (e.isDirectory()) queue.push(join(dir, e.name));
|
|
1855
|
+
}
|
|
1856
|
+
throw new Error(`plugin.json not found under ${start} (corrupt archive?)`);
|
|
1857
|
+
}
|
|
1858
|
+
async function installFromExtracted(ref, extractedDir, opts) {
|
|
1859
|
+
const { scopeNamespace, pluginName } = parsePluginRef$1(ref);
|
|
1860
|
+
const storeRoot = getStoreRoot(opts.scope, opts.cwd);
|
|
1861
|
+
const pluginCanonical = join(storeRoot, "plugins", `${scopeNamespace}__${pluginName}`);
|
|
1862
|
+
const components = await classifyExtracted(extractedDir);
|
|
1863
|
+
const ignoredComponents = await collectIgnoredTopLevel(extractedDir);
|
|
1864
|
+
const preflight = buildPreflightList(pluginCanonical, components, opts);
|
|
1865
|
+
const foreignConflicts = [];
|
|
1866
|
+
const forgeOtherReplacements = [];
|
|
1867
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
1868
|
+
for (const pe of preflight) {
|
|
1869
|
+
const intendedRef = ref + "/" + componentSubRef(pe.component);
|
|
1870
|
+
const status = await assertEntryReplaceable(pe.entryPath, {
|
|
1871
|
+
intendedRef,
|
|
1872
|
+
intendedCanonical: pe.intendedCanonical,
|
|
1873
|
+
storeRoot
|
|
1874
|
+
});
|
|
1875
|
+
statuses.set(pe.entryPath, status);
|
|
1876
|
+
if (status.kind === "forge-other") forgeOtherReplacements.push({
|
|
1877
|
+
entryPath: pe.entryPath,
|
|
1878
|
+
otherRef: status.otherRef
|
|
1879
|
+
});
|
|
1880
|
+
else if (status.kind === "foreign-file" || status.kind === "foreign-dir" || status.kind === "foreign-symlink") foreignConflicts.push({
|
|
1881
|
+
entryPath: pe.entryPath,
|
|
1882
|
+
kind: status.kind
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
if (foreignConflicts.length > 0) {
|
|
1886
|
+
if (!await promptForeignOverwrite(foreignConflicts, {
|
|
1887
|
+
yes: opts.yes,
|
|
1888
|
+
force: opts.force
|
|
1889
|
+
})) throw new Error(`aborted: ${foreignConflicts.length} foreign conflicts at plugin entry paths`);
|
|
1890
|
+
}
|
|
1891
|
+
return {
|
|
1892
|
+
pluginRef: ref,
|
|
1893
|
+
pluginCanonical,
|
|
1894
|
+
installedComponents: await commitPluginInstall({
|
|
1895
|
+
ref,
|
|
1896
|
+
scopeNamespace,
|
|
1897
|
+
pluginName,
|
|
1898
|
+
pluginCanonical,
|
|
1899
|
+
extractedDir,
|
|
1900
|
+
components,
|
|
1901
|
+
preflight,
|
|
1902
|
+
statuses,
|
|
1903
|
+
opts
|
|
1904
|
+
}),
|
|
1905
|
+
installedEntries: preflight.map((pe) => ({
|
|
1906
|
+
agent: pe.agent,
|
|
1907
|
+
kind: componentLedgerKind(pe.component.kind),
|
|
1908
|
+
name: componentDisplayName(pe.component),
|
|
1909
|
+
entryPath: pe.entryPath
|
|
1910
|
+
})),
|
|
1911
|
+
forgeOtherReplacements,
|
|
1912
|
+
ignoredComponents
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
function componentLedgerKind(kind) {
|
|
1916
|
+
switch (kind) {
|
|
1917
|
+
case "plugin-skill": return "skill";
|
|
1918
|
+
case "plugin-agent-manifest": return "agent-manifest";
|
|
1919
|
+
case "plugin-agent-deps-dir": return "agent-deps-dir";
|
|
1920
|
+
case "plugin-rule-manifest": return "rule-manifest";
|
|
1921
|
+
case "plugin-rule-deps-dir": return "rule-deps-dir";
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
function componentDisplayName(c) {
|
|
1925
|
+
switch (c.kind) {
|
|
1926
|
+
case "plugin-skill": return c.skillName;
|
|
1927
|
+
case "plugin-agent-manifest":
|
|
1928
|
+
case "plugin-rule-manifest": return c.filename;
|
|
1929
|
+
case "plugin-agent-deps-dir":
|
|
1930
|
+
case "plugin-rule-deps-dir": return c.dirname;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
function buildPreflightList(pluginCanonical, components, opts) {
|
|
1934
|
+
const preflight = [];
|
|
1935
|
+
for (const agent of opts.agents) {
|
|
1936
|
+
for (const skillName of components.skills) {
|
|
1937
|
+
const c = {
|
|
1938
|
+
kind: "plugin-skill",
|
|
1939
|
+
pluginCanonical,
|
|
1940
|
+
skillName
|
|
1941
|
+
};
|
|
1942
|
+
const t = getInstallTargets(agent, c, opts.scope, opts.cwd);
|
|
1943
|
+
preflight.push({
|
|
1944
|
+
agent,
|
|
1945
|
+
component: c,
|
|
1946
|
+
entryPath: t.entry,
|
|
1947
|
+
intendedCanonical: t.canonical
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
for (const filename of components.agentManifests) {
|
|
1951
|
+
const c = {
|
|
1952
|
+
kind: "plugin-agent-manifest",
|
|
1953
|
+
pluginCanonical,
|
|
1954
|
+
filename
|
|
1955
|
+
};
|
|
1956
|
+
const t = getInstallTargets(agent, c, opts.scope, opts.cwd);
|
|
1957
|
+
preflight.push({
|
|
1958
|
+
agent,
|
|
1959
|
+
component: c,
|
|
1960
|
+
entryPath: t.entry,
|
|
1961
|
+
intendedCanonical: t.canonical
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
for (const dirname of components.agentDepsDirs) {
|
|
1965
|
+
const c = {
|
|
1966
|
+
kind: "plugin-agent-deps-dir",
|
|
1967
|
+
pluginCanonical,
|
|
1968
|
+
dirname
|
|
1969
|
+
};
|
|
1970
|
+
const t = getInstallTargets(agent, c, opts.scope, opts.cwd);
|
|
1971
|
+
preflight.push({
|
|
1972
|
+
agent,
|
|
1973
|
+
component: c,
|
|
1974
|
+
entryPath: t.entry,
|
|
1975
|
+
intendedCanonical: t.canonical
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
for (const filename of components.ruleManifests) {
|
|
1979
|
+
const c = {
|
|
1980
|
+
kind: "plugin-rule-manifest",
|
|
1981
|
+
pluginCanonical,
|
|
1982
|
+
filename
|
|
1983
|
+
};
|
|
1984
|
+
const t = getInstallTargets(agent, c, opts.scope, opts.cwd);
|
|
1985
|
+
if (t.entry) preflight.push({
|
|
1986
|
+
agent,
|
|
1987
|
+
component: c,
|
|
1988
|
+
entryPath: t.entry,
|
|
1989
|
+
intendedCanonical: t.canonical
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
for (const dirname of components.ruleDepsDirs) {
|
|
1993
|
+
const c = {
|
|
1994
|
+
kind: "plugin-rule-deps-dir",
|
|
1995
|
+
pluginCanonical,
|
|
1996
|
+
dirname
|
|
1997
|
+
};
|
|
1998
|
+
const t = getInstallTargets(agent, c, opts.scope, opts.cwd);
|
|
1999
|
+
if (t.entry) preflight.push({
|
|
2000
|
+
agent,
|
|
2001
|
+
component: c,
|
|
2002
|
+
entryPath: t.entry,
|
|
2003
|
+
intendedCanonical: t.canonical
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
return preflight;
|
|
2008
|
+
}
|
|
2009
|
+
async function classifyExtracted(dir) {
|
|
2010
|
+
const out = {
|
|
2011
|
+
skills: [],
|
|
2012
|
+
agentManifests: [],
|
|
2013
|
+
agentDepsDirs: [],
|
|
2014
|
+
ruleManifests: [],
|
|
2015
|
+
ruleDepsDirs: []
|
|
2016
|
+
};
|
|
2017
|
+
if (existsSync(join(dir, "skills"))) {
|
|
2018
|
+
for (const e of await readdir(join(dir, "skills"), { withFileTypes: true })) if (e.isDirectory()) out.skills.push(e.name);
|
|
2019
|
+
}
|
|
2020
|
+
if (existsSync(join(dir, "agents"))) {
|
|
2021
|
+
for (const e of await readdir(join(dir, "agents"), { withFileTypes: true })) if (e.isFile() && e.name.endsWith(".md")) out.agentManifests.push(e.name);
|
|
2022
|
+
else if (e.isDirectory()) out.agentDepsDirs.push(e.name);
|
|
2023
|
+
}
|
|
2024
|
+
if (existsSync(join(dir, "rules"))) {
|
|
2025
|
+
for (const e of await readdir(join(dir, "rules"), { withFileTypes: true })) if (e.isFile() && (e.name.endsWith(".md") || e.name.endsWith(".mdc"))) out.ruleManifests.push(e.name);
|
|
2026
|
+
else if (e.isDirectory()) out.ruleDepsDirs.push(e.name);
|
|
2027
|
+
}
|
|
2028
|
+
return out;
|
|
2029
|
+
}
|
|
2030
|
+
async function collectIgnoredTopLevel(dir) {
|
|
2031
|
+
const accepted = new Set([
|
|
2032
|
+
"skills",
|
|
2033
|
+
"agents",
|
|
2034
|
+
"rules",
|
|
2035
|
+
"plugin.json",
|
|
2036
|
+
".claude-plugin"
|
|
2037
|
+
]);
|
|
2038
|
+
const out = [];
|
|
2039
|
+
for (const e of await readdir(dir, { withFileTypes: true })) if (!accepted.has(e.name)) out.push(e.isDirectory() ? `${e.name}/` : e.name);
|
|
2040
|
+
out.sort();
|
|
2041
|
+
return out;
|
|
2042
|
+
}
|
|
2043
|
+
function parsePluginRef$1(ref) {
|
|
2044
|
+
let m = /^@public\/plugins\/(.+)$/.exec(ref);
|
|
2045
|
+
if (m) return {
|
|
2046
|
+
scopeNamespace: "public",
|
|
2047
|
+
pluginName: m[1]
|
|
2048
|
+
};
|
|
2049
|
+
m = /^@teams\/([^/]+)\/plugins\/(.+)$/.exec(ref);
|
|
2050
|
+
if (m) return {
|
|
2051
|
+
scopeNamespace: m[1],
|
|
2052
|
+
pluginName: m[2]
|
|
2053
|
+
};
|
|
2054
|
+
throw new Error(`invalid plugin ref: ${ref}`);
|
|
2055
|
+
}
|
|
2056
|
+
function componentSubRef(c) {
|
|
2057
|
+
switch (c.kind) {
|
|
2058
|
+
case "plugin-skill": return `skills/${c.skillName}`;
|
|
2059
|
+
case "plugin-agent-manifest": return `agents/${c.filename}`;
|
|
2060
|
+
case "plugin-agent-deps-dir": return `agents/${c.dirname}`;
|
|
2061
|
+
case "plugin-rule-manifest": return `rules/${c.filename}`;
|
|
2062
|
+
case "plugin-rule-deps-dir": return `rules/${c.dirname}`;
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
async function commitPluginInstall(params) {
|
|
2066
|
+
const { ref, scopeNamespace, pluginName, pluginCanonical, extractedDir, components, preflight, statuses, opts } = params;
|
|
2067
|
+
await rm(pluginCanonical, {
|
|
2068
|
+
recursive: true,
|
|
2069
|
+
force: true
|
|
2070
|
+
});
|
|
2071
|
+
await mkdir(pluginCanonical, { recursive: true });
|
|
2072
|
+
await copyDirRecursive(extractedDir, pluginCanonical, true);
|
|
2073
|
+
for (const skillName of components.skills) {
|
|
2074
|
+
const sidecarPath = join(pluginCanonical, "skills", skillName, ".forge-source.json");
|
|
2075
|
+
await mkdir(dirname(sidecarPath), { recursive: true });
|
|
2076
|
+
await writeFile(sidecarPath, JSON.stringify({
|
|
2077
|
+
ref: `${ref}/skills/${skillName}`,
|
|
2078
|
+
scope: scopeNamespace,
|
|
2079
|
+
type: "skill",
|
|
2080
|
+
name: skillName,
|
|
2081
|
+
install_mode: opts.agents[0],
|
|
2082
|
+
parent_plugin_ref: ref,
|
|
2083
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2084
|
+
}, null, 2) + "\n");
|
|
2085
|
+
}
|
|
2086
|
+
const ledger = [
|
|
2087
|
+
...components.skills.map((n) => ({
|
|
2088
|
+
type: "skill",
|
|
2089
|
+
name: n
|
|
2090
|
+
})),
|
|
2091
|
+
...components.agentManifests.map((n) => ({
|
|
2092
|
+
type: "agent-manifest",
|
|
2093
|
+
name: n
|
|
2094
|
+
})),
|
|
2095
|
+
...components.agentDepsDirs.map((n) => ({
|
|
2096
|
+
type: "agent-deps-dir",
|
|
2097
|
+
name: n
|
|
2098
|
+
})),
|
|
2099
|
+
...components.ruleManifests.map((n) => ({
|
|
2100
|
+
type: "rule-manifest",
|
|
2101
|
+
name: n
|
|
2102
|
+
})),
|
|
2103
|
+
...components.ruleDepsDirs.map((n) => ({
|
|
2104
|
+
type: "rule-deps-dir",
|
|
2105
|
+
name: n
|
|
2106
|
+
}))
|
|
2107
|
+
];
|
|
2108
|
+
await writeFile(join(pluginCanonical, ".forge-plugin.json"), JSON.stringify({
|
|
2109
|
+
ref,
|
|
2110
|
+
scope: scopeNamespace,
|
|
2111
|
+
name: pluginName,
|
|
2112
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2113
|
+
install_modes: opts.agents,
|
|
2114
|
+
installed_components: ledger
|
|
2115
|
+
}, null, 2) + "\n");
|
|
2116
|
+
for (const pe of preflight) {
|
|
2117
|
+
const status = statuses.get(pe.entryPath);
|
|
2118
|
+
if (!status) continue;
|
|
2119
|
+
if (status.kind !== "absent" && status.kind !== "forge-self") try {
|
|
2120
|
+
await rm(pe.entryPath, {
|
|
2121
|
+
recursive: true,
|
|
2122
|
+
force: true
|
|
2123
|
+
});
|
|
2124
|
+
} catch {}
|
|
2125
|
+
await mkdir(dirname(pe.entryPath), { recursive: true });
|
|
2126
|
+
if (!await createSymlink(pe.intendedCanonical, pe.entryPath)) {
|
|
2127
|
+
let stEntry;
|
|
2128
|
+
try {
|
|
2129
|
+
stEntry = await stat(pe.intendedCanonical);
|
|
2130
|
+
} catch {
|
|
2131
|
+
continue;
|
|
2132
|
+
}
|
|
2133
|
+
try {
|
|
2134
|
+
await rm(pe.entryPath, {
|
|
2135
|
+
recursive: true,
|
|
2136
|
+
force: true
|
|
2137
|
+
});
|
|
2138
|
+
} catch {}
|
|
2139
|
+
if (stEntry.isDirectory()) await copyDirRecursive(pe.intendedCanonical, pe.entryPath, false);
|
|
2140
|
+
else await copyFileShallow(pe.intendedCanonical, pe.entryPath);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
if (opts.agents.includes("codex") && components.ruleManifests.length > 0) await injectCodexAgentsMdForPlugin({
|
|
2144
|
+
pluginCanonical,
|
|
2145
|
+
pluginRef: ref,
|
|
2146
|
+
pluginKey: `${scopeNamespace}:${pluginName}`,
|
|
2147
|
+
ruleManifests: components.ruleManifests,
|
|
2148
|
+
scope: opts.scope,
|
|
2149
|
+
cwd: opts.cwd
|
|
2150
|
+
});
|
|
2151
|
+
if (opts.agents.includes("codex") && components.agentManifests.length > 0) await registerCodexAgents({
|
|
2152
|
+
pluginCanonical,
|
|
2153
|
+
agentFilenames: components.agentManifests,
|
|
2154
|
+
scope: opts.scope,
|
|
2155
|
+
cwd: opts.cwd
|
|
2156
|
+
});
|
|
2157
|
+
if (components.skills.length > 0) for (const agent of opts.agents) await ensureForgeHooks(agent);
|
|
2158
|
+
return ledger;
|
|
2159
|
+
}
|
|
2160
|
+
async function copyDirRecursive(src, dest, skipDotClaudePlugin) {
|
|
2161
|
+
await mkdir(dest, { recursive: true });
|
|
2162
|
+
for (const e of await readdir(src, { withFileTypes: true })) {
|
|
2163
|
+
if (skipDotClaudePlugin && e.name === ".claude-plugin") continue;
|
|
2164
|
+
const s = join(src, e.name);
|
|
2165
|
+
const d = join(dest, e.name);
|
|
2166
|
+
if (e.isDirectory()) await copyDirRecursive(s, d, false);
|
|
2167
|
+
else if (e.isFile()) await writeFile(d, await readFile(s));
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
async function copyFileShallow(src, dest) {
|
|
2171
|
+
const buf = await readFile(src);
|
|
2172
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
2173
|
+
await writeFile(dest, buf);
|
|
2174
|
+
}
|
|
2175
|
+
async function injectCodexAgentsMdForPlugin(params) {
|
|
2176
|
+
const codexAgentsMd = join(params.scope === "global" ? getAgentHome("codex") : join(params.cwd ?? process.cwd(), ".codex"), "AGENTS.md");
|
|
2177
|
+
const canonicalAgentsMd = join(getStoreRoot(params.scope, params.cwd), "codex-agents.md");
|
|
2178
|
+
let userContent = "";
|
|
2179
|
+
try {
|
|
2180
|
+
if ((await stat(codexAgentsMd)).isFile()) userContent = await readFile(codexAgentsMd, "utf-8");
|
|
2181
|
+
} catch {}
|
|
2182
|
+
let canonicalContent = "";
|
|
2183
|
+
try {
|
|
2184
|
+
canonicalContent = await readFile(canonicalAgentsMd, "utf-8");
|
|
2185
|
+
} catch {}
|
|
2186
|
+
if (!canonicalContent) canonicalContent = takeoverWithUserContent(userContent);
|
|
2187
|
+
const rules = [];
|
|
2188
|
+
for (const fname of params.ruleManifests) {
|
|
2189
|
+
const content = await readFile(join(params.pluginCanonical, "rules", fname), "utf-8");
|
|
2190
|
+
rules.push({
|
|
2191
|
+
relativePath: `rules/${fname}`,
|
|
2192
|
+
content
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
const updated = injectPluginRules(canonicalContent, {
|
|
2196
|
+
ref: params.pluginRef,
|
|
2197
|
+
key: params.pluginKey
|
|
2198
|
+
}, rules);
|
|
2199
|
+
await mkdir(dirname(canonicalAgentsMd), { recursive: true });
|
|
2200
|
+
await writeFile(canonicalAgentsMd, updated, "utf-8");
|
|
2201
|
+
try {
|
|
2202
|
+
await rm(codexAgentsMd, { force: true });
|
|
2203
|
+
} catch {}
|
|
2204
|
+
await mkdir(dirname(codexAgentsMd), { recursive: true });
|
|
2205
|
+
await symlink(canonicalAgentsMd, codexAgentsMd);
|
|
2206
|
+
}
|
|
2207
|
+
async function registerCodexAgents(params) {
|
|
2208
|
+
const configToml = join(params.scope === "global" ? getAgentHome("codex") : join(params.cwd ?? process.cwd(), ".codex"), "config.toml");
|
|
2209
|
+
let raw = "";
|
|
2210
|
+
try {
|
|
2211
|
+
raw = await readFile(configToml, "utf-8");
|
|
2212
|
+
} catch {}
|
|
2213
|
+
for (const fname of params.agentFilenames) {
|
|
2214
|
+
const id = fname.replace(/\.md$/, "");
|
|
2215
|
+
const configFile = join(params.pluginCanonical, "agents", fname);
|
|
2216
|
+
raw = upsertCodexAgent(raw, {
|
|
2217
|
+
id,
|
|
2218
|
+
configFile
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
await mkdir(dirname(configToml), { recursive: true });
|
|
2222
|
+
await writeFile(configToml, raw, "utf-8");
|
|
2223
|
+
}
|
|
2224
|
+
const silentOutput = new Writable({ write(_chunk, _encoding, callback) {
|
|
2225
|
+
callback();
|
|
2226
|
+
} });
|
|
2227
|
+
const S_STEP_ACTIVE = import_picocolors.default.green("◆");
|
|
2228
|
+
const S_STEP_CANCEL = import_picocolors.default.red("■");
|
|
2229
|
+
const S_STEP_SUBMIT = import_picocolors.default.green("◇");
|
|
2230
|
+
const S_RADIO_ACTIVE = import_picocolors.default.green("●");
|
|
2231
|
+
const S_RADIO_INACTIVE = import_picocolors.default.dim("○");
|
|
2232
|
+
import_picocolors.default.green("✓");
|
|
2233
|
+
const S_BULLET = import_picocolors.default.green("•");
|
|
2234
|
+
const S_BAR = import_picocolors.default.dim("│");
|
|
2235
|
+
const S_BAR_H = import_picocolors.default.dim("─");
|
|
2236
|
+
const cancelSymbol = Symbol("cancel");
|
|
2237
|
+
function approxStringWidth(plain) {
|
|
2238
|
+
let width = 0;
|
|
2239
|
+
for (const ch of plain) {
|
|
2240
|
+
const code = ch.codePointAt(0);
|
|
2241
|
+
if (code === 0) continue;
|
|
2242
|
+
width += code >= 4352 && code <= 4447 || code >= 8986 && code <= 8987 || code >= 9001 && code <= 9002 || code >= 9193 && code <= 9196 || code === 9200 || code === 9203 || code >= 9725 && code <= 9726 || code >= 9748 && code <= 9749 || code >= 9800 && code <= 9811 || code >= 9855 && code <= 9855 || code >= 9875 && code <= 9875 || code >= 9889 && code <= 9889 || code >= 9898 && code <= 9899 || code >= 9917 && code <= 9918 || code >= 9924 && code <= 9925 || code >= 9934 && code <= 9934 || code >= 9940 && code <= 9940 || code >= 9962 && code <= 9962 || code >= 9970 && code <= 9971 || code >= 9973 && code <= 9973 || code >= 9978 && code <= 9978 || code >= 9981 && code <= 9981 || code >= 9989 && code <= 9989 || code >= 9994 && code <= 9995 || code >= 10024 && code <= 10024 || code >= 10060 && code <= 10060 || code >= 10062 && code <= 10062 || code >= 10067 && code <= 10069 || code >= 10071 && code <= 10071 || code >= 10133 && code <= 10135 || code >= 10160 && code <= 10160 || code >= 10175 && code <= 10175 || code >= 11035 && code <= 11036 || code >= 11088 && code <= 11088 || code >= 11093 && code <= 11093 || code >= 11904 && code <= 42191 && code !== 12351 || code >= 43360 && code <= 43388 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65040 && code <= 65049 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 126976 && code <= 129535 ? 2 : 1;
|
|
2243
|
+
}
|
|
2244
|
+
return width;
|
|
2245
|
+
}
|
|
2246
|
+
function visualRowsForLine(line, columns) {
|
|
2247
|
+
const plain = stripVTControlCharacters(line);
|
|
2248
|
+
const cols = Math.max(1, columns);
|
|
2249
|
+
const w = approxStringWidth(plain);
|
|
2250
|
+
return Math.max(1, Math.ceil(w / cols));
|
|
2251
|
+
}
|
|
2252
|
+
function countVisualRowsForLines(lines, columns) {
|
|
2253
|
+
const cols = columns !== void 0 && columns > 0 ? columns : process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
|
|
2254
|
+
return lines.reduce((sum, line) => sum + visualRowsForLine(line, cols), 0);
|
|
2255
|
+
}
|
|
2256
|
+
async function searchMultiselect(options) {
|
|
2257
|
+
const { message, items, maxVisible = 8, initialSelected = [], required = false, lockedSection } = options;
|
|
2258
|
+
return new Promise((resolve) => {
|
|
2259
|
+
const rl = readline.createInterface({
|
|
2260
|
+
input: process.stdin,
|
|
2261
|
+
output: silentOutput,
|
|
2262
|
+
terminal: false
|
|
2263
|
+
});
|
|
2264
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
2265
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
2266
|
+
let query = "";
|
|
2267
|
+
let cursor = 0;
|
|
2268
|
+
const selected = new Set(initialSelected);
|
|
2269
|
+
let lastRenderHeight = 0;
|
|
2270
|
+
const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : [];
|
|
2271
|
+
const filter = (item, q) => {
|
|
2272
|
+
if (!q) return true;
|
|
2273
|
+
const lowerQ = q.toLowerCase();
|
|
2274
|
+
return item.label.toLowerCase().includes(lowerQ) || String(item.value).toLowerCase().includes(lowerQ);
|
|
2275
|
+
};
|
|
2276
|
+
const getFiltered = () => {
|
|
2277
|
+
return items.filter((item) => filter(item, query));
|
|
2278
|
+
};
|
|
2279
|
+
const clearRender = () => {
|
|
2280
|
+
if (lastRenderHeight > 0) {
|
|
2281
|
+
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
2282
|
+
for (let i = 0; i < lastRenderHeight; i++) process.stdout.write("\x1B[2K\x1B[1B");
|
|
2283
|
+
process.stdout.write(`\x1b[${lastRenderHeight}A`);
|
|
2284
|
+
}
|
|
2285
|
+
};
|
|
2286
|
+
const render = (state = "active") => {
|
|
2287
|
+
clearRender();
|
|
2288
|
+
const lines = [];
|
|
2289
|
+
const filtered = getFiltered();
|
|
2290
|
+
const icon = state === "active" ? S_STEP_ACTIVE : state === "cancel" ? S_STEP_CANCEL : S_STEP_SUBMIT;
|
|
2291
|
+
lines.push(`${icon} ${import_picocolors.default.bold(message)}`);
|
|
2292
|
+
if (state === "active") {
|
|
2293
|
+
if (lockedSection && lockedSection.items.length > 0) {
|
|
2294
|
+
lines.push(`${S_BAR}`);
|
|
2295
|
+
const lockedTitle = `${import_picocolors.default.bold(lockedSection.title)} ${import_picocolors.default.dim("── always included")}`;
|
|
2296
|
+
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`);
|
|
2297
|
+
for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_BULLET} ${import_picocolors.default.bold(item.label)}`);
|
|
2298
|
+
lines.push(`${S_BAR}`);
|
|
2299
|
+
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`);
|
|
2300
|
+
}
|
|
2301
|
+
const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
|
|
2302
|
+
lines.push(searchLine);
|
|
2303
|
+
lines.push(`${S_BAR} ${import_picocolors.default.dim("↑↓ move, space select, enter confirm")}`);
|
|
2304
|
+
lines.push(`${S_BAR}`);
|
|
2305
|
+
const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible));
|
|
2306
|
+
const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible);
|
|
2307
|
+
const visibleItems = filtered.slice(visibleStart, visibleEnd);
|
|
2308
|
+
if (filtered.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("No matches found")}`);
|
|
2309
|
+
else {
|
|
2310
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
2311
|
+
const item = visibleItems[i];
|
|
2312
|
+
const actualIndex = visibleStart + i;
|
|
2313
|
+
const isSelected = selected.has(item.value);
|
|
2314
|
+
const isCursor = actualIndex === cursor;
|
|
2315
|
+
const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
|
|
2316
|
+
const label = isCursor ? import_picocolors.default.underline(item.label) : item.label;
|
|
2317
|
+
const hint = item.hint ? import_picocolors.default.dim(` (${item.hint})`) : "";
|
|
2318
|
+
const prefix = isCursor ? import_picocolors.default.cyan("❯") : " ";
|
|
2319
|
+
lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`);
|
|
2320
|
+
}
|
|
2321
|
+
const hiddenBefore = visibleStart;
|
|
2322
|
+
const hiddenAfter = filtered.length - visibleEnd;
|
|
2323
|
+
if (hiddenBefore > 0 || hiddenAfter > 0) {
|
|
2324
|
+
const parts = [];
|
|
2325
|
+
if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`);
|
|
2326
|
+
if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`);
|
|
2327
|
+
lines.push(`${S_BAR} ${import_picocolors.default.dim(parts.join(" "))}`);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
lines.push(`${S_BAR}`);
|
|
2331
|
+
const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
|
|
2332
|
+
if (allSelectedLabels.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("Selected: (none)")}`);
|
|
2333
|
+
else {
|
|
2334
|
+
const summary = allSelectedLabels.length <= 3 ? allSelectedLabels.join(", ") : `${allSelectedLabels.slice(0, 3).join(", ")} +${allSelectedLabels.length - 3} more`;
|
|
2335
|
+
lines.push(`${S_BAR} ${import_picocolors.default.green("Selected:")} ${summary}`);
|
|
2336
|
+
}
|
|
2337
|
+
lines.push(`${import_picocolors.default.dim("└")}`);
|
|
2338
|
+
} else if (state === "submit") {
|
|
2339
|
+
const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
|
|
2340
|
+
lines.push(`${S_BAR} ${import_picocolors.default.dim(allSelectedLabels.join(", "))}`);
|
|
2341
|
+
} else if (state === "cancel") lines.push(`${S_BAR} ${import_picocolors.default.strikethrough(import_picocolors.default.dim("Cancelled"))}`);
|
|
2342
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
2343
|
+
lastRenderHeight = countVisualRowsForLines(lines, process.stdout.columns);
|
|
2344
|
+
};
|
|
2345
|
+
const cleanup = () => {
|
|
2346
|
+
process.stdin.removeListener("keypress", keypressHandler);
|
|
2347
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
2348
|
+
rl.close();
|
|
2349
|
+
};
|
|
2350
|
+
const submit = () => {
|
|
2351
|
+
if (required && selected.size === 0 && lockedValues.length === 0) return;
|
|
2352
|
+
render("submit");
|
|
2353
|
+
cleanup();
|
|
2354
|
+
resolve([...lockedValues, ...Array.from(selected)]);
|
|
2355
|
+
};
|
|
2356
|
+
const cancel = () => {
|
|
2357
|
+
render("cancel");
|
|
2358
|
+
cleanup();
|
|
2359
|
+
resolve(cancelSymbol);
|
|
2360
|
+
};
|
|
2361
|
+
const keypressHandler = (_str, key) => {
|
|
2362
|
+
if (!key) return;
|
|
2363
|
+
const filtered = getFiltered();
|
|
2364
|
+
if (key.name === "return") {
|
|
2365
|
+
submit();
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
2369
|
+
cancel();
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
if (key.name === "up") {
|
|
2373
|
+
cursor = Math.max(0, cursor - 1);
|
|
2374
|
+
render();
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
if (key.name === "down") {
|
|
2378
|
+
cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
2379
|
+
render();
|
|
2380
|
+
return;
|
|
2381
|
+
}
|
|
2382
|
+
if (key.name === "space") {
|
|
2383
|
+
const item = filtered[cursor];
|
|
2384
|
+
if (item) if (selected.has(item.value)) selected.delete(item.value);
|
|
2385
|
+
else selected.add(item.value);
|
|
2386
|
+
render();
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
if (key.name === "backspace") {
|
|
2390
|
+
query = query.slice(0, -1);
|
|
2391
|
+
cursor = 0;
|
|
2392
|
+
render();
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
2396
|
+
query += key.sequence;
|
|
2397
|
+
cursor = 0;
|
|
2398
|
+
render();
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
process.stdin.on("keypress", keypressHandler);
|
|
2403
|
+
render();
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
const DEFAULT_CLONE_TIMEOUT_MS = 3e5;
|
|
2407
|
+
const CLONE_TIMEOUT_MS = (() => {
|
|
2408
|
+
const raw = process.env.SKILLS_CLONE_TIMEOUT_MS;
|
|
2409
|
+
if (!raw) return DEFAULT_CLONE_TIMEOUT_MS;
|
|
2410
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2411
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
|
|
2412
|
+
})();
|
|
2413
|
+
var GitCloneError = class extends Error {
|
|
2414
|
+
url;
|
|
2415
|
+
isTimeout;
|
|
2416
|
+
isAuthError;
|
|
2417
|
+
constructor(message, url, isTimeout = false, isAuthError = false) {
|
|
2418
|
+
super(message);
|
|
2419
|
+
this.name = "GitCloneError";
|
|
2420
|
+
this.url = url;
|
|
2421
|
+
this.isTimeout = isTimeout;
|
|
2422
|
+
this.isAuthError = isAuthError;
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
async function cloneRepo(url, ref) {
|
|
2426
|
+
const tempDir = await mkdtemp(join(tmpdir(), "skills-"));
|
|
2427
|
+
const git = esm_default({
|
|
2428
|
+
timeout: { block: CLONE_TIMEOUT_MS },
|
|
2429
|
+
config: [
|
|
2430
|
+
"filter.lfs.required=false",
|
|
2431
|
+
"filter.lfs.smudge=",
|
|
2432
|
+
"filter.lfs.clean=",
|
|
2433
|
+
"filter.lfs.process="
|
|
2434
|
+
]
|
|
2435
|
+
}).env({
|
|
2436
|
+
...process.env,
|
|
2437
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
2438
|
+
GIT_LFS_SKIP_SMUDGE: "1"
|
|
2439
|
+
});
|
|
2440
|
+
const cloneOptions = ref ? [
|
|
2441
|
+
"--depth",
|
|
2442
|
+
"1",
|
|
2443
|
+
"--branch",
|
|
2444
|
+
ref
|
|
2445
|
+
] : ["--depth", "1"];
|
|
2446
|
+
try {
|
|
2447
|
+
await git.clone(url, tempDir, cloneOptions);
|
|
2448
|
+
return tempDir;
|
|
2449
|
+
} catch (error) {
|
|
2450
|
+
await rm(tempDir, {
|
|
2451
|
+
recursive: true,
|
|
2452
|
+
force: true
|
|
2453
|
+
}).catch(() => {});
|
|
2454
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2455
|
+
const isTimeout = errorMessage.includes("block timeout") || errorMessage.includes("timed out");
|
|
2456
|
+
const isAuthError = errorMessage.includes("Authentication failed") || errorMessage.includes("could not read Username") || errorMessage.includes("Permission denied") || errorMessage.includes("Repository not found");
|
|
2457
|
+
if (isTimeout) throw new GitCloneError(`Clone timed out after ${Math.round(CLONE_TIMEOUT_MS / 1e3)}s. Common causes:\n - Large repository: raise the timeout with SKILLS_CLONE_TIMEOUT_MS=600000 (10m)\n - Slow network: retry, or clone manually and pass the local path to 'skills add'\n - Private repo without credentials: ensure auth is configured\n - For SSH: ssh-add -l (to check loaded keys)\n - For HTTPS: gh auth status (if using GitHub CLI)`, url, true, false);
|
|
2458
|
+
if (isAuthError) throw new GitCloneError(`Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`, url, false, true);
|
|
2459
|
+
throw new GitCloneError(`Failed to clone ${url}: ${errorMessage}`, url, false, false);
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
async function cleanupTempDir(dir) {
|
|
2463
|
+
const normalizedDir = normalize(resolve(dir));
|
|
2464
|
+
const normalizedTmpDir = normalize(resolve(tmpdir()));
|
|
2465
|
+
if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) throw new Error("Attempted to clean up directory outside of temp directory");
|
|
2466
|
+
await rm(dir, {
|
|
2467
|
+
recursive: true,
|
|
2468
|
+
force: true
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
1475
2471
|
const TELEMETRY_URL_SUFFIX = "/events";
|
|
1476
2472
|
const AUDIT_URL = "https://add-skill.vercel.sh/audit";
|
|
1477
2473
|
let portalUrl = null;
|
|
@@ -1780,10 +2776,30 @@ async function getSkillFromLock(skillName) {
|
|
|
1780
2776
|
async function getAllLockedSkills() {
|
|
1781
2777
|
return (await readSkillLock$1()).skills;
|
|
1782
2778
|
}
|
|
2779
|
+
async function addPluginToLock(pluginName, entry) {
|
|
2780
|
+
const lock = await readSkillLock$1();
|
|
2781
|
+
if (!lock.plugins) lock.plugins = {};
|
|
2782
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2783
|
+
const existing = lock.plugins[pluginName];
|
|
2784
|
+
lock.plugins[pluginName] = {
|
|
2785
|
+
...entry,
|
|
2786
|
+
installedAt: existing?.installedAt ?? now,
|
|
2787
|
+
updatedAt: now
|
|
2788
|
+
};
|
|
2789
|
+
await writeSkillLock(lock);
|
|
2790
|
+
}
|
|
2791
|
+
async function removePluginFromLock(pluginName) {
|
|
2792
|
+
const lock = await readSkillLock$1();
|
|
2793
|
+
if (!lock.plugins || !(pluginName in lock.plugins)) return false;
|
|
2794
|
+
delete lock.plugins[pluginName];
|
|
2795
|
+
await writeSkillLock(lock);
|
|
2796
|
+
return true;
|
|
2797
|
+
}
|
|
1783
2798
|
function createEmptyLockFile() {
|
|
1784
2799
|
return {
|
|
1785
2800
|
version: CURRENT_VERSION$1,
|
|
1786
2801
|
skills: {},
|
|
2802
|
+
plugins: {},
|
|
1787
2803
|
dismissed: {}
|
|
1788
2804
|
};
|
|
1789
2805
|
}
|
|
@@ -1821,6 +2837,11 @@ async function writeLocalLock(lock, cwd) {
|
|
|
1821
2837
|
version: lock.version,
|
|
1822
2838
|
skills: sortedSkills
|
|
1823
2839
|
};
|
|
2840
|
+
if (lock.plugins && Object.keys(lock.plugins).length > 0) {
|
|
2841
|
+
const sortedPlugins = {};
|
|
2842
|
+
for (const key of Object.keys(lock.plugins).sort()) sortedPlugins[key] = lock.plugins[key];
|
|
2843
|
+
sorted.plugins = sortedPlugins;
|
|
2844
|
+
}
|
|
1824
2845
|
await writeFile(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
1825
2846
|
}
|
|
1826
2847
|
async function computeSkillFolderHash(skillDir) {
|
|
@@ -1856,6 +2877,19 @@ async function addSkillToLocalLock(skillName, entry, cwd) {
|
|
|
1856
2877
|
lock.skills[skillName] = entry;
|
|
1857
2878
|
await writeLocalLock(lock, cwd);
|
|
1858
2879
|
}
|
|
2880
|
+
async function addPluginToLocalLock(pluginName, entry, cwd) {
|
|
2881
|
+
const lock = await readLocalLock(cwd);
|
|
2882
|
+
if (!lock.plugins) lock.plugins = {};
|
|
2883
|
+
lock.plugins[pluginName] = entry;
|
|
2884
|
+
await writeLocalLock(lock, cwd);
|
|
2885
|
+
}
|
|
2886
|
+
async function removePluginFromLocalLock(pluginName, cwd) {
|
|
2887
|
+
const lock = await readLocalLock(cwd);
|
|
2888
|
+
if (!lock.plugins || !(pluginName in lock.plugins)) return false;
|
|
2889
|
+
delete lock.plugins[pluginName];
|
|
2890
|
+
await writeLocalLock(lock, cwd);
|
|
2891
|
+
return true;
|
|
2892
|
+
}
|
|
1859
2893
|
function createEmptyLocalLock() {
|
|
1860
2894
|
return {
|
|
1861
2895
|
version: CURRENT_VERSION,
|
|
@@ -2115,6 +3149,10 @@ function buildSecurityLines(auditData, skills, source) {
|
|
|
2115
3149
|
lines.push(`${import_picocolors.default.dim("Details:")} ${import_picocolors.default.dim(`https://skills.sh/${source}`)}`);
|
|
2116
3150
|
return lines;
|
|
2117
3151
|
}
|
|
3152
|
+
function agentTypeFromDisplay(display) {
|
|
3153
|
+
for (const [type, cfg] of Object.entries(agents)) if (cfg.displayName === display) return type;
|
|
3154
|
+
return null;
|
|
3155
|
+
}
|
|
2118
3156
|
function shortenPath$2(fullPath, cwd) {
|
|
2119
3157
|
const home = homedir();
|
|
2120
3158
|
if (fullPath === home || fullPath.startsWith(home + sep)) return "~" + fullPath.slice(home.length);
|
|
@@ -2476,6 +3514,126 @@ async function handleWellKnownSkills(source, url, options, spinner) {
|
|
|
2476
3514
|
Se(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Review skills before use; they run with full agent permissions."));
|
|
2477
3515
|
await promptForFindSkills(options, targetAgents);
|
|
2478
3516
|
}
|
|
3517
|
+
async function runTeamBatchAdd(teamScope, options) {
|
|
3518
|
+
const cfg = loadConfig();
|
|
3519
|
+
if (!cfg.token) {
|
|
3520
|
+
console.log();
|
|
3521
|
+
console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Login required for team installs"));
|
|
3522
|
+
console.log();
|
|
3523
|
+
console.log(` Run ${import_picocolors.default.cyan("shoplazza-ai-dev-cli login")} first.`);
|
|
3524
|
+
console.log();
|
|
3525
|
+
process.exit(1);
|
|
3526
|
+
}
|
|
3527
|
+
console.log();
|
|
3528
|
+
Ie(import_picocolors.default.bgCyan(import_picocolors.default.black(" skills ")));
|
|
3529
|
+
const spinner = Y();
|
|
3530
|
+
spinner.start(`Fetching team "${teamScope}" assets...`);
|
|
3531
|
+
let items;
|
|
3532
|
+
try {
|
|
3533
|
+
items = await fetchTeamAssets(teamScope, cfg.portal, cfg.token);
|
|
3534
|
+
} catch (err) {
|
|
3535
|
+
spinner.stop(import_picocolors.default.red("Failed to list team assets"));
|
|
3536
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3537
|
+
Se(import_picocolors.default.red(msg));
|
|
3538
|
+
process.exit(1);
|
|
3539
|
+
}
|
|
3540
|
+
const skills = items.filter((i) => i.kind === "skill");
|
|
3541
|
+
spinner.stop(`Found ${skills.length} skill${skills.length === 1 ? "" : "s"}`);
|
|
3542
|
+
if (skills.length === 0) {
|
|
3543
|
+
Se(import_picocolors.default.dim(`Team "${teamScope}" has no skills.`));
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
const validAgents = Object.keys(agents);
|
|
3547
|
+
let targetAgents;
|
|
3548
|
+
if (options.agent?.includes("*")) {
|
|
3549
|
+
targetAgents = validAgents;
|
|
3550
|
+
M.info(`Installing to all ${targetAgents.length} agents`);
|
|
3551
|
+
} else if (options.agent && options.agent.length > 0) {
|
|
3552
|
+
const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
|
|
3553
|
+
if (invalidAgents.length > 0) {
|
|
3554
|
+
M.error(`Invalid agents: ${invalidAgents.join(", ")}`);
|
|
3555
|
+
M.info(`Valid agents: ${validAgents.join(", ")}`);
|
|
3556
|
+
process.exit(1);
|
|
3557
|
+
}
|
|
3558
|
+
targetAgents = options.agent;
|
|
3559
|
+
} else {
|
|
3560
|
+
spinner.start("Loading agents...");
|
|
3561
|
+
const installedAgents = await detectInstalledAgents();
|
|
3562
|
+
spinner.stop(`${validAgents.length} agents`);
|
|
3563
|
+
if (installedAgents.length === 0) if (options.yes) {
|
|
3564
|
+
targetAgents = validAgents;
|
|
3565
|
+
M.info("Installing to all agents");
|
|
3566
|
+
} else {
|
|
3567
|
+
M.info("Select agents to install skills to");
|
|
3568
|
+
const selected = await promptForAgents("Which agents do you want to install to?", buildAgentChoices(validAgents, { global: options.global }));
|
|
3569
|
+
if (pD(selected)) {
|
|
3570
|
+
xe("Installation cancelled");
|
|
3571
|
+
process.exit(0);
|
|
3572
|
+
}
|
|
3573
|
+
targetAgents = selected;
|
|
3574
|
+
}
|
|
3575
|
+
else if (installedAgents.length === 1 || options.yes) {
|
|
3576
|
+
targetAgents = ensureUniversalAgents(installedAgents);
|
|
3577
|
+
if (installedAgents.length === 1) {
|
|
3578
|
+
const firstAgent = installedAgents[0];
|
|
3579
|
+
M.info(`Installing to: ${import_picocolors.default.cyan(agents[firstAgent].displayName)}`);
|
|
3580
|
+
} else M.info(`Installing to: ${installedAgents.map((a) => import_picocolors.default.cyan(agents[a].displayName)).join(", ")}`);
|
|
3581
|
+
} else {
|
|
3582
|
+
const selected = await selectAgentsInteractive({ global: options.global });
|
|
3583
|
+
if (pD(selected)) {
|
|
3584
|
+
xe("Installation cancelled");
|
|
3585
|
+
process.exit(0);
|
|
3586
|
+
}
|
|
3587
|
+
targetAgents = selected;
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
let installGlobally = options.global ?? false;
|
|
3591
|
+
const supportsGlobal = targetAgents.some((a) => agents[a].globalSkillsDir !== void 0);
|
|
3592
|
+
if (options.global === void 0 && !options.yes && supportsGlobal) {
|
|
3593
|
+
const scope = await ve({
|
|
3594
|
+
message: "Installation scope",
|
|
3595
|
+
options: [{
|
|
3596
|
+
value: false,
|
|
3597
|
+
label: "Project",
|
|
3598
|
+
hint: "Install in current directory (committed with your project)"
|
|
3599
|
+
}, {
|
|
3600
|
+
value: true,
|
|
3601
|
+
label: "Global",
|
|
3602
|
+
hint: "Install in home directory (available across all projects)"
|
|
3603
|
+
}]
|
|
3604
|
+
});
|
|
3605
|
+
if (pD(scope)) {
|
|
3606
|
+
xe("Installation cancelled");
|
|
3607
|
+
process.exit(0);
|
|
3608
|
+
}
|
|
3609
|
+
installGlobally = scope;
|
|
3610
|
+
}
|
|
3611
|
+
const childOptions = {
|
|
3612
|
+
...options,
|
|
3613
|
+
team: void 0,
|
|
3614
|
+
yes: true,
|
|
3615
|
+
agent: targetAgents,
|
|
3616
|
+
global: installGlobally
|
|
3617
|
+
};
|
|
3618
|
+
let failures = 0;
|
|
3619
|
+
for (const item of skills) {
|
|
3620
|
+
M.step(`Installing ${import_picocolors.default.cyan(item.name)}`);
|
|
3621
|
+
try {
|
|
3622
|
+
await runAdd([item.ref], childOptions);
|
|
3623
|
+
} catch (err) {
|
|
3624
|
+
failures++;
|
|
3625
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3626
|
+
M.warn(`Failed to install ${item.name}: ${msg}`);
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
console.log();
|
|
3630
|
+
if (failures === 0) Se(import_picocolors.default.green(`Installed ${skills.length} skill${skills.length === 1 ? "" : "s"} from team "${teamScope}"`));
|
|
3631
|
+
else {
|
|
3632
|
+
Se(import_picocolors.default.yellow(`Installed ${skills.length - failures}/${skills.length} skills (${failures} failed) from team "${teamScope}"`));
|
|
3633
|
+
process.exit(1);
|
|
3634
|
+
}
|
|
3635
|
+
await promptForFindSkills(options, targetAgents);
|
|
3636
|
+
}
|
|
2479
3637
|
async function runAdd(args, options = {}) {
|
|
2480
3638
|
const source = args[0];
|
|
2481
3639
|
let installTipShown = false;
|
|
@@ -2484,6 +3642,20 @@ async function runAdd(args, options = {}) {
|
|
|
2484
3642
|
M.message(import_picocolors.default.dim("Tip: use the --yes (-y) and --global (-g) flags to install without prompts."));
|
|
2485
3643
|
installTipShown = true;
|
|
2486
3644
|
};
|
|
3645
|
+
if (options.team && source) {
|
|
3646
|
+
console.log();
|
|
3647
|
+
console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("--team cannot be combined with a positional source"));
|
|
3648
|
+
console.log();
|
|
3649
|
+
console.log(import_picocolors.default.dim(" Use one of:"));
|
|
3650
|
+
console.log(` ${import_picocolors.default.cyan("npx shoplazza-ai-dev-cli add")} ${import_picocolors.default.yellow("<source>")}`);
|
|
3651
|
+
console.log(` ${import_picocolors.default.cyan("npx shoplazza-ai-dev-cli add")} ${import_picocolors.default.yellow("--team <scope>")}`);
|
|
3652
|
+
console.log();
|
|
3653
|
+
process.exit(1);
|
|
3654
|
+
}
|
|
3655
|
+
if (!source && options.team) {
|
|
3656
|
+
await runTeamBatchAdd(options.team, options);
|
|
3657
|
+
return;
|
|
3658
|
+
}
|
|
2487
3659
|
if (!source) {
|
|
2488
3660
|
console.log();
|
|
2489
3661
|
console.log(import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" ERROR "))) + " " + import_picocolors.default.red("Missing required argument: source"));
|
|
@@ -2537,6 +3709,176 @@ async function runAdd(args, options = {}) {
|
|
|
2537
3709
|
options.skill = options.skill || [];
|
|
2538
3710
|
if (!options.skill.includes(parsed.skillFilter)) options.skill.push(parsed.skillFilter);
|
|
2539
3711
|
}
|
|
3712
|
+
if (parsed.type === "portal" && parsed.portalRef) {
|
|
3713
|
+
const portalRef = parsed.portalRef;
|
|
3714
|
+
const pluginsDirRe = /^(?:@public|@teams\/[^/]+)\/plugins$/;
|
|
3715
|
+
const pluginsItemRe = /^(?:@public|@teams\/[^/]+)\/plugins\/[^/]+/;
|
|
3716
|
+
let resolvedPluginRef = null;
|
|
3717
|
+
if (pluginsDirRe.test(portalRef) && options.plugin && options.plugin.length === 1) resolvedPluginRef = `${portalRef}/${options.plugin[0]}`;
|
|
3718
|
+
else if (pluginsItemRe.test(portalRef)) resolvedPluginRef = portalRef;
|
|
3719
|
+
if (resolvedPluginRef !== null) {
|
|
3720
|
+
const isTeam = resolvedPluginRef.startsWith("@teams/");
|
|
3721
|
+
const teamMatch = isTeam ? resolvedPluginRef.match(/^@teams\/([^/]+)/) : null;
|
|
3722
|
+
const scopeNs = isTeam && teamMatch ? teamMatch[1] : "public";
|
|
3723
|
+
let targetAgents;
|
|
3724
|
+
const validAgents = Object.keys(agents);
|
|
3725
|
+
if (options.agent?.includes("*")) targetAgents = validAgents;
|
|
3726
|
+
else if (options.agent && options.agent.length > 0) targetAgents = options.agent;
|
|
3727
|
+
else {
|
|
3728
|
+
spinner.start("Loading agents...");
|
|
3729
|
+
const installedAgents = await detectInstalledAgents();
|
|
3730
|
+
spinner.stop(`${Object.keys(agents).length} agents`);
|
|
3731
|
+
if (options.yes) targetAgents = installedAgents.length > 0 ? ensureUniversalAgents(installedAgents) : validAgents;
|
|
3732
|
+
else if (installedAgents.length === 0) {
|
|
3733
|
+
const selected = await promptForAgents("Which IDEs do you want to install this plugin to?", buildAgentChoices(Object.keys(agents), { global: options.global }));
|
|
3734
|
+
if (pD(selected)) {
|
|
3735
|
+
xe("Installation cancelled");
|
|
3736
|
+
await cleanup(tempDir);
|
|
3737
|
+
process.exit(0);
|
|
3738
|
+
}
|
|
3739
|
+
targetAgents = selected;
|
|
3740
|
+
} else if (installedAgents.length === 1) {
|
|
3741
|
+
targetAgents = ensureUniversalAgents(installedAgents);
|
|
3742
|
+
M.info(`Installing to: ${import_picocolors.default.cyan(agents[installedAgents[0]].displayName)}`);
|
|
3743
|
+
} else {
|
|
3744
|
+
const selected = await selectAgentsInteractive({ global: options.global });
|
|
3745
|
+
if (pD(selected)) {
|
|
3746
|
+
xe("Installation cancelled");
|
|
3747
|
+
await cleanup(tempDir);
|
|
3748
|
+
process.exit(0);
|
|
3749
|
+
}
|
|
3750
|
+
targetAgents = selected;
|
|
3751
|
+
}
|
|
3752
|
+
}
|
|
3753
|
+
let installGloballyForPlugin = options.global ?? false;
|
|
3754
|
+
if (options.global === void 0 && !options.yes) {
|
|
3755
|
+
const scopeChoice = await ve({
|
|
3756
|
+
message: "Installation scope",
|
|
3757
|
+
options: [{
|
|
3758
|
+
value: false,
|
|
3759
|
+
label: "Project",
|
|
3760
|
+
hint: "Install in current directory"
|
|
3761
|
+
}, {
|
|
3762
|
+
value: true,
|
|
3763
|
+
label: "Global",
|
|
3764
|
+
hint: "Install in home directory"
|
|
3765
|
+
}]
|
|
3766
|
+
});
|
|
3767
|
+
if (pD(scopeChoice)) {
|
|
3768
|
+
xe("Installation cancelled");
|
|
3769
|
+
await cleanup(tempDir);
|
|
3770
|
+
process.exit(0);
|
|
3771
|
+
}
|
|
3772
|
+
installGloballyForPlugin = scopeChoice;
|
|
3773
|
+
}
|
|
3774
|
+
{
|
|
3775
|
+
const previewCanonical = join(getStoreRoot(), "plugins", `${scopeNs}__${options.plugin && options.plugin[0] || resolvedPluginRef.split("/").pop()}`);
|
|
3776
|
+
const summaryLines = [];
|
|
3777
|
+
summaryLines.push(import_picocolors.default.cyan(shortenPath$2(previewCanonical, process.cwd())));
|
|
3778
|
+
const agentNames = targetAgents.map((a) => agents[a].displayName);
|
|
3779
|
+
if (agentNames.length > 0) summaryLines.push(` ${import_picocolors.default.dim("symlink →")} ${formatList$1(agentNames)}`);
|
|
3780
|
+
if (existsSync(previewCanonical)) summaryLines.push(` ${import_picocolors.default.yellow("overwrites:")} ${formatList$1(agentNames)}`);
|
|
3781
|
+
console.log();
|
|
3782
|
+
Me(summaryLines.join("\n"), "Installation Summary");
|
|
3783
|
+
}
|
|
3784
|
+
if (!options.yes) {
|
|
3785
|
+
const confirmed = await ye({ message: `Install plugin ${import_picocolors.default.cyan(resolvedPluginRef)}?` });
|
|
3786
|
+
if (pD(confirmed) || !confirmed) {
|
|
3787
|
+
xe("Installation cancelled");
|
|
3788
|
+
await cleanup(tempDir);
|
|
3789
|
+
process.exit(0);
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
spinner.start(`Installing plugin ${import_picocolors.default.cyan(resolvedPluginRef)}...`);
|
|
3793
|
+
let pluginResult;
|
|
3794
|
+
try {
|
|
3795
|
+
pluginResult = await installPlugin(resolvedPluginRef, {
|
|
3796
|
+
scope: installGloballyForPlugin ? "global" : "project",
|
|
3797
|
+
agents: targetAgents,
|
|
3798
|
+
cwd: process.cwd(),
|
|
3799
|
+
yes: options.yes,
|
|
3800
|
+
force: options.force
|
|
3801
|
+
});
|
|
3802
|
+
} catch (err) {
|
|
3803
|
+
spinner.stop(import_picocolors.default.red("Plugin installation failed"));
|
|
3804
|
+
throw err;
|
|
3805
|
+
}
|
|
3806
|
+
spinner.stop(import_picocolors.default.green("Plugin installed"));
|
|
3807
|
+
if (pluginResult.installedEntries.length > 0) {
|
|
3808
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3809
|
+
for (const e of pluginResult.installedEntries) {
|
|
3810
|
+
const list = grouped.get(e.agent) ?? [];
|
|
3811
|
+
list.push(e);
|
|
3812
|
+
grouped.set(e.agent, list);
|
|
3813
|
+
}
|
|
3814
|
+
console.log();
|
|
3815
|
+
for (const [agent, list] of grouped) {
|
|
3816
|
+
console.log(import_picocolors.default.bold(agents[agent].displayName));
|
|
3817
|
+
for (const e of list) console.log(` ${import_picocolors.default.dim(e.kind.padEnd(16))}${e.entryPath}`);
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
if (pluginResult.forgeOtherReplacements.length > 0) {
|
|
3821
|
+
console.log();
|
|
3822
|
+
console.log(import_picocolors.default.yellow(`⚠ Replaced ${pluginResult.forgeOtherReplacements.length} forge-other entries:`));
|
|
3823
|
+
for (const r of pluginResult.forgeOtherReplacements) {
|
|
3824
|
+
console.log(` ${r.entryPath}`);
|
|
3825
|
+
console.log(` was ${r.otherRef} (now broken — uninstall recommended)`);
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
if (pluginResult.ignoredComponents.length > 0) {
|
|
3829
|
+
console.log();
|
|
3830
|
+
console.log(import_picocolors.default.dim(`ℹ Ignored non-MVP components: ${pluginResult.ignoredComponents.join(", ")}`));
|
|
3831
|
+
}
|
|
3832
|
+
for (const agentType of targetAgents) {
|
|
3833
|
+
const { warnings } = await ensureForgeHooks(agentType);
|
|
3834
|
+
for (const w of warnings) M.warn(import_picocolors.default.yellow(w));
|
|
3835
|
+
}
|
|
3836
|
+
const { pluginName: lockPluginName } = parsePluginRef$1(resolvedPluginRef);
|
|
3837
|
+
const lockSource = resolvedPluginRef.startsWith("@teams/") ? `@teams/${resolvedPluginRef.split("/")[1]}` : "@public";
|
|
3838
|
+
const lockPluginPath = `plugins/${lockPluginName}/plugin.json`;
|
|
3839
|
+
let lockHash = "";
|
|
3840
|
+
try {
|
|
3841
|
+
lockHash = await computeSkillFolderHash(pluginResult.pluginCanonical);
|
|
3842
|
+
} catch {}
|
|
3843
|
+
try {
|
|
3844
|
+
if (installGloballyForPlugin) {
|
|
3845
|
+
await addPluginToLock(lockPluginName, {
|
|
3846
|
+
source: lockSource,
|
|
3847
|
+
sourceType: "portal",
|
|
3848
|
+
pluginPath: lockPluginPath,
|
|
3849
|
+
computedHash: lockHash
|
|
3850
|
+
});
|
|
3851
|
+
M.info(`Recorded in lock file: ${import_picocolors.default.dim(getSkillLockPath$1())}`);
|
|
3852
|
+
} else {
|
|
3853
|
+
await addPluginToLocalLock(lockPluginName, {
|
|
3854
|
+
source: lockSource,
|
|
3855
|
+
sourceType: "portal",
|
|
3856
|
+
pluginPath: lockPluginPath,
|
|
3857
|
+
computedHash: lockHash
|
|
3858
|
+
}, process.cwd());
|
|
3859
|
+
M.info(`Recorded in lock file: ${import_picocolors.default.dim(getLocalLockPath(process.cwd()))}`);
|
|
3860
|
+
}
|
|
3861
|
+
} catch (err) {
|
|
3862
|
+
M.warn(import_picocolors.default.yellow(`failed to record plugin in lock file (install still succeeded): ${err instanceof Error ? err.message : String(err)}`));
|
|
3863
|
+
}
|
|
3864
|
+
track({
|
|
3865
|
+
event_type: "install",
|
|
3866
|
+
asset_ref: resolvedPluginRef,
|
|
3867
|
+
agent: "cli",
|
|
3868
|
+
install_mode: "cli"
|
|
3869
|
+
});
|
|
3870
|
+
Se(import_picocolors.default.green(`Plugin ${import_picocolors.default.bold(resolvedPluginRef)} installed successfully`));
|
|
3871
|
+
return;
|
|
3872
|
+
}
|
|
3873
|
+
if (/^(?:@public|@teams\/[^/]+)\/plugins/.test(portalRef) && !resolvedPluginRef) {
|
|
3874
|
+
Se(import_picocolors.default.red(`Cannot install plugin directory without a name. Use --plugin <name> or specify a full ref: ${portalRef}/<name>`));
|
|
3875
|
+
process.exit(1);
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
if (parsed.type === "portal" && parsed.portalRef && /^(?:@public|@teams\/[^/]+)\/rules/.test(parsed.portalRef)) {
|
|
3879
|
+
Se(import_picocolors.default.red("Rules are not installable as standalone — install the plugin that contains the rule."));
|
|
3880
|
+
process.exit(1);
|
|
3881
|
+
}
|
|
2540
3882
|
if (parsed.type === "portal" && parsed.portalRef && isDirectoryPortalRef(parsed.portalRef) && options.skill && options.skill.length === 1 && options.skill[0] !== "*") parsed.portalRef = `${parsed.portalRef}/${options.skill[0]}`;
|
|
2541
3883
|
const includeInternal = !!(options.skill && options.skill.length > 0);
|
|
2542
3884
|
let skills;
|
|
@@ -2858,8 +4200,12 @@ async function runAdd(args, options = {}) {
|
|
|
2858
4200
|
process.exit(0);
|
|
2859
4201
|
}
|
|
2860
4202
|
installMode = modeChoice;
|
|
2861
|
-
}
|
|
4203
|
+
}
|
|
2862
4204
|
const cwd = process.cwd();
|
|
4205
|
+
const _summaryPortalRef = parsed.type === "portal" || parsed.type === "public" || parsed.type === "team" ? parsed.portalRef ?? null : null;
|
|
4206
|
+
const _summaryIsTeam = _summaryPortalRef?.startsWith("@teams/") ?? false;
|
|
4207
|
+
const _summaryTeamMatch = _summaryIsTeam ? _summaryPortalRef?.match(/^@teams\/([^/]+)/) : null;
|
|
4208
|
+
const _summaryScopeNs = _summaryIsTeam && _summaryTeamMatch ? _summaryTeamMatch[1] : "public";
|
|
2863
4209
|
const summaryLines = [];
|
|
2864
4210
|
targetAgents.map((a) => agents[a].displayName);
|
|
2865
4211
|
const overwriteChecks = await Promise.all(selectedSkills.flatMap((skill) => targetAgents.map(async (agent) => ({
|
|
@@ -2879,10 +4225,11 @@ async function runAdd(args, options = {}) {
|
|
|
2879
4225
|
if (!groupedSummary[group]) groupedSummary[group] = [];
|
|
2880
4226
|
groupedSummary[group].push(skill);
|
|
2881
4227
|
} else ungroupedSummary.push(skill);
|
|
4228
|
+
const _summaryStoreRoot = getStoreRoot(installGlobally ? "global" : "project", cwd);
|
|
2882
4229
|
const printSkillSummary = (skills) => {
|
|
2883
4230
|
for (const skill of skills) {
|
|
2884
4231
|
if (summaryLines.length > 0) summaryLines.push("");
|
|
2885
|
-
const shortCanonical = shortenPath$2(
|
|
4232
|
+
const shortCanonical = shortenPath$2(join(_summaryStoreRoot, "skills", `${_summaryScopeNs}__${sanitizeName(skill.name)}`), cwd);
|
|
2886
4233
|
summaryLines.push(`${import_picocolors.default.cyan(shortCanonical)}`);
|
|
2887
4234
|
summaryLines.push(...buildAgentSummaryLines(targetAgents, installMode));
|
|
2888
4235
|
const skillOverwrites = overwriteStatus.get(skill.name);
|
|
@@ -2926,6 +4273,11 @@ async function runAdd(args, options = {}) {
|
|
|
2926
4273
|
}
|
|
2927
4274
|
spinner.start("Installing skills...");
|
|
2928
4275
|
const results = [];
|
|
4276
|
+
const allForgeOtherReplacements = [];
|
|
4277
|
+
const _installPortalRef = parsed.type === "portal" || parsed.type === "public" || parsed.type === "team" ? parsed.portalRef ?? null : null;
|
|
4278
|
+
const _installIsTeam = _installPortalRef?.startsWith("@teams/") ?? false;
|
|
4279
|
+
const _installTeamMatch = _installIsTeam ? _installPortalRef?.match(/^@teams\/([^/]+)/) : null;
|
|
4280
|
+
const _installScopeNs = _installIsTeam && _installTeamMatch ? _installTeamMatch[1] : "public";
|
|
2929
4281
|
for (const skill of selectedSkills) for (const agent of targetAgents) {
|
|
2930
4282
|
let result;
|
|
2931
4283
|
if (blobResult && "files" in skill) {
|
|
@@ -2937,10 +4289,21 @@ async function runAdd(args, options = {}) {
|
|
|
2937
4289
|
global: installGlobally,
|
|
2938
4290
|
mode: installMode
|
|
2939
4291
|
});
|
|
2940
|
-
} else
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
4292
|
+
} else {
|
|
4293
|
+
const skillRef = _installPortalRef ? `${_installPortalRef}/${skill.name ?? ""}` : skill.name ?? "unknown";
|
|
4294
|
+
result = await installSkillForAgent(skill, agent, {
|
|
4295
|
+
scope: installGlobally ? "global" : "project",
|
|
4296
|
+
ref: skillRef,
|
|
4297
|
+
scopeNamespace: _installScopeNs,
|
|
4298
|
+
mode: installMode,
|
|
4299
|
+
forceOverwrite: options.force || options.yes,
|
|
4300
|
+
onConflictPrompt: (conflicts) => promptForeignOverwrite(conflicts, {
|
|
4301
|
+
yes: options.yes,
|
|
4302
|
+
force: options.force
|
|
4303
|
+
})
|
|
4304
|
+
});
|
|
4305
|
+
if (result.forgeOtherReplacements.length > 0) allForgeOtherReplacements.push(...result.forgeOtherReplacements);
|
|
4306
|
+
}
|
|
2944
4307
|
results.push({
|
|
2945
4308
|
skill: getSkillDisplayName(skill),
|
|
2946
4309
|
agent: agents[agent].displayName,
|
|
@@ -2952,6 +4315,57 @@ async function runAdd(args, options = {}) {
|
|
|
2952
4315
|
console.log();
|
|
2953
4316
|
const successful = results.filter((r) => r.success);
|
|
2954
4317
|
const failed = results.filter((r) => !r.success);
|
|
4318
|
+
if (allForgeOtherReplacements.length > 0) {
|
|
4319
|
+
console.log();
|
|
4320
|
+
console.log(import_picocolors.default.yellow(`⚠ Replaced ${allForgeOtherReplacements.length} forge-other entries:`));
|
|
4321
|
+
for (const r of allForgeOtherReplacements) console.log(` ${r.entryPath}\n was ${r.otherRef} (now broken — uninstall recommended)`);
|
|
4322
|
+
}
|
|
4323
|
+
if (parsed.type === "portal" && parsed.portalRef && successful.length > 0) {
|
|
4324
|
+
const portalRef = parsed.portalRef;
|
|
4325
|
+
const isTeam = portalRef.startsWith("@teams/");
|
|
4326
|
+
const teamMatch = isTeam ? portalRef.match(/^@teams\/([^/]+)/) : null;
|
|
4327
|
+
const scope = isTeam && teamMatch ? `team:${teamMatch[1]}` : "public";
|
|
4328
|
+
const directoryRef = isDirectoryPortalRef(portalRef);
|
|
4329
|
+
const assetType = (portalRef.match(/\/(skills|rules)(?:\/|$)/)?.[1] || "skills") === "rules" ? "rule" : "skill";
|
|
4330
|
+
const installedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4331
|
+
const manifestWarnings = [];
|
|
4332
|
+
for (const r of successful) {
|
|
4333
|
+
const targetDir = r.canonicalPath || r.path;
|
|
4334
|
+
if (!targetDir) continue;
|
|
4335
|
+
const skillName = r.skill;
|
|
4336
|
+
const manifest = {
|
|
4337
|
+
ref: directoryRef ? `${portalRef}/${skillName}` : portalRef,
|
|
4338
|
+
scope,
|
|
4339
|
+
type: assetType,
|
|
4340
|
+
name: skillName,
|
|
4341
|
+
install_mode: agentTypeFromDisplay(r.agent) ?? "claude-code",
|
|
4342
|
+
installed_at: installedAt
|
|
4343
|
+
};
|
|
4344
|
+
try {
|
|
4345
|
+
await writeForgeSourceManifest(targetDir, manifest);
|
|
4346
|
+
} catch (err) {
|
|
4347
|
+
manifestWarnings.push(`Failed to write .forge-source.json for ${skillName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
if (manifestWarnings.length > 0) for (const w of manifestWarnings) M.warn(import_picocolors.default.yellow(w));
|
|
4351
|
+
const successfulAgentTypes = /* @__PURE__ */ new Set();
|
|
4352
|
+
for (const r of successful) {
|
|
4353
|
+
const t = agentTypeFromDisplay(r.agent);
|
|
4354
|
+
if (t) successfulAgentTypes.add(t);
|
|
4355
|
+
}
|
|
4356
|
+
for (const agentType of successfulAgentTypes) {
|
|
4357
|
+
const { warnings } = await ensureForgeHooks(agentType);
|
|
4358
|
+
for (const w of warnings) M.warn(import_picocolors.default.yellow(w));
|
|
4359
|
+
}
|
|
4360
|
+
const installedSkillNames = /* @__PURE__ */ new Set();
|
|
4361
|
+
for (const r of successful) installedSkillNames.add(r.skill);
|
|
4362
|
+
for (const skillName of installedSkillNames) track({
|
|
4363
|
+
event_type: "install",
|
|
4364
|
+
asset_ref: directoryRef ? `${portalRef}/${skillName}` : portalRef,
|
|
4365
|
+
agent: "cli",
|
|
4366
|
+
install_mode: "cli"
|
|
4367
|
+
});
|
|
4368
|
+
}
|
|
2955
4369
|
const skillFiles = {};
|
|
2956
4370
|
for (const skill of selectedSkills) if (blobResult && "repoPath" in skill) skillFiles[skill.name] = skill.repoPath;
|
|
2957
4371
|
else if (repoRoot && skill.path === repoRoot) skillFiles[skill.name] = "SKILL.md";
|
|
@@ -3154,6 +4568,7 @@ function parseAddOptions(args) {
|
|
|
3154
4568
|
const arg = args[i];
|
|
3155
4569
|
if (arg === "-g" || arg === "--global") options.global = true;
|
|
3156
4570
|
else if (arg === "-y" || arg === "--yes") options.yes = true;
|
|
4571
|
+
else if (arg === "-f" || arg === "--force") options.force = true;
|
|
3157
4572
|
else if (arg === "-l" || arg === "--list") options.list = true;
|
|
3158
4573
|
else if (arg === "--all") options.all = true;
|
|
3159
4574
|
else if (arg === "-a" || arg === "--agent") {
|
|
@@ -3176,6 +4591,16 @@ function parseAddOptions(args) {
|
|
|
3176
4591
|
nextArg = args[i];
|
|
3177
4592
|
}
|
|
3178
4593
|
i--;
|
|
4594
|
+
} else if (arg === "--plugin") {
|
|
4595
|
+
options.plugin = options.plugin || [];
|
|
4596
|
+
i++;
|
|
4597
|
+
let nextArg = args[i];
|
|
4598
|
+
while (i < args.length && nextArg && !nextArg.startsWith("-")) {
|
|
4599
|
+
options.plugin.push(nextArg);
|
|
4600
|
+
i++;
|
|
4601
|
+
nextArg = args[i];
|
|
4602
|
+
}
|
|
4603
|
+
i--;
|
|
3179
4604
|
} else if (arg === "--rule") {
|
|
3180
4605
|
options.rule = options.rule || [];
|
|
3181
4606
|
i++;
|
|
@@ -3868,6 +5293,21 @@ async function runList(args) {
|
|
|
3868
5293
|
async function removeCommand(skillNames, options) {
|
|
3869
5294
|
const isGlobal = options.global ?? false;
|
|
3870
5295
|
const cwd = process.cwd();
|
|
5296
|
+
const pluginRefRe = /^@[^/]+\/plugins\/[^/]+/;
|
|
5297
|
+
const pluginRefs = skillNames.filter((n) => pluginRefRe.test(n));
|
|
5298
|
+
if (pluginRefs.length > 0) {
|
|
5299
|
+
const scope = isGlobal ? "global" : "project";
|
|
5300
|
+
for (const ref of pluginRefs) try {
|
|
5301
|
+
M.info(`Uninstalling plugin ${import_picocolors.default.cyan(ref)}...`);
|
|
5302
|
+
await uninstallPlugin(ref, scope, cwd);
|
|
5303
|
+
M.success(import_picocolors.default.green(`Plugin ${ref} uninstalled`));
|
|
5304
|
+
} catch (err) {
|
|
5305
|
+
M.error(import_picocolors.default.red(`Failed to uninstall plugin ${ref}: ${err instanceof Error ? err.message : String(err)}`));
|
|
5306
|
+
}
|
|
5307
|
+
const nonPluginNames = skillNames.filter((n) => !pluginRefRe.test(n));
|
|
5308
|
+
if (nonPluginNames.length === 0) return;
|
|
5309
|
+
skillNames = nonPluginNames;
|
|
5310
|
+
}
|
|
3871
5311
|
const spinner = Y();
|
|
3872
5312
|
spinner.start("Scanning for installed skills...");
|
|
3873
5313
|
const skillNamesSet = /* @__PURE__ */ new Set();
|
|
@@ -4055,6 +5495,130 @@ function parseRemoveOptions(args) {
|
|
|
4055
5495
|
options
|
|
4056
5496
|
};
|
|
4057
5497
|
}
|
|
5498
|
+
async function uninstallPlugin(ref, scope, cwd) {
|
|
5499
|
+
const { scopeNamespace, pluginName } = parsePluginRef(ref);
|
|
5500
|
+
const storeRoot = getStoreRoot(scope, cwd);
|
|
5501
|
+
const pluginCanonical = join(storeRoot, "plugins", `${scopeNamespace}__${pluginName}`);
|
|
5502
|
+
const ledgerPath = join(pluginCanonical, ".forge-plugin.json");
|
|
5503
|
+
let ledger;
|
|
5504
|
+
try {
|
|
5505
|
+
ledger = JSON.parse(await readFile(ledgerPath, "utf-8"));
|
|
5506
|
+
} catch {
|
|
5507
|
+
throw new Error(`plugin not installed (no ledger at ${ledgerPath})`);
|
|
5508
|
+
}
|
|
5509
|
+
for (const agent of ledger.install_modes) for (const c of ledger.installed_components) {
|
|
5510
|
+
const entryPath = resolveEntryPath(agent, c, pluginCanonical, scope, cwd);
|
|
5511
|
+
if (!entryPath) continue;
|
|
5512
|
+
const status = await assertEntryReplaceable(entryPath, {
|
|
5513
|
+
intendedRef: ref,
|
|
5514
|
+
intendedCanonical: pluginCanonical,
|
|
5515
|
+
storeRoot
|
|
5516
|
+
});
|
|
5517
|
+
if (status.kind === "forge-self") try {
|
|
5518
|
+
await rm(entryPath, {
|
|
5519
|
+
recursive: true,
|
|
5520
|
+
force: true
|
|
5521
|
+
});
|
|
5522
|
+
} catch {}
|
|
5523
|
+
else if (status.kind !== "absent") M.warn(`skipping ${entryPath} (${status.kind}) — not deleted (preserved by forge)`);
|
|
5524
|
+
}
|
|
5525
|
+
if (ledger.install_modes.includes("codex")) await cleanupCodexConfig(ledger, scope, cwd, scopeNamespace, pluginName);
|
|
5526
|
+
try {
|
|
5527
|
+
await rm(pluginCanonical, {
|
|
5528
|
+
recursive: true,
|
|
5529
|
+
force: true
|
|
5530
|
+
});
|
|
5531
|
+
} catch {}
|
|
5532
|
+
try {
|
|
5533
|
+
await removePluginFromLock(pluginName);
|
|
5534
|
+
} catch {}
|
|
5535
|
+
try {
|
|
5536
|
+
await removePluginFromLocalLock(pluginName, cwd);
|
|
5537
|
+
} catch {}
|
|
5538
|
+
}
|
|
5539
|
+
function resolveEntryPath(agent, c, pluginCanonical, scope, cwd) {
|
|
5540
|
+
let component;
|
|
5541
|
+
switch (c.type) {
|
|
5542
|
+
case "skill":
|
|
5543
|
+
component = {
|
|
5544
|
+
kind: "plugin-skill",
|
|
5545
|
+
pluginCanonical,
|
|
5546
|
+
skillName: c.name
|
|
5547
|
+
};
|
|
5548
|
+
break;
|
|
5549
|
+
case "agent-manifest":
|
|
5550
|
+
component = {
|
|
5551
|
+
kind: "plugin-agent-manifest",
|
|
5552
|
+
pluginCanonical,
|
|
5553
|
+
filename: c.name
|
|
5554
|
+
};
|
|
5555
|
+
break;
|
|
5556
|
+
case "agent-deps-dir":
|
|
5557
|
+
component = {
|
|
5558
|
+
kind: "plugin-agent-deps-dir",
|
|
5559
|
+
pluginCanonical,
|
|
5560
|
+
dirname: c.name
|
|
5561
|
+
};
|
|
5562
|
+
break;
|
|
5563
|
+
case "rule-manifest":
|
|
5564
|
+
component = {
|
|
5565
|
+
kind: "plugin-rule-manifest",
|
|
5566
|
+
pluginCanonical,
|
|
5567
|
+
filename: c.name
|
|
5568
|
+
};
|
|
5569
|
+
break;
|
|
5570
|
+
case "rule-deps-dir":
|
|
5571
|
+
component = {
|
|
5572
|
+
kind: "plugin-rule-deps-dir",
|
|
5573
|
+
pluginCanonical,
|
|
5574
|
+
dirname: c.name
|
|
5575
|
+
};
|
|
5576
|
+
break;
|
|
5577
|
+
default: return "";
|
|
5578
|
+
}
|
|
5579
|
+
return getInstallTargets(agent, component, scope, cwd).entry;
|
|
5580
|
+
}
|
|
5581
|
+
async function cleanupCodexConfig(ledger, scope, cwd, scopeNs, pluginName) {
|
|
5582
|
+
const configToml = join(scope === "global" ? getAgentHome("codex") : join(cwd ?? process.cwd(), ".codex"), "config.toml");
|
|
5583
|
+
let tomlRaw = "";
|
|
5584
|
+
try {
|
|
5585
|
+
tomlRaw = await readFile(configToml, "utf-8");
|
|
5586
|
+
} catch {}
|
|
5587
|
+
if (tomlRaw) {
|
|
5588
|
+
let changed = false;
|
|
5589
|
+
for (const c of ledger.installed_components) if (c.type === "agent-manifest") {
|
|
5590
|
+
const id = c.name.replace(/\.md$/, "");
|
|
5591
|
+
const updated = removeCodexAgent(tomlRaw, id);
|
|
5592
|
+
if (updated !== tomlRaw) {
|
|
5593
|
+
tomlRaw = updated;
|
|
5594
|
+
changed = true;
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
if (changed) await writeFile(configToml, tomlRaw, "utf-8");
|
|
5598
|
+
}
|
|
5599
|
+
const canonicalAgentsMd = join(getStoreRoot(scope, cwd), "codex-agents.md");
|
|
5600
|
+
let agentsRaw = "";
|
|
5601
|
+
try {
|
|
5602
|
+
agentsRaw = await readFile(canonicalAgentsMd, "utf-8");
|
|
5603
|
+
} catch {
|
|
5604
|
+
return;
|
|
5605
|
+
}
|
|
5606
|
+
const updatedAgentsMd = removePluginRules(agentsRaw, `${scopeNs}:${pluginName}`);
|
|
5607
|
+
if (updatedAgentsMd !== agentsRaw) await writeFile(canonicalAgentsMd, updatedAgentsMd, "utf-8");
|
|
5608
|
+
}
|
|
5609
|
+
function parsePluginRef(ref) {
|
|
5610
|
+
let m = /^@public\/plugins\/(.+)$/.exec(ref);
|
|
5611
|
+
if (m) return {
|
|
5612
|
+
scopeNamespace: "public",
|
|
5613
|
+
pluginName: m[1]
|
|
5614
|
+
};
|
|
5615
|
+
m = /^@teams\/([^/]+)\/plugins\/(.+)$/.exec(ref);
|
|
5616
|
+
if (m) return {
|
|
5617
|
+
scopeNamespace: m[1],
|
|
5618
|
+
pluginName: m[2]
|
|
5619
|
+
};
|
|
5620
|
+
throw new Error(`invalid plugin ref: ${ref}`);
|
|
5621
|
+
}
|
|
4058
5622
|
function formatSourceInput(sourceUrl, ref) {
|
|
4059
5623
|
if (!ref) return sourceUrl;
|
|
4060
5624
|
return `${sourceUrl}#${ref}`;
|
|
@@ -4594,6 +6158,7 @@ ${BOLD}Manage Skills:${RESET}
|
|
|
4594
6158
|
@teams/<scope>/skills --skill <name>
|
|
4595
6159
|
@public/skills/<name> # legacy, still supported
|
|
4596
6160
|
https://gitlab.shoplazza.site/.../<repo>.git
|
|
6161
|
+
add --team <scope> Install every skill owned by a team
|
|
4597
6162
|
remove [skills] Remove installed skills
|
|
4598
6163
|
list, ls List installed skills
|
|
4599
6164
|
find [query] Search for skills interactively
|
|
@@ -4615,6 +6180,8 @@ ${BOLD}Add Options:${RESET}
|
|
|
4615
6180
|
-s, --skill <skills> Specify skill names to install (use '*' for all skills)
|
|
4616
6181
|
-l, --list List available skills in the source without installing
|
|
4617
6182
|
-y, --yes Skip confirmation prompts
|
|
6183
|
+
--team <scope> Install every skill owned by the team
|
|
6184
|
+
(no positional source; mutually exclusive with <source>)
|
|
4618
6185
|
--copy Copy files instead of symlinking to agent directories
|
|
4619
6186
|
--all Shorthand for --skill '*' --agent '*' -y
|
|
4620
6187
|
--full-depth Search all subdirectories even when a root SKILL.md exists
|