@kurokeita/add-skill 1.20.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/dist/bin/cli.js +576 -792
- package/dist/skills/git-commit/SKILL.md +101 -37
- package/dist/skills/handoff/SKILL.md +16 -0
- package/dist/skills/init-agents-md/SKILL.md +51 -0
- package/dist/skills/init-agents-md/references/agents_template.md +42 -0
- package/dist/skills/init-agents-md/references/patterns_template.md +19 -0
- package/dist/skills/koreader-plugin-development/SKILL.md +141 -0
- package/dist/skills/koreader-plugin-development/examples/2-example-patch.lua +17 -0
- package/dist/skills/koreader-plugin-development/examples/_meta.lua +6 -0
- package/dist/skills/koreader-plugin-development/examples/helloworld.lua +36 -0
- package/dist/skills/koreader-plugin-development/references/debugging.md +92 -0
- package/dist/skills/koreader-plugin-development/references/events-and-widgets.md +102 -0
- package/dist/skills/koreader-plugin-development/references/patches.md +118 -0
- package/dist/skills/koreader-plugin-development/references/plugin-anatomy.md +97 -0
- package/dist/skills/statusline-setup/SKILL.md +211 -66
- package/dist/skills/universalize-agents/SKILL.md +245 -0
- package/dist/skills/universalize-agents/reference/hook-templates/README.md +30 -0
- package/dist/skills/universalize-agents/reference/hook-templates/claude-code.json +15 -0
- package/dist/skills/universalize-agents/reference/hook-templates/codex.toml +10 -0
- package/dist/skills/universalize-agents/reference/hook-templates/copilot.json +9 -0
- package/dist/skills/universalize-agents/reference/hook-templates/gemini.json +11 -0
- package/dist/skills/universalize-agents/reference/mapping.md +100 -0
- package/dist/skills/universalize-agents/scripts/agent-setup.ps1 +83 -0
- package/dist/skills/universalize-agents/scripts/agent-setup.sh +98 -0
- package/dist/skills/update-agents-md/SKILL.md +43 -0
- package/package.json +2 -5
package/dist/bin/cli.js
CHANGED
|
@@ -6,8 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
import pc6 from "picocolors";
|
|
7
7
|
|
|
8
8
|
// src/commands/add.ts
|
|
9
|
-
import
|
|
10
|
-
import path6 from "node:path";
|
|
9
|
+
import path4 from "node:path";
|
|
11
10
|
import {
|
|
12
11
|
autocompleteMultiselect,
|
|
13
12
|
cancel,
|
|
@@ -23,99 +22,30 @@ import {
|
|
|
23
22
|
import fs4 from "fs-extra";
|
|
24
23
|
import pc2 from "picocolors";
|
|
25
24
|
|
|
26
|
-
// src/utils/
|
|
27
|
-
import
|
|
28
|
-
import path from "node:path";
|
|
29
|
-
import fs from "fs-extra";
|
|
30
|
-
async function fetchSkillFromGitHub(url) {
|
|
31
|
-
const regex = /github\.com\/([^/]+)\/([^/]+)\/(?:tree|blob)\/([^/]+)\/(.+)/;
|
|
32
|
-
const match = url.match(regex);
|
|
33
|
-
if (!match) {
|
|
34
|
-
throw new Error(
|
|
35
|
-
"Invalid GitHub URL. Format: https://github.com/owner/repo/tree/branch/path/to/skill or blob/branch/path/to/file"
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
const [, owner, repo, ref, skillPath] = match;
|
|
39
|
-
const skillName = path.basename(skillPath);
|
|
40
|
-
const initialApiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${skillPath}?ref=${ref}`;
|
|
41
|
-
const tempDir = path.join(os.tmpdir(), "ai-agents-install", skillName);
|
|
42
|
-
await fs.ensureDir(tempDir);
|
|
43
|
-
await fs.emptyDir(tempDir);
|
|
44
|
-
let isFile = false;
|
|
45
|
-
async function downloadDirectory(apiUrl, localDir) {
|
|
46
|
-
const response = await fetch(apiUrl, {
|
|
47
|
-
headers: {
|
|
48
|
-
"User-Agent": "ai-agents-cli",
|
|
49
|
-
Accept: "application/vnd.github.v3+json"
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`Failed to fetch from GitHub (${response.status}): ${response.statusText}`
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
const data = await response.json();
|
|
58
|
-
if (!Array.isArray(data) && data.type === "file") {
|
|
59
|
-
isFile = true;
|
|
60
|
-
const item = data;
|
|
61
|
-
if (item.download_url) {
|
|
62
|
-
const fileContentResponse = await fetch(item.download_url);
|
|
63
|
-
if (!fileContentResponse.ok) {
|
|
64
|
-
throw new Error(
|
|
65
|
-
`Failed to download ${item.name}: ${fileContentResponse.statusText}`
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
const content = await fileContentResponse.text();
|
|
69
|
-
await fs.writeFile(path.join(localDir, item.name), content);
|
|
70
|
-
}
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
if (!Array.isArray(data)) {
|
|
74
|
-
throw new Error(
|
|
75
|
-
"Invalid response from GitHub API. Expected directory listing or file object."
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
const files = data;
|
|
79
|
-
for (const item of files) {
|
|
80
|
-
if (item.type === "file" && item.download_url) {
|
|
81
|
-
const fileContentResponse = await fetch(item.download_url);
|
|
82
|
-
if (!fileContentResponse.ok) {
|
|
83
|
-
console.warn(
|
|
84
|
-
`Failed to download ${item.name}: ${fileContentResponse.statusText}`
|
|
85
|
-
);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
const content = await fileContentResponse.text();
|
|
89
|
-
await fs.writeFile(path.join(localDir, item.name), content);
|
|
90
|
-
} else if (item.type === "dir") {
|
|
91
|
-
const newLocalDir = path.join(localDir, item.name);
|
|
92
|
-
await fs.ensureDir(newLocalDir);
|
|
93
|
-
await downloadDirectory(item.url, newLocalDir);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
await downloadDirectory(initialApiUrl, tempDir);
|
|
98
|
-
return { tempDir, skillName, isFile };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// src/utils/paths.ts
|
|
102
|
-
import fs2 from "node:fs";
|
|
25
|
+
// src/utils/agent-setup.ts
|
|
26
|
+
import { execFileSync } from "node:child_process";
|
|
103
27
|
import os2 from "node:os";
|
|
104
28
|
import path2 from "node:path";
|
|
29
|
+
import fs2 from "fs-extra";
|
|
30
|
+
|
|
31
|
+
// src/utils/paths.ts
|
|
32
|
+
import fs from "node:fs";
|
|
33
|
+
import os from "node:os";
|
|
34
|
+
import path from "node:path";
|
|
105
35
|
import { fileURLToPath } from "node:url";
|
|
106
36
|
var __filename = fileURLToPath(import.meta.url);
|
|
107
|
-
var __dirname =
|
|
37
|
+
var __dirname = path.dirname(__filename);
|
|
108
38
|
function hasBundledAssets(rootDir) {
|
|
109
39
|
return ["skills", "agents", "workflows"].some(
|
|
110
|
-
(dir) =>
|
|
40
|
+
(dir) => fs.existsSync(path.join(rootDir, dir))
|
|
111
41
|
);
|
|
112
42
|
}
|
|
113
43
|
function resolveProjectRoot(moduleDir) {
|
|
114
|
-
const sourceRoot =
|
|
44
|
+
const sourceRoot = path.resolve(moduleDir, "../..");
|
|
115
45
|
if (hasBundledAssets(sourceRoot)) {
|
|
116
46
|
return sourceRoot;
|
|
117
47
|
}
|
|
118
|
-
return
|
|
48
|
+
return path.resolve(moduleDir, "..");
|
|
119
49
|
}
|
|
120
50
|
var PROJECT_ROOT = resolveProjectRoot(__dirname);
|
|
121
51
|
var PLATFORM_LABELS = {
|
|
@@ -127,26 +57,26 @@ var PLATFORM_LABELS = {
|
|
|
127
57
|
windsurf: "Windsurf"
|
|
128
58
|
};
|
|
129
59
|
var PLATFORM_PATHS_SKILLS = {
|
|
130
|
-
antigravity:
|
|
131
|
-
"claude-code":
|
|
132
|
-
codex:
|
|
133
|
-
copilot:
|
|
134
|
-
gemini:
|
|
135
|
-
windsurf:
|
|
60
|
+
antigravity: path.join(os.homedir(), ".gemini/antigravity/global_skills"),
|
|
61
|
+
"claude-code": path.join(os.homedir(), ".claude/skills"),
|
|
62
|
+
codex: path.join(os.homedir(), ".codex/skills"),
|
|
63
|
+
copilot: path.join(os.homedir(), ".copilot/skills"),
|
|
64
|
+
gemini: path.join(os.homedir(), ".gemini/skills"),
|
|
65
|
+
windsurf: path.join(os.homedir(), ".codeium/windsurf/skills")
|
|
136
66
|
};
|
|
137
67
|
var PLATFORM_PATHS_AGENTS = {
|
|
138
|
-
"claude-code":
|
|
139
|
-
codex:
|
|
140
|
-
copilot:
|
|
141
|
-
gemini:
|
|
68
|
+
"claude-code": path.join(os.homedir(), ".claude/agents"),
|
|
69
|
+
codex: path.join(os.homedir(), ".codex/skills"),
|
|
70
|
+
copilot: path.join(os.homedir(), ".copilot/agents"),
|
|
71
|
+
gemini: path.join(os.homedir(), ".gemini/agents")
|
|
142
72
|
};
|
|
143
73
|
var PLATFORM_PATHS_WORKFLOWS = {
|
|
144
|
-
antigravity:
|
|
145
|
-
"claude-code":
|
|
146
|
-
codex:
|
|
147
|
-
copilot:
|
|
148
|
-
gemini:
|
|
149
|
-
windsurf:
|
|
74
|
+
antigravity: path.join(os.homedir(), ".gemini/antigravity/global_workflows"),
|
|
75
|
+
"claude-code": path.join(os.homedir(), ".claude/commands"),
|
|
76
|
+
codex: path.join(os.homedir(), ".codex/skills"),
|
|
77
|
+
copilot: path.join(os.homedir(), ".copilot/prompts"),
|
|
78
|
+
gemini: path.join(os.homedir(), ".gemini/commands"),
|
|
79
|
+
windsurf: path.join(os.homedir(), ".codeium/windsurf/global_workflows")
|
|
150
80
|
};
|
|
151
81
|
var PROJECT_RELATIVE_PLATFORM_PATHS_SKILLS = {
|
|
152
82
|
antigravity: ".gemini/antigravity/skills",
|
|
@@ -164,7 +94,7 @@ var PROJECT_RELATIVE_PLATFORM_PATHS_AGENTS = {
|
|
|
164
94
|
};
|
|
165
95
|
var PROJECT_RELATIVE_PLATFORM_PATHS_WORKFLOWS = {
|
|
166
96
|
antigravity: ".gemini/antigravity/workflows",
|
|
167
|
-
"claude-code": ".claude/
|
|
97
|
+
"claude-code": ".claude/commands",
|
|
168
98
|
codex: ".codex/skills",
|
|
169
99
|
copilot: ".copilot/prompts",
|
|
170
100
|
gemini: ".gemini/commands",
|
|
@@ -173,7 +103,7 @@ var PROJECT_RELATIVE_PLATFORM_PATHS_WORKFLOWS = {
|
|
|
173
103
|
function resolveProjectPaths(relative, root) {
|
|
174
104
|
const out = {};
|
|
175
105
|
for (const [platform, rel] of Object.entries(relative)) {
|
|
176
|
-
if (rel) out[platform] =
|
|
106
|
+
if (rel) out[platform] = path.join(root, rel);
|
|
177
107
|
}
|
|
178
108
|
return out;
|
|
179
109
|
}
|
|
@@ -187,438 +117,352 @@ function getProjectPlatformPathsWorkflows(root) {
|
|
|
187
117
|
return resolveProjectPaths(PROJECT_RELATIVE_PLATFORM_PATHS_WORKFLOWS, root);
|
|
188
118
|
}
|
|
189
119
|
var TYPE_DIRS = {
|
|
190
|
-
skill:
|
|
191
|
-
agent:
|
|
192
|
-
workflow:
|
|
120
|
+
skill: path.join(PROJECT_ROOT, "skills"),
|
|
121
|
+
agent: path.join(PROJECT_ROOT, "agents"),
|
|
122
|
+
workflow: path.join(PROJECT_ROOT, "workflows")
|
|
193
123
|
};
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
case "workflow":
|
|
209
|
-
return getProjectPlatformPathsWorkflows(root);
|
|
210
|
-
default:
|
|
211
|
-
return getProjectPlatformPathsSkills(root);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
function getTargetPaths(type, scope = "global", projectRoot) {
|
|
124
|
+
var TYPE_SUBDIRS = {
|
|
125
|
+
skill: "skills",
|
|
126
|
+
agent: "agents",
|
|
127
|
+
workflow: "commands"
|
|
128
|
+
};
|
|
129
|
+
var AGENT_SETUP_SCRIPTS_DIR = path.join(
|
|
130
|
+
PROJECT_ROOT,
|
|
131
|
+
"skills/universalize-agents/scripts"
|
|
132
|
+
);
|
|
133
|
+
var HOOK_TEMPLATES_DIR = path.join(
|
|
134
|
+
PROJECT_ROOT,
|
|
135
|
+
"skills/universalize-agents/reference/hook-templates"
|
|
136
|
+
);
|
|
137
|
+
function getAgentsBase(scope, root) {
|
|
215
138
|
if (scope === "project") {
|
|
216
|
-
|
|
217
|
-
return getProjectTargetPaths(type, root);
|
|
139
|
+
return path.join(root ?? process.cwd(), ".agents");
|
|
218
140
|
}
|
|
219
|
-
return
|
|
141
|
+
return path.join(os.homedir(), ".agents");
|
|
220
142
|
}
|
|
221
143
|
|
|
222
|
-
// src/utils/
|
|
223
|
-
var
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
// src/utils/platforms/claude-code.ts
|
|
234
|
-
import path3 from "node:path";
|
|
235
|
-
import yaml from "js-yaml";
|
|
236
|
-
var frontmatterRegex = /^---\n([\s\S]*?)\n---/;
|
|
237
|
-
var CLAUDE_CODE_TOOLS = [
|
|
238
|
-
"Read",
|
|
239
|
-
"Edit",
|
|
240
|
-
"Write",
|
|
241
|
-
"Bash",
|
|
242
|
-
"Glob",
|
|
243
|
-
"Grep",
|
|
244
|
-
"WebFetch",
|
|
245
|
-
"WebSearch"
|
|
246
|
-
];
|
|
247
|
-
var ClaudeCodeHandler = class {
|
|
248
|
-
platform = "claude-code";
|
|
249
|
-
getTargetFileName(itemName, type) {
|
|
250
|
-
if (type === "agent") {
|
|
251
|
-
const name = itemName.replace(/\.md$/, "");
|
|
252
|
-
return path3.join(name, `${name}.md`);
|
|
253
|
-
}
|
|
254
|
-
if (type === "workflow") {
|
|
255
|
-
const name = itemName.replace(/\.md$/, "");
|
|
256
|
-
return path3.join(name, "SKILL.md");
|
|
257
|
-
}
|
|
258
|
-
return itemName;
|
|
259
|
-
}
|
|
260
|
-
transform(content, type, itemName) {
|
|
261
|
-
if (type === "skill") return content;
|
|
262
|
-
if (type === "agent") {
|
|
263
|
-
return this.transformAgent(content, itemName);
|
|
264
|
-
}
|
|
265
|
-
if (type === "workflow") {
|
|
266
|
-
return this.transformWorkflow(content, itemName);
|
|
267
|
-
}
|
|
268
|
-
return content;
|
|
269
|
-
}
|
|
270
|
-
transformAgent(content, itemName) {
|
|
271
|
-
const match = content.match(frontmatterRegex);
|
|
272
|
-
let frontmatter = {
|
|
273
|
-
name: itemName.replace(/\.md$/, ""),
|
|
274
|
-
description: "",
|
|
275
|
-
tools: CLAUDE_CODE_TOOLS
|
|
144
|
+
// src/utils/agent-setup.ts
|
|
145
|
+
var LINK_SUBDIRS = ["skills", "agents", "commands"];
|
|
146
|
+
function getScopePaths(scope, root) {
|
|
147
|
+
if (scope === "project") {
|
|
148
|
+
return {
|
|
149
|
+
skills: getProjectPlatformPathsSkills(root),
|
|
150
|
+
agents: getProjectPlatformPathsAgents(root),
|
|
151
|
+
commands: getProjectPlatformPathsWorkflows(root)
|
|
276
152
|
};
|
|
277
|
-
let body = content;
|
|
278
|
-
if (match) {
|
|
279
|
-
try {
|
|
280
|
-
const parsed = yaml.load(match[1]);
|
|
281
|
-
frontmatter = { ...frontmatter, ...parsed };
|
|
282
|
-
frontmatter.tools = CLAUDE_CODE_TOOLS;
|
|
283
|
-
delete frontmatter.target;
|
|
284
|
-
body = content.replace(frontmatterRegex, "").trim();
|
|
285
|
-
} catch (_) {
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
const yamlStr = yaml.dump(frontmatter);
|
|
289
|
-
return `---
|
|
290
|
-
${yamlStr}---
|
|
291
|
-
|
|
292
|
-
${body}`;
|
|
293
153
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
const frontmatter = yaml.dump({
|
|
317
|
-
name,
|
|
318
|
-
description,
|
|
319
|
-
"user-invocable": true
|
|
320
|
-
});
|
|
321
|
-
return `---
|
|
322
|
-
${frontmatter}---
|
|
323
|
-
|
|
324
|
-
${body}`;
|
|
325
|
-
}
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
// src/utils/platforms/codex.ts
|
|
329
|
-
import path4 from "node:path";
|
|
330
|
-
import fs3 from "fs-extra";
|
|
331
|
-
import yaml2 from "js-yaml";
|
|
332
|
-
var frontmatterRegex2 = /^---\n([\s\S]*?)\n---/;
|
|
333
|
-
var CODEX_TYPE_FIELD = "x-ai-agents-type";
|
|
334
|
-
function extractTitle(body) {
|
|
335
|
-
const match = body.match(/^#\s+(.+)$/m);
|
|
336
|
-
return match?.[1]?.trim();
|
|
337
|
-
}
|
|
338
|
-
function buildDescription(type, itemName, title) {
|
|
339
|
-
const subject = title || itemName;
|
|
340
|
-
if (type === "agent") {
|
|
341
|
-
return `This skill should be used when Codex needs to follow the ${subject} agent instructions.`;
|
|
342
|
-
}
|
|
343
|
-
if (type === "workflow") {
|
|
344
|
-
return `This skill should be used when Codex needs to follow the ${subject} workflow instructions.`;
|
|
345
|
-
}
|
|
346
|
-
return `This skill should be used when Codex needs the ${subject} skill.`;
|
|
347
|
-
}
|
|
348
|
-
function toYamlFrontmatter(name, description, extraFields = {}) {
|
|
349
|
-
const normalizedName = name.replace(/\n+/g, " ").trim();
|
|
350
|
-
const normalizedDescription = description.replace(/\n+/g, " ").trim();
|
|
351
|
-
return yaml2.dump(
|
|
352
|
-
{
|
|
353
|
-
name: normalizedName,
|
|
354
|
-
description: normalizedDescription,
|
|
355
|
-
...extraFields
|
|
356
|
-
},
|
|
357
|
-
{ lineWidth: -1 }
|
|
358
|
-
).trimEnd();
|
|
359
|
-
}
|
|
360
|
-
function parseCodexType(content) {
|
|
361
|
-
const match = content.match(frontmatterRegex2);
|
|
362
|
-
if (!match) return "skill";
|
|
363
|
-
try {
|
|
364
|
-
const parsed = yaml2.load(match[1]);
|
|
365
|
-
if (parsed && typeof parsed === "object") {
|
|
366
|
-
const type = parsed[CODEX_TYPE_FIELD];
|
|
367
|
-
if (type === "agent" || type === "workflow" || type === "skill") {
|
|
368
|
-
return type;
|
|
369
|
-
}
|
|
154
|
+
return {
|
|
155
|
+
skills: PLATFORM_PATHS_SKILLS,
|
|
156
|
+
agents: PLATFORM_PATHS_AGENTS,
|
|
157
|
+
commands: PLATFORM_PATHS_WORKFLOWS
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function buildLinkMap(scope, root, presentSubdirs, platforms) {
|
|
161
|
+
const scopePaths = getScopePaths(scope, root);
|
|
162
|
+
const lines = [];
|
|
163
|
+
const seen = /* @__PURE__ */ new Set();
|
|
164
|
+
for (const sub of LINK_SUBDIRS) {
|
|
165
|
+
if (!presentSubdirs.includes(sub)) continue;
|
|
166
|
+
const platformDirs = scopePaths[sub];
|
|
167
|
+
for (const [platform, dest] of Object.entries(platformDirs)) {
|
|
168
|
+
if (!dest) continue;
|
|
169
|
+
if (platforms && !platforms.includes(platform)) continue;
|
|
170
|
+
const formattedDest = scope === "project" ? path2.relative(root, dest).replace(/\\/g, "/") : dest;
|
|
171
|
+
const line = `${sub}|${formattedDest}`;
|
|
172
|
+
if (seen.has(line)) continue;
|
|
173
|
+
seen.add(line);
|
|
174
|
+
lines.push(line);
|
|
370
175
|
}
|
|
371
|
-
} catch {
|
|
372
|
-
return "skill";
|
|
373
176
|
}
|
|
374
|
-
return "
|
|
375
|
-
}
|
|
376
|
-
async function
|
|
377
|
-
let targetPath = null;
|
|
378
|
-
if (entry.isDirectory()) {
|
|
379
|
-
targetPath = path4.join(basePath, entry.name, "SKILL.md");
|
|
380
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
381
|
-
targetPath = path4.join(basePath, entry.name);
|
|
382
|
-
}
|
|
383
|
-
if (!targetPath) return false;
|
|
177
|
+
return lines.join("\n");
|
|
178
|
+
}
|
|
179
|
+
async function isRealConflict(target) {
|
|
384
180
|
try {
|
|
385
|
-
const
|
|
386
|
-
return
|
|
181
|
+
const stat = await fs2.lstat(target);
|
|
182
|
+
return !stat.isSymbolicLink();
|
|
387
183
|
} catch {
|
|
388
184
|
return false;
|
|
389
185
|
}
|
|
390
186
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
body = content.replace(frontmatterRegex2, "").trim();
|
|
408
|
-
try {
|
|
409
|
-
const parsed = yaml2.load(match[1]);
|
|
410
|
-
if (parsed && typeof parsed === "object") {
|
|
411
|
-
const frontmatter = parsed;
|
|
412
|
-
if (typeof frontmatter.name === "string" && frontmatter.name.trim()) {
|
|
413
|
-
name = frontmatter.name.trim();
|
|
414
|
-
}
|
|
415
|
-
if (typeof frontmatter.description === "string" && frontmatter.description.trim()) {
|
|
416
|
-
description = frontmatter.description.trim();
|
|
417
|
-
}
|
|
187
|
+
async function detectConflicts(agentsBase, platforms, scope, root) {
|
|
188
|
+
const scopePaths = getScopePaths(scope, root);
|
|
189
|
+
const conflicts = [];
|
|
190
|
+
for (const sub of LINK_SUBDIRS) {
|
|
191
|
+
const srcDir = path2.join(agentsBase, sub);
|
|
192
|
+
if (!await fs2.pathExists(srcDir)) continue;
|
|
193
|
+
const entries = await fs2.readdir(srcDir);
|
|
194
|
+
const platformDirs = Object.entries(scopePaths[sub]).filter(
|
|
195
|
+
([platform, dest]) => !!dest && platforms.includes(platform)
|
|
196
|
+
);
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
const targetPaths = [];
|
|
199
|
+
for (const [, dest] of platformDirs) {
|
|
200
|
+
const target = path2.join(dest, entry);
|
|
201
|
+
if (await isRealConflict(target)) {
|
|
202
|
+
targetPaths.push(target);
|
|
418
203
|
}
|
|
419
|
-
} catch {
|
|
420
|
-
name = itemName;
|
|
421
|
-
description = "";
|
|
422
204
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
description = buildDescription(type, itemName, extractTitle(body));
|
|
426
|
-
}
|
|
427
|
-
const yamlStr = toYamlFrontmatter(name, description, {
|
|
428
|
-
[CODEX_TYPE_FIELD]: type
|
|
429
|
-
});
|
|
430
|
-
return `---
|
|
431
|
-
${yamlStr}
|
|
432
|
-
---
|
|
433
|
-
|
|
434
|
-
${body}`;
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
// src/utils/platforms/copilot.ts
|
|
439
|
-
import yaml3 from "js-yaml";
|
|
440
|
-
var CopilotHandler = class {
|
|
441
|
-
platform = "copilot";
|
|
442
|
-
getTargetFileName(itemName, type) {
|
|
443
|
-
if (type === "agent") {
|
|
444
|
-
return `${itemName.replace(/\.md$/, "")}.md`;
|
|
445
|
-
}
|
|
446
|
-
return itemName;
|
|
447
|
-
}
|
|
448
|
-
transform(content, type, itemName) {
|
|
449
|
-
if (type !== "agent") return content;
|
|
450
|
-
const frontmatterRegex3 = /^---\n([\s\S]*?)\n---/;
|
|
451
|
-
const match = content.match(frontmatterRegex3);
|
|
452
|
-
let frontmatter = {
|
|
453
|
-
name: itemName,
|
|
454
|
-
description: "",
|
|
455
|
-
target: "github-copilot",
|
|
456
|
-
tools: ["edit", "terminal", "ls", "grep_search", "githubRepo"]
|
|
457
|
-
};
|
|
458
|
-
let body = content;
|
|
459
|
-
if (match) {
|
|
460
|
-
try {
|
|
461
|
-
const parsed = yaml3.load(match[1]);
|
|
462
|
-
frontmatter = { ...frontmatter, ...parsed };
|
|
463
|
-
frontmatter.target = "github-copilot";
|
|
464
|
-
frontmatter.tools = [
|
|
465
|
-
"edit",
|
|
466
|
-
"terminal",
|
|
467
|
-
"ls",
|
|
468
|
-
"grep_search",
|
|
469
|
-
"githubRepo"
|
|
470
|
-
];
|
|
471
|
-
body = content.replace(frontmatterRegex3, "").trim();
|
|
472
|
-
} catch (_) {
|
|
205
|
+
if (targetPaths.length > 0) {
|
|
206
|
+
conflicts.push({ entry, subdir: sub, targetPaths });
|
|
473
207
|
}
|
|
474
208
|
}
|
|
475
|
-
const yamlStr = yaml3.dump(frontmatter);
|
|
476
|
-
return `---
|
|
477
|
-
${yamlStr}---
|
|
478
|
-
|
|
479
|
-
${body}`;
|
|
480
209
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
this.platform = platform;
|
|
210
|
+
return conflicts;
|
|
211
|
+
}
|
|
212
|
+
async function removeAgentDirEntries(targetPaths) {
|
|
213
|
+
for (const target of targetPaths) {
|
|
214
|
+
await fs2.remove(target);
|
|
487
215
|
}
|
|
488
|
-
|
|
489
|
-
|
|
216
|
+
}
|
|
217
|
+
async function dropAgentsEntry(agentsBase, subdir, entry) {
|
|
218
|
+
await fs2.remove(path2.join(agentsBase, subdir, entry));
|
|
219
|
+
}
|
|
220
|
+
async function presentLinkSubdirs(agentsBase) {
|
|
221
|
+
const present = [];
|
|
222
|
+
for (const sub of LINK_SUBDIRS) {
|
|
223
|
+
if (await fs2.pathExists(path2.join(agentsBase, sub))) {
|
|
224
|
+
present.push(sub);
|
|
225
|
+
}
|
|
490
226
|
}
|
|
491
|
-
|
|
492
|
-
|
|
227
|
+
return present;
|
|
228
|
+
}
|
|
229
|
+
async function installAgentSetupScripts(agentsBase, scope, linkMap) {
|
|
230
|
+
const hooksDir = path2.join(agentsBase, "hooks");
|
|
231
|
+
await fs2.ensureDir(hooksDir);
|
|
232
|
+
const agentsDirToken = scope === "project" ? ".agents" : agentsBase;
|
|
233
|
+
const shPath = path2.join(hooksDir, "agent-setup.sh");
|
|
234
|
+
const ps1Path = path2.join(hooksDir, "agent-setup.ps1");
|
|
235
|
+
for (const [src, dest] of [
|
|
236
|
+
["agent-setup.sh", shPath],
|
|
237
|
+
["agent-setup.ps1", ps1Path]
|
|
238
|
+
]) {
|
|
239
|
+
let content = await fs2.readFile(
|
|
240
|
+
path2.join(AGENT_SETUP_SCRIPTS_DIR, src),
|
|
241
|
+
"utf-8"
|
|
242
|
+
);
|
|
243
|
+
content = content.replace(/__AGENTS_DIR__/g, agentsDirToken).replace(/^__LINK_MAP__$/m, linkMap);
|
|
244
|
+
await fs2.writeFile(dest, content);
|
|
493
245
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
function
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
246
|
+
await fs2.chmod(shPath, 493);
|
|
247
|
+
return { shPath, ps1Path };
|
|
248
|
+
}
|
|
249
|
+
function jsonEntryEquals(a, b) {
|
|
250
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
251
|
+
}
|
|
252
|
+
function ensureArray(value) {
|
|
253
|
+
return Array.isArray(value) ? value : [];
|
|
254
|
+
}
|
|
255
|
+
function mergeJsonSessionStart(existing, hookKey, entry) {
|
|
256
|
+
const next = { ...existing };
|
|
257
|
+
const hooks = { ...next.hooks ?? {} };
|
|
258
|
+
const list2 = ensureArray(hooks[hookKey]);
|
|
259
|
+
if (!list2.some((item) => jsonEntryEquals(item, entry))) {
|
|
260
|
+
list2.push(entry);
|
|
261
|
+
}
|
|
262
|
+
hooks[hookKey] = list2;
|
|
263
|
+
next.hooks = hooks;
|
|
264
|
+
return next;
|
|
265
|
+
}
|
|
266
|
+
function buildClaudeCodeEntry(command) {
|
|
267
|
+
return { hooks: [{ type: "command", command }] };
|
|
268
|
+
}
|
|
269
|
+
function buildGeminiEntry(command) {
|
|
270
|
+
return { matcher: "startup", hooks: [{ type: "command", command }] };
|
|
271
|
+
}
|
|
272
|
+
function buildCopilotEntry(command) {
|
|
273
|
+
return { type: "command", bash: command, timeoutSec: 30 };
|
|
274
|
+
}
|
|
275
|
+
function buildCodexBlock(command) {
|
|
276
|
+
return [
|
|
277
|
+
"[[hooks.SessionStart]]",
|
|
278
|
+
'matcher = "startup"',
|
|
279
|
+
"",
|
|
280
|
+
"[[hooks.SessionStart.hooks]]",
|
|
281
|
+
'type = "command"',
|
|
282
|
+
`command = ${JSON.stringify(command)}`,
|
|
283
|
+
""
|
|
284
|
+
].join("\n");
|
|
285
|
+
}
|
|
286
|
+
function codexBlockAlreadyPresent(existing, command) {
|
|
287
|
+
return existing.includes(`command = ${JSON.stringify(command)}`);
|
|
288
|
+
}
|
|
289
|
+
async function backupOnce(file) {
|
|
290
|
+
if (await fs2.pathExists(file) && !await fs2.pathExists(`${file}.bak`)) {
|
|
291
|
+
await fs2.copy(file, `${file}.bak`);
|
|
513
292
|
}
|
|
514
|
-
|
|
515
|
-
|
|
293
|
+
}
|
|
294
|
+
async function readJsonFile(file) {
|
|
295
|
+
if (!await fs2.pathExists(file)) return {};
|
|
296
|
+
const raw = await fs2.readFile(file, "utf-8");
|
|
297
|
+
const trimmed = raw.trim();
|
|
298
|
+
if (!trimmed) return {};
|
|
299
|
+
return JSON.parse(trimmed);
|
|
300
|
+
}
|
|
301
|
+
async function mergeJsonHookFile(file, hookKey, entry) {
|
|
302
|
+
await fs2.ensureDir(path2.dirname(file));
|
|
303
|
+
await backupOnce(file);
|
|
304
|
+
const existing = await readJsonFile(file);
|
|
305
|
+
const merged = mergeJsonSessionStart(existing, hookKey, entry);
|
|
306
|
+
await fs2.writeFile(file, `${JSON.stringify(merged, null, " ")}
|
|
307
|
+
`);
|
|
308
|
+
}
|
|
309
|
+
async function mergeCodexHookFile(file, command) {
|
|
310
|
+
await fs2.ensureDir(path2.dirname(file));
|
|
311
|
+
await backupOnce(file);
|
|
312
|
+
const existing = await fs2.pathExists(file) ? await fs2.readFile(file, "utf-8") : "";
|
|
313
|
+
if (codexBlockAlreadyPresent(existing, command)) return;
|
|
314
|
+
const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
315
|
+
await fs2.writeFile(file, `${existing}${sep}
|
|
316
|
+
${buildCodexBlock(command)}`);
|
|
317
|
+
}
|
|
318
|
+
function hookConfigFiles(scope, root) {
|
|
319
|
+
const base = scope === "project" ? root : os2.homedir();
|
|
320
|
+
const j = (...p) => path2.join(base, ...p);
|
|
321
|
+
const targets = [];
|
|
322
|
+
const claudeFile = j(".claude", "settings.json");
|
|
323
|
+
targets.push({
|
|
324
|
+
platform: "claude-code",
|
|
325
|
+
file: claudeFile,
|
|
326
|
+
merge: (command) => mergeJsonHookFile(
|
|
327
|
+
claudeFile,
|
|
328
|
+
"SessionStart",
|
|
329
|
+
buildClaudeCodeEntry(command)
|
|
330
|
+
)
|
|
331
|
+
});
|
|
332
|
+
const geminiFile = j(".gemini", "settings.json");
|
|
333
|
+
targets.push({
|
|
334
|
+
platform: "gemini",
|
|
335
|
+
file: geminiFile,
|
|
336
|
+
merge: (command) => mergeJsonHookFile(geminiFile, "SessionStart", buildGeminiEntry(command))
|
|
337
|
+
});
|
|
338
|
+
const codexFile = j(".codex", "config.toml");
|
|
339
|
+
targets.push({
|
|
340
|
+
platform: "codex",
|
|
341
|
+
file: codexFile,
|
|
342
|
+
merge: (command) => mergeCodexHookFile(codexFile, command)
|
|
343
|
+
});
|
|
344
|
+
const copilotFile = scope === "project" ? j(".github", "hooks", "agent-setup.json") : j(".copilot", "hooks", "agent-setup.json");
|
|
345
|
+
targets.push({
|
|
346
|
+
platform: "copilot",
|
|
347
|
+
file: copilotFile,
|
|
348
|
+
merge: (command) => mergeJsonHookFile(
|
|
349
|
+
copilotFile,
|
|
350
|
+
"sessionStart",
|
|
351
|
+
buildCopilotEntry(command)
|
|
352
|
+
)
|
|
353
|
+
});
|
|
354
|
+
return targets;
|
|
355
|
+
}
|
|
356
|
+
function expandSharedConfigPlatforms(platforms) {
|
|
357
|
+
return platforms.includes("antigravity") ? [...platforms, "gemini"] : platforms;
|
|
358
|
+
}
|
|
359
|
+
async function wireSessionStartHooks(scope, root, command, platforms) {
|
|
360
|
+
const targets = hookConfigFiles(scope, root);
|
|
361
|
+
const selected = platforms && expandSharedConfigPlatforms(platforms);
|
|
362
|
+
const wired = [];
|
|
363
|
+
for (const target of targets) {
|
|
364
|
+
if (selected && !selected.includes(target.platform)) continue;
|
|
365
|
+
await target.merge(command);
|
|
366
|
+
wired.push(target.platform);
|
|
367
|
+
}
|
|
368
|
+
return wired;
|
|
369
|
+
}
|
|
370
|
+
function runAgentSetupScript(shPath) {
|
|
371
|
+
if (process.platform === "win32") return;
|
|
372
|
+
execFileSync("bash", [shPath], { stdio: "ignore" });
|
|
373
|
+
}
|
|
374
|
+
async function wireAgentSetup(agentsBase, scope, root, platforms) {
|
|
375
|
+
const present = await presentLinkSubdirs(agentsBase);
|
|
376
|
+
const linkMap = buildLinkMap(scope, root, present, platforms);
|
|
377
|
+
const { shPath } = await installAgentSetupScripts(agentsBase, scope, linkMap);
|
|
378
|
+
const command = scope === "project" ? path2.join(".agents", "hooks", "agent-setup.sh").replace(/\\/g, "/") : shPath;
|
|
379
|
+
const wiredPlatforms = await wireSessionStartHooks(
|
|
380
|
+
scope,
|
|
381
|
+
root,
|
|
382
|
+
command,
|
|
383
|
+
platforms
|
|
384
|
+
);
|
|
385
|
+
let ranScript = false;
|
|
386
|
+
if (process.platform !== "win32") {
|
|
387
|
+
runAgentSetupScript(shPath);
|
|
388
|
+
ranScript = true;
|
|
516
389
|
}
|
|
517
|
-
|
|
518
|
-
return `description = ${JSON.stringify(description)}
|
|
519
|
-
|
|
520
|
-
prompt = """
|
|
521
|
-
${escapedContent.trim()}
|
|
522
|
-
"""
|
|
523
|
-
`;
|
|
390
|
+
return { wiredPlatforms, ranScript };
|
|
524
391
|
}
|
|
525
392
|
|
|
526
|
-
// src/utils/
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
393
|
+
// src/utils/github.ts
|
|
394
|
+
import os3 from "node:os";
|
|
395
|
+
import path3 from "node:path";
|
|
396
|
+
import fs3 from "fs-extra";
|
|
397
|
+
async function fetchSkillFromGitHub(url) {
|
|
398
|
+
const regex = /github\.com\/([^/]+)\/([^/]+)\/(?:tree|blob)\/([^/]+)\/(.+)/;
|
|
399
|
+
const match = url.match(regex);
|
|
400
|
+
if (!match) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
"Invalid GitHub URL. Format: https://github.com/owner/repo/tree/branch/path/to/skill or blob/branch/path/to/file"
|
|
403
|
+
);
|
|
534
404
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
405
|
+
const [, owner, repo, ref, skillPath] = match;
|
|
406
|
+
const skillName = path3.basename(skillPath);
|
|
407
|
+
const initialApiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${skillPath}?ref=${ref}`;
|
|
408
|
+
const tempDir = path3.join(os3.tmpdir(), "ai-agents-install", skillName);
|
|
409
|
+
await fs3.ensureDir(tempDir);
|
|
410
|
+
await fs3.emptyDir(tempDir);
|
|
411
|
+
let isFile = false;
|
|
412
|
+
async function downloadDirectory(apiUrl, localDir) {
|
|
413
|
+
const response = await fetch(apiUrl, {
|
|
414
|
+
headers: {
|
|
415
|
+
"User-Agent": "ai-agents-cli",
|
|
416
|
+
Accept: "application/vnd.github.v3+json"
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`Failed to fetch from GitHub (${response.status}): ${response.statusText}`
|
|
422
|
+
);
|
|
538
423
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
"glob",
|
|
550
|
-
"grep_search",
|
|
551
|
-
"replace",
|
|
552
|
-
"run_shell_command",
|
|
553
|
-
"google_web_search",
|
|
554
|
-
"web_fetch",
|
|
555
|
-
"ask_user",
|
|
556
|
-
"save_memory",
|
|
557
|
-
"activate_skill"
|
|
558
|
-
]
|
|
559
|
-
};
|
|
560
|
-
let body = content;
|
|
561
|
-
if (match) {
|
|
562
|
-
try {
|
|
563
|
-
const parsed = yaml4.load(match[1]);
|
|
564
|
-
frontmatter = { ...frontmatter, ...parsed };
|
|
565
|
-
frontmatter.tools = [
|
|
566
|
-
"list_directory",
|
|
567
|
-
"read_file",
|
|
568
|
-
"write_file",
|
|
569
|
-
"glob",
|
|
570
|
-
"grep_search",
|
|
571
|
-
"replace",
|
|
572
|
-
"run_shell_command",
|
|
573
|
-
"google_web_search",
|
|
574
|
-
"web_fetch",
|
|
575
|
-
"ask_user",
|
|
576
|
-
"save_memory",
|
|
577
|
-
"activate_skill"
|
|
578
|
-
];
|
|
579
|
-
delete frontmatter.skills;
|
|
580
|
-
body = content.replace(frontmatterRegex3, "").trim();
|
|
581
|
-
} catch (_) {
|
|
424
|
+
const data = await response.json();
|
|
425
|
+
if (!Array.isArray(data) && data.type === "file") {
|
|
426
|
+
isFile = true;
|
|
427
|
+
const item = data;
|
|
428
|
+
if (item.download_url) {
|
|
429
|
+
const fileContentResponse = await fetch(item.download_url);
|
|
430
|
+
if (!fileContentResponse.ok) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Failed to download ${item.name}: ${fileContentResponse.statusText}`
|
|
433
|
+
);
|
|
582
434
|
}
|
|
435
|
+
const content = await fileContentResponse.text();
|
|
436
|
+
await fs3.writeFile(path3.join(localDir, item.name), content);
|
|
583
437
|
}
|
|
584
|
-
|
|
585
|
-
return `---
|
|
586
|
-
${yamlStr}---
|
|
587
|
-
|
|
588
|
-
${body}`;
|
|
438
|
+
return;
|
|
589
439
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
440
|
+
if (!Array.isArray(data)) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
"Invalid response from GitHub API. Expected directory listing or file object."
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const files = data;
|
|
446
|
+
for (const item of files) {
|
|
447
|
+
if (item.type === "file" && item.download_url) {
|
|
448
|
+
const fileContentResponse = await fetch(item.download_url);
|
|
449
|
+
if (!fileContentResponse.ok) {
|
|
450
|
+
console.warn(
|
|
451
|
+
`Failed to download ${item.name}: ${fileContentResponse.statusText}`
|
|
452
|
+
);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const content = await fileContentResponse.text();
|
|
456
|
+
await fs3.writeFile(path3.join(localDir, item.name), content);
|
|
457
|
+
} else if (item.type === "dir") {
|
|
458
|
+
const newLocalDir = path3.join(localDir, item.name);
|
|
459
|
+
await fs3.ensureDir(newLocalDir);
|
|
460
|
+
await downloadDirectory(item.url, newLocalDir);
|
|
461
|
+
}
|
|
601
462
|
}
|
|
602
|
-
return itemName;
|
|
603
|
-
}
|
|
604
|
-
transform(content, _type, _itemName) {
|
|
605
|
-
return content;
|
|
606
463
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
// src/utils/platforms/index.ts
|
|
610
|
-
var handlers = /* @__PURE__ */ new Map();
|
|
611
|
-
function registerHandler(handler) {
|
|
612
|
-
handlers.set(handler.platform, handler);
|
|
613
|
-
}
|
|
614
|
-
registerHandler(new AntigravityHandler());
|
|
615
|
-
registerHandler(new ClaudeCodeHandler());
|
|
616
|
-
registerHandler(new CodexHandler());
|
|
617
|
-
registerHandler(new CopilotHandler());
|
|
618
|
-
registerHandler(new GeminiHandler());
|
|
619
|
-
registerHandler(new WindsurfHandler());
|
|
620
|
-
function getHandler(platform) {
|
|
621
|
-
return handlers.get(platform) || new DefaultHandler(platform);
|
|
464
|
+
await downloadDirectory(initialApiUrl, tempDir);
|
|
465
|
+
return { tempDir, skillName, isFile };
|
|
622
466
|
}
|
|
623
467
|
|
|
624
468
|
// src/utils/prompts.ts
|
|
@@ -743,11 +587,11 @@ import { isCancel, note, select } from "@clack/prompts";
|
|
|
743
587
|
import pc from "picocolors";
|
|
744
588
|
|
|
745
589
|
// src/utils/scope.ts
|
|
746
|
-
import { execFileSync } from "node:child_process";
|
|
747
|
-
import
|
|
590
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
591
|
+
import os4 from "node:os";
|
|
748
592
|
function isInsideGitWorkTree(cwd) {
|
|
749
593
|
try {
|
|
750
|
-
const out =
|
|
594
|
+
const out = execFileSync2("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
751
595
|
cwd,
|
|
752
596
|
stdio: ["ignore", "pipe", "ignore"],
|
|
753
597
|
encoding: "utf-8"
|
|
@@ -760,9 +604,9 @@ function isInsideGitWorkTree(cwd) {
|
|
|
760
604
|
function resolveScope(scope, options = {}) {
|
|
761
605
|
const cwd = options.cwd ?? process.cwd();
|
|
762
606
|
if (scope === "global") {
|
|
763
|
-
return { ok: true, scope: "global", root: options.homedir ??
|
|
607
|
+
return { ok: true, scope: "global", root: options.homedir ?? os4.homedir() };
|
|
764
608
|
}
|
|
765
|
-
const home = options.homedir ??
|
|
609
|
+
const home = options.homedir ?? os4.homedir();
|
|
766
610
|
if (cwd === home) {
|
|
767
611
|
return { ok: false, reason: "homedir" };
|
|
768
612
|
}
|
|
@@ -931,26 +775,6 @@ async function chooseListRemoveScope(options) {
|
|
|
931
775
|
|
|
932
776
|
// src/commands/add.ts
|
|
933
777
|
enableAutocompleteMultiSelectShiftAToggle();
|
|
934
|
-
function getPlatformOptions(type) {
|
|
935
|
-
const paths = getTargetPaths(type);
|
|
936
|
-
return Object.entries(paths).filter(([_, pathStr]) => !!pathStr).map(([platform, pathStr]) => ({
|
|
937
|
-
label: PLATFORM_LABELS[platform],
|
|
938
|
-
value: platform,
|
|
939
|
-
hint: pathStr.replace(os4.homedir(), "~")
|
|
940
|
-
}));
|
|
941
|
-
}
|
|
942
|
-
async function installItem(itemName, overwrite, sourcePath, targetBaseDir) {
|
|
943
|
-
const targetPath = path6.join(targetBaseDir, itemName);
|
|
944
|
-
if (!await fs4.pathExists(sourcePath)) {
|
|
945
|
-
throw new Error(`Item '${itemName}' not found at ${sourcePath}`);
|
|
946
|
-
}
|
|
947
|
-
if (!overwrite && await fs4.pathExists(targetPath)) {
|
|
948
|
-
return false;
|
|
949
|
-
}
|
|
950
|
-
await fs4.ensureDir(targetBaseDir);
|
|
951
|
-
await fs4.copy(sourcePath, targetPath, { overwrite });
|
|
952
|
-
return true;
|
|
953
|
-
}
|
|
954
778
|
async function add(type, url, options) {
|
|
955
779
|
if (!options?.skipIntro) {
|
|
956
780
|
intro(pc2.bgCyan(pc2.black(" AI Manager : Add ")));
|
|
@@ -980,6 +804,7 @@ async function add(type, url, options) {
|
|
|
980
804
|
return;
|
|
981
805
|
}
|
|
982
806
|
let tempDir = null;
|
|
807
|
+
let isGitHubFile = false;
|
|
983
808
|
let selectedItems = [];
|
|
984
809
|
try {
|
|
985
810
|
if (url) {
|
|
@@ -988,6 +813,7 @@ async function add(type, url, options) {
|
|
|
988
813
|
try {
|
|
989
814
|
const result = await fetchSkillFromGitHub(url);
|
|
990
815
|
tempDir = result.tempDir;
|
|
816
|
+
isGitHubFile = result.isFile;
|
|
991
817
|
selectedItems = [result.skillName];
|
|
992
818
|
s2.stop(pc2.green(`Fetched ${normalizedType}: ${result.skillName}`));
|
|
993
819
|
} catch (e) {
|
|
@@ -1022,24 +848,6 @@ async function add(type, url, options) {
|
|
|
1022
848
|
}
|
|
1023
849
|
selectedItems = items;
|
|
1024
850
|
}
|
|
1025
|
-
const currentPlatformOptions = getPlatformOptions(normalizedType);
|
|
1026
|
-
if (currentPlatformOptions.length === 0) {
|
|
1027
|
-
cancel(`No supported platforms found for type '${normalizedType}'.`);
|
|
1028
|
-
process.exit(1);
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
const platforms = await multiselect({
|
|
1032
|
-
message: "Select target platforms:",
|
|
1033
|
-
options: currentPlatformOptions,
|
|
1034
|
-
required: true
|
|
1035
|
-
});
|
|
1036
|
-
if (isCancel2(platforms)) {
|
|
1037
|
-
if (tempDir) await fs4.remove(tempDir);
|
|
1038
|
-
if (isSingleShot) break;
|
|
1039
|
-
currentType = void 0;
|
|
1040
|
-
continue;
|
|
1041
|
-
}
|
|
1042
|
-
const selectedPlatforms = platforms;
|
|
1043
851
|
const scopeChoice = await chooseInstallScope({ flag: options?.scope });
|
|
1044
852
|
if ("rejected" in scopeChoice) {
|
|
1045
853
|
if (tempDir) await fs4.remove(tempDir);
|
|
@@ -1056,11 +864,9 @@ async function add(type, url, options) {
|
|
|
1056
864
|
continue;
|
|
1057
865
|
}
|
|
1058
866
|
const { scope: chosenScope, root: scopeRoot } = scopeChoice;
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
scopeRoot
|
|
1063
|
-
);
|
|
867
|
+
const agentsBase = getAgentsBase(chosenScope, scopeRoot);
|
|
868
|
+
const subdir = TYPE_SUBDIRS[normalizedType] ?? "skills";
|
|
869
|
+
const targetBase = path4.join(agentsBase, subdir);
|
|
1064
870
|
if (chosenScope === "project") {
|
|
1065
871
|
const proceed = await confirm({
|
|
1066
872
|
message: `Install to project scope at ${pc2.cyan(scopeRoot)}?`,
|
|
@@ -1073,116 +879,66 @@ async function add(type, url, options) {
|
|
|
1073
879
|
continue;
|
|
1074
880
|
}
|
|
1075
881
|
}
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
const targetItemName = handler.getTargetFileName(item, normalizedType);
|
|
1083
|
-
const targetPath = path6.join(targetBase, targetItemName);
|
|
1084
|
-
if (await fs4.pathExists(targetPath)) {
|
|
1085
|
-
existingItems.push({ item, platform });
|
|
1086
|
-
}
|
|
882
|
+
const subdirName = subdir;
|
|
883
|
+
const preExisting = [];
|
|
884
|
+
for (const item of selectedItems) {
|
|
885
|
+
const targetName = normalizedType === "skill" ? path4.parse(item).name : item;
|
|
886
|
+
if (await fs4.pathExists(path4.join(targetBase, targetName))) {
|
|
887
|
+
preExisting.push({ entry: targetName, subdir: subdirName });
|
|
1087
888
|
}
|
|
1088
889
|
}
|
|
1089
|
-
|
|
1090
|
-
if (
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
note2(message, "Attention");
|
|
1096
|
-
const shouldOverwrite = await confirm({
|
|
1097
|
-
message: "Do you want to overwrite existing items?",
|
|
1098
|
-
initialValue: false
|
|
890
|
+
const skipEntries = /* @__PURE__ */ new Set();
|
|
891
|
+
if (preExisting.length > 0) {
|
|
892
|
+
const toOverwrite = await multiselect({
|
|
893
|
+
message: "These entries already exist in .agents. Choose which to OVERWRITE (unselected entries are kept and skipped):",
|
|
894
|
+
options: preExisting.map((p) => ({ label: p.entry, value: p.entry })),
|
|
895
|
+
required: false
|
|
1099
896
|
});
|
|
1100
|
-
if (isCancel2(
|
|
897
|
+
if (isCancel2(toOverwrite)) {
|
|
1101
898
|
if (tempDir) await fs4.remove(tempDir);
|
|
1102
899
|
if (isSingleShot) break;
|
|
1103
900
|
currentType = void 0;
|
|
1104
901
|
continue;
|
|
1105
902
|
}
|
|
1106
|
-
|
|
903
|
+
const keep = new Set(toOverwrite);
|
|
904
|
+
for (const { entry } of preExisting) {
|
|
905
|
+
if (!keep.has(entry)) skipEntries.add(entry);
|
|
906
|
+
}
|
|
1107
907
|
}
|
|
1108
908
|
note2(
|
|
1109
|
-
`Installing ${selectedItems.length} ${normalizedType}s to ${
|
|
909
|
+
`Installing ${selectedItems.length} ${normalizedType}s to ${targetBase} (scope: ${chosenScope})...`,
|
|
1110
910
|
"Summary"
|
|
1111
911
|
);
|
|
1112
912
|
const s = spinner();
|
|
1113
913
|
s.start("Installing...");
|
|
1114
914
|
const errors = [];
|
|
1115
915
|
let installedCount = 0;
|
|
1116
|
-
|
|
1117
|
-
for (const
|
|
1118
|
-
const
|
|
1119
|
-
if (
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
if (url && tempDir) {
|
|
1126
|
-
const potentialFile = path6.join(tempDir, item);
|
|
1127
|
-
if (await fs4.pathExists(potentialFile) && (await fs4.stat(potentialFile)).isFile()) {
|
|
1128
|
-
currentSourcePath = potentialFile;
|
|
1129
|
-
} else {
|
|
1130
|
-
currentSourcePath = tempDir;
|
|
1131
|
-
}
|
|
1132
|
-
} else {
|
|
1133
|
-
const src = path6.join(TYPE_DIRS[normalizedType], item);
|
|
1134
|
-
if ((await fs4.stat(src)).isFile()) {
|
|
1135
|
-
currentSourcePath = src;
|
|
1136
|
-
} else {
|
|
1137
|
-
currentSourcePath = src;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
const handler = getHandler(platform);
|
|
1141
|
-
const targetItemName = handler.getTargetFileName(
|
|
1142
|
-
item,
|
|
1143
|
-
normalizedType
|
|
1144
|
-
);
|
|
1145
|
-
if ((normalizedType === "agent" || normalizedType === "workflow") && currentSourcePath.endsWith(".md")) {
|
|
1146
|
-
const targetPath = path6.join(targetBase, targetItemName);
|
|
1147
|
-
if (!overwrite && await fs4.pathExists(targetPath)) {
|
|
1148
|
-
skippedCount++;
|
|
1149
|
-
} else {
|
|
1150
|
-
let content = await fs4.readFile(currentSourcePath, "utf-8");
|
|
1151
|
-
content = handler.transform(
|
|
1152
|
-
content,
|
|
1153
|
-
normalizedType,
|
|
1154
|
-
path6.parse(item).name
|
|
1155
|
-
);
|
|
1156
|
-
await fs4.ensureDir(path6.dirname(targetPath));
|
|
1157
|
-
await fs4.writeFile(targetPath, content);
|
|
1158
|
-
installedCount++;
|
|
1159
|
-
}
|
|
1160
|
-
continue;
|
|
1161
|
-
}
|
|
1162
|
-
const installed = await installItem(
|
|
1163
|
-
targetItemName,
|
|
1164
|
-
overwrite,
|
|
1165
|
-
currentSourcePath,
|
|
1166
|
-
targetBase
|
|
1167
|
-
);
|
|
1168
|
-
if (installed) {
|
|
1169
|
-
installedCount++;
|
|
1170
|
-
} else {
|
|
1171
|
-
skippedCount++;
|
|
1172
|
-
}
|
|
1173
|
-
} catch (err) {
|
|
1174
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1175
|
-
errors.push(`${item} -> ${platform}: ${errorMessage}`);
|
|
916
|
+
await fs4.ensureDir(targetBase);
|
|
917
|
+
for (const item of selectedItems) {
|
|
918
|
+
const targetName = normalizedType === "skill" ? path4.parse(item).name : item;
|
|
919
|
+
if (skipEntries.has(targetName)) continue;
|
|
920
|
+
s.message(`Installing ${pc2.bold(item)}...`);
|
|
921
|
+
try {
|
|
922
|
+
const sourcePath = url && tempDir ? isGitHubFile ? path4.join(tempDir, item) : tempDir : path4.join(TYPE_DIRS[normalizedType], item);
|
|
923
|
+
if (!await fs4.pathExists(sourcePath)) {
|
|
924
|
+
throw new Error(`Item '${item}' not found at ${sourcePath}`);
|
|
1176
925
|
}
|
|
926
|
+
const targetPath = path4.join(targetBase, targetName);
|
|
927
|
+
await fs4.copy(sourcePath, targetPath, { overwrite: true });
|
|
928
|
+
installedCount++;
|
|
929
|
+
} catch (err) {
|
|
930
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
931
|
+
errors.push(`${item}: ${errorMessage}`);
|
|
1177
932
|
}
|
|
1178
933
|
}
|
|
1179
934
|
if (tempDir) {
|
|
1180
935
|
await fs4.remove(tempDir);
|
|
936
|
+
tempDir = null;
|
|
1181
937
|
}
|
|
1182
938
|
if (errors.length > 0) {
|
|
1183
939
|
s.stop(
|
|
1184
940
|
pc2.yellow(
|
|
1185
|
-
`Completed with errors. Installed: ${installedCount},
|
|
941
|
+
`Completed with errors. Installed: ${installedCount}, Errors: ${errors.length}`
|
|
1186
942
|
)
|
|
1187
943
|
);
|
|
1188
944
|
console.error(pc2.red("\nErrors encountered:"));
|
|
@@ -1192,10 +948,13 @@ async function add(type, url, options) {
|
|
|
1192
948
|
} else {
|
|
1193
949
|
s.stop(
|
|
1194
950
|
pc2.green(
|
|
1195
|
-
`Successfully installed ${installedCount} ${normalizedType}s
|
|
951
|
+
`Successfully installed ${installedCount} ${normalizedType}s.`
|
|
1196
952
|
)
|
|
1197
953
|
);
|
|
1198
954
|
}
|
|
955
|
+
if (installedCount > 0) {
|
|
956
|
+
await maybeWireAgentSetup(agentsBase, chosenScope, scopeRoot);
|
|
957
|
+
}
|
|
1199
958
|
} catch (error) {
|
|
1200
959
|
if (tempDir) await fs4.remove(tempDir);
|
|
1201
960
|
cancel(`An error occurred: ${error}`);
|
|
@@ -1209,9 +968,66 @@ async function add(type, url, options) {
|
|
|
1209
968
|
outro("You're all set!");
|
|
1210
969
|
}
|
|
1211
970
|
}
|
|
971
|
+
async function maybeWireAgentSetup(agentsBase, scope, root) {
|
|
972
|
+
const wire = await confirm({
|
|
973
|
+
message: "Wire the agent-setup session-start hook now?",
|
|
974
|
+
initialValue: true
|
|
975
|
+
});
|
|
976
|
+
if (isCancel2(wire) || !wire) return;
|
|
977
|
+
const selected = await multiselect({
|
|
978
|
+
message: "Which agents should be wired?",
|
|
979
|
+
options: Object.keys(PLATFORM_LABELS).map((platform) => ({
|
|
980
|
+
label: PLATFORM_LABELS[platform],
|
|
981
|
+
value: platform
|
|
982
|
+
})),
|
|
983
|
+
required: true
|
|
984
|
+
});
|
|
985
|
+
if (isCancel2(selected)) return;
|
|
986
|
+
const platforms = selected;
|
|
987
|
+
const resolved = await resolveConflicts(agentsBase, platforms, scope, root);
|
|
988
|
+
if (resolved === "cancelled") return;
|
|
989
|
+
const s = spinner();
|
|
990
|
+
s.start("Wiring agent-setup hook...");
|
|
991
|
+
try {
|
|
992
|
+
const result = await wireAgentSetup(agentsBase, scope, root, platforms);
|
|
993
|
+
s.stop(
|
|
994
|
+
pc2.green(
|
|
995
|
+
`Wired session-start hook for: ${result.wiredPlatforms.join(", ")}.`
|
|
996
|
+
)
|
|
997
|
+
);
|
|
998
|
+
if (platforms.includes("windsurf")) {
|
|
999
|
+
note2(
|
|
1000
|
+
"Windsurf has no session-start hook; its symlinks are created once now. Re-run .agents/hooks/agent-setup.sh to refresh.",
|
|
1001
|
+
"Note"
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1006
|
+
s.error(`Failed to wire agent-setup hook: ${errorMessage}`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
async function resolveConflicts(agentsBase, platforms, scope, root) {
|
|
1010
|
+
const conflicts = await detectConflicts(agentsBase, platforms, scope, root);
|
|
1011
|
+
if (conflicts.length === 0) return "ok";
|
|
1012
|
+
const overwrite = await multiselect({
|
|
1013
|
+
message: "These entries already exist in the selected agent dirs. Choose which to OVERWRITE (unselected entries are dropped from .agents):",
|
|
1014
|
+
options: conflicts.map((c) => ({ label: c.entry, value: c.entry })),
|
|
1015
|
+
required: false
|
|
1016
|
+
});
|
|
1017
|
+
if (isCancel2(overwrite)) return "cancelled";
|
|
1018
|
+
const toOverwrite = new Set(overwrite);
|
|
1019
|
+
for (const conflict of conflicts) {
|
|
1020
|
+
if (toOverwrite.has(conflict.entry)) {
|
|
1021
|
+
await removeAgentDirEntries(conflict.targetPaths);
|
|
1022
|
+
} else {
|
|
1023
|
+
await dropAgentsEntry(agentsBase, conflict.subdir, conflict.entry);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return "ok";
|
|
1027
|
+
}
|
|
1212
1028
|
|
|
1213
1029
|
// src/commands/import.ts
|
|
1214
|
-
import
|
|
1030
|
+
import path5 from "node:path";
|
|
1215
1031
|
import {
|
|
1216
1032
|
cancel as cancel2,
|
|
1217
1033
|
confirm as confirm2,
|
|
@@ -1280,12 +1096,12 @@ async function importItem(type, url, options) {
|
|
|
1280
1096
|
throw e;
|
|
1281
1097
|
}
|
|
1282
1098
|
const shouldFlatten = (normalizedType === "agent" || normalizedType === "workflow") && !isFile;
|
|
1283
|
-
const targetPath = shouldFlatten ? targetBaseDir :
|
|
1099
|
+
const targetPath = shouldFlatten ? targetBaseDir : path5.join(targetBaseDir, itemName);
|
|
1284
1100
|
if (shouldFlatten) {
|
|
1285
1101
|
const srcFiles = await fs5.readdir(tempDir);
|
|
1286
1102
|
const conflicts = [];
|
|
1287
1103
|
for (const file of srcFiles) {
|
|
1288
|
-
if (await fs5.pathExists(
|
|
1104
|
+
if (await fs5.pathExists(path5.join(targetBaseDir, file))) {
|
|
1289
1105
|
conflicts.push(file);
|
|
1290
1106
|
}
|
|
1291
1107
|
}
|
|
@@ -1316,7 +1132,7 @@ async function importItem(type, url, options) {
|
|
|
1316
1132
|
s.start(`Importing ${itemName} to ${targetBaseDir}...`);
|
|
1317
1133
|
await fs5.ensureDir(targetBaseDir);
|
|
1318
1134
|
if (isFile) {
|
|
1319
|
-
await fs5.copy(
|
|
1135
|
+
await fs5.copy(path5.join(tempDir, itemName), targetPath, {
|
|
1320
1136
|
overwrite: true
|
|
1321
1137
|
});
|
|
1322
1138
|
} else {
|
|
@@ -1341,68 +1157,40 @@ async function importItem(type, url, options) {
|
|
|
1341
1157
|
|
|
1342
1158
|
// src/commands/list.ts
|
|
1343
1159
|
import os5 from "node:os";
|
|
1160
|
+
import path6 from "node:path";
|
|
1344
1161
|
import { cancel as cancel3, intro as intro3, isCancel as isCancel4, outro as outro3, select as select4 } from "@clack/prompts";
|
|
1345
1162
|
import fs6 from "fs-extra";
|
|
1346
1163
|
import pc4 from "picocolors";
|
|
1347
|
-
async function
|
|
1348
|
-
const
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
if (await codexEntryMatchesType(fullPath, entry, normalizedType)) {
|
|
1353
|
-
items.push(entry.name);
|
|
1354
|
-
}
|
|
1355
|
-
continue;
|
|
1356
|
-
}
|
|
1357
|
-
if (entry.isDirectory() || entry.isFile() && entry.name.endsWith(".md")) {
|
|
1358
|
-
items.push(entry.name);
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
items.sort();
|
|
1362
|
-
return items;
|
|
1164
|
+
async function collectEntries(subdirPath) {
|
|
1165
|
+
const entries = await fs6.readdir(subdirPath, { withFileTypes: true });
|
|
1166
|
+
return entries.filter(
|
|
1167
|
+
(entry) => entry.isDirectory() || entry.isFile() && entry.name.endsWith(".md")
|
|
1168
|
+
).map((entry) => entry.name).sort();
|
|
1363
1169
|
}
|
|
1364
|
-
function
|
|
1365
|
-
if (scope === "global") return
|
|
1366
|
-
|
|
1367
|
-
return rel;
|
|
1170
|
+
function displayBase(base, scope) {
|
|
1171
|
+
if (scope === "global") return base.replace(os5.homedir(), "~");
|
|
1172
|
+
return base;
|
|
1368
1173
|
}
|
|
1369
1174
|
async function listLocalForScopes(normalizedType, sections) {
|
|
1175
|
+
const subdir = TYPE_SUBDIRS[normalizedType];
|
|
1370
1176
|
console.log(pc4.bold(pc4.blue(`Installed ${normalizedType}s (Local):`)));
|
|
1371
1177
|
let foundAny = false;
|
|
1372
|
-
const platformsByOrder = [];
|
|
1373
1178
|
for (const section of sections) {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
platform,
|
|
1386
|
-
fullPath,
|
|
1387
|
-
normalizedType
|
|
1388
|
-
);
|
|
1389
|
-
if (items.length === 0) continue;
|
|
1390
|
-
foundAny = true;
|
|
1391
|
-
if (!printedHeader) {
|
|
1392
|
-
console.log(pc4.cyan(` ${platform}:`));
|
|
1393
|
-
printedHeader = true;
|
|
1179
|
+
const subdirPath = path6.join(section.base, subdir);
|
|
1180
|
+
if (!await fs6.pathExists(subdirPath)) continue;
|
|
1181
|
+
const items = await collectEntries(subdirPath);
|
|
1182
|
+
if (items.length === 0) continue;
|
|
1183
|
+
foundAny = true;
|
|
1184
|
+
if (sections.length > 1) {
|
|
1185
|
+
const scopeLabel = section.scope === "global" ? "Global" : "Project";
|
|
1186
|
+
const display = displayBase(section.base, section.scope);
|
|
1187
|
+
console.log(pc4.cyan(` ${scopeLabel} (${display}):`));
|
|
1188
|
+
for (const item of items) {
|
|
1189
|
+
console.log(` - ${item}`);
|
|
1394
1190
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
console.log(pc4.dim(` ${scopeLabel} (${display}):`));
|
|
1399
|
-
for (const item of items) {
|
|
1400
|
-
console.log(` - ${item}`);
|
|
1401
|
-
}
|
|
1402
|
-
} else {
|
|
1403
|
-
for (const item of items) {
|
|
1404
|
-
console.log(` - ${item}`);
|
|
1405
|
-
}
|
|
1191
|
+
} else {
|
|
1192
|
+
for (const item of items) {
|
|
1193
|
+
console.log(` - ${item}`);
|
|
1406
1194
|
}
|
|
1407
1195
|
}
|
|
1408
1196
|
}
|
|
@@ -1411,33 +1199,21 @@ async function listLocalForScopes(normalizedType, sections) {
|
|
|
1411
1199
|
}
|
|
1412
1200
|
console.log("");
|
|
1413
1201
|
}
|
|
1414
|
-
function buildSections(
|
|
1202
|
+
function buildSections(scope, projectRoot) {
|
|
1415
1203
|
const sections = [];
|
|
1416
1204
|
if (scope === "global" || scope === "both") {
|
|
1205
|
+
sections.push({ scope: "global", base: getAgentsBase("global") });
|
|
1206
|
+
}
|
|
1207
|
+
if ((scope === "project" || scope === "both") && projectRoot) {
|
|
1417
1208
|
sections.push({
|
|
1418
|
-
scope: "
|
|
1419
|
-
|
|
1420
|
-
paths: getTargetPaths(normalizedType, "global")
|
|
1209
|
+
scope: "project",
|
|
1210
|
+
base: getAgentsBase("project", projectRoot)
|
|
1421
1211
|
});
|
|
1422
1212
|
}
|
|
1423
|
-
if (scope === "project" || scope === "both") {
|
|
1424
|
-
if (projectRoot) {
|
|
1425
|
-
sections.push({
|
|
1426
|
-
scope: "project",
|
|
1427
|
-
root: projectRoot,
|
|
1428
|
-
paths: getTargetPaths(normalizedType, "project", projectRoot)
|
|
1429
|
-
});
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
1213
|
return sections;
|
|
1433
1214
|
}
|
|
1434
|
-
async function performListing(type
|
|
1215
|
+
async function performListing(type) {
|
|
1435
1216
|
const normalizedType = type.toLowerCase().endsWith("s") ? type.slice(0, -1) : type.toLowerCase();
|
|
1436
|
-
if (local) {
|
|
1437
|
-
throw new Error(
|
|
1438
|
-
"performListing should not be called for local mode; use listLocalForScopes"
|
|
1439
|
-
);
|
|
1440
|
-
}
|
|
1441
1217
|
if (!TYPE_DIRS[normalizedType]) {
|
|
1442
1218
|
console.error(
|
|
1443
1219
|
pc4.red(`Unknown type: ${type}. Supported types: skill, agent, workflow`)
|
|
@@ -1519,16 +1295,14 @@ async function list(type, options) {
|
|
|
1519
1295
|
for (const t of typesToList) {
|
|
1520
1296
|
const normalizedType = t.toLowerCase().endsWith("s") ? t.slice(0, -1) : t.toLowerCase();
|
|
1521
1297
|
const sections = buildSections(
|
|
1522
|
-
normalizedType,
|
|
1523
1298
|
scopeChoice.scope,
|
|
1524
|
-
scopeChoice.homedir,
|
|
1525
1299
|
scopeChoice.projectRoot
|
|
1526
1300
|
);
|
|
1527
1301
|
await listLocalForScopes(normalizedType, sections);
|
|
1528
1302
|
}
|
|
1529
1303
|
} else {
|
|
1530
1304
|
for (const t of typesToList) {
|
|
1531
|
-
await performListing(t
|
|
1305
|
+
await performListing(t);
|
|
1532
1306
|
}
|
|
1533
1307
|
}
|
|
1534
1308
|
if (isSingleShot) break;
|
|
@@ -1543,14 +1317,13 @@ async function list(type, options) {
|
|
|
1543
1317
|
}
|
|
1544
1318
|
|
|
1545
1319
|
// src/commands/remove.ts
|
|
1546
|
-
import
|
|
1320
|
+
import path7 from "node:path";
|
|
1547
1321
|
import {
|
|
1548
1322
|
autocompleteMultiselect as autocompleteMultiselect2,
|
|
1549
1323
|
cancel as cancel4,
|
|
1550
1324
|
confirm as confirm3,
|
|
1551
1325
|
intro as intro4,
|
|
1552
1326
|
isCancel as isCancel5,
|
|
1553
|
-
multiselect as multiselect2,
|
|
1554
1327
|
note as note3,
|
|
1555
1328
|
outro as outro4,
|
|
1556
1329
|
select as select5,
|
|
@@ -1559,57 +1332,83 @@ import {
|
|
|
1559
1332
|
import fs7 from "fs-extra";
|
|
1560
1333
|
import pc5 from "picocolors";
|
|
1561
1334
|
enableAutocompleteMultiSelectShiftAToggle();
|
|
1562
|
-
function
|
|
1335
|
+
function buildScopedBases(scope, homedir, projectRoot) {
|
|
1563
1336
|
const out = [];
|
|
1564
1337
|
if (scope === "global" || scope === "both") {
|
|
1565
1338
|
out.push({
|
|
1566
1339
|
scope: "global",
|
|
1567
|
-
|
|
1340
|
+
base: getAgentsBase("global"),
|
|
1341
|
+
root: homedir
|
|
1568
1342
|
});
|
|
1569
1343
|
}
|
|
1570
1344
|
if ((scope === "project" || scope === "both") && projectRoot) {
|
|
1571
1345
|
out.push({
|
|
1572
1346
|
scope: "project",
|
|
1573
|
-
|
|
1347
|
+
base: getAgentsBase("project", projectRoot),
|
|
1348
|
+
root: projectRoot
|
|
1574
1349
|
});
|
|
1575
1350
|
}
|
|
1576
1351
|
return out;
|
|
1577
1352
|
}
|
|
1578
|
-
async function scanInstalledItems(
|
|
1353
|
+
async function scanInstalledItems(scopedBases, subdir, normalizedType) {
|
|
1579
1354
|
const seen = /* @__PURE__ */ new Set();
|
|
1580
1355
|
const items = [];
|
|
1581
|
-
const showScopePrefix =
|
|
1582
|
-
for (const { scope,
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
if (!qualifies) continue;
|
|
1599
|
-
const key = showScopePrefix ? `${scope}:${entry.name}` : entry.name;
|
|
1600
|
-
if (seen.has(key)) continue;
|
|
1601
|
-
seen.add(key);
|
|
1602
|
-
items.push({
|
|
1603
|
-
key,
|
|
1604
|
-
name: entry.name,
|
|
1605
|
-
scope,
|
|
1606
|
-
label: showScopePrefix ? `[${scope}] ${entry.name}` : entry.name
|
|
1607
|
-
});
|
|
1608
|
-
}
|
|
1356
|
+
const showScopePrefix = scopedBases.length > 1;
|
|
1357
|
+
for (const { scope, base } of scopedBases) {
|
|
1358
|
+
const subdirPath = path7.join(base, subdir);
|
|
1359
|
+
if (!await fs7.pathExists(subdirPath)) continue;
|
|
1360
|
+
const entries = await fs7.readdir(subdirPath, { withFileTypes: true });
|
|
1361
|
+
for (const entry of entries) {
|
|
1362
|
+
const qualifies = entry.isDirectory() || normalizedType !== "skill" && entry.isFile() && entry.name.endsWith(".md");
|
|
1363
|
+
if (!qualifies) continue;
|
|
1364
|
+
const key = showScopePrefix ? `${scope}:${entry.name}` : entry.name;
|
|
1365
|
+
if (seen.has(key)) continue;
|
|
1366
|
+
seen.add(key);
|
|
1367
|
+
items.push({
|
|
1368
|
+
key,
|
|
1369
|
+
name: entry.name,
|
|
1370
|
+
scope,
|
|
1371
|
+
label: showScopePrefix ? `[${scope}] ${entry.name}` : entry.name
|
|
1372
|
+
});
|
|
1609
1373
|
}
|
|
1610
1374
|
}
|
|
1611
1375
|
return items.sort((a, b) => a.label.localeCompare(b.label));
|
|
1612
1376
|
}
|
|
1377
|
+
function getPlatformDirs(scope, root, normalizedType) {
|
|
1378
|
+
if (scope === "project") {
|
|
1379
|
+
switch (normalizedType) {
|
|
1380
|
+
case "agent":
|
|
1381
|
+
return getProjectPlatformPathsAgents(root);
|
|
1382
|
+
case "workflow":
|
|
1383
|
+
return getProjectPlatformPathsWorkflows(root);
|
|
1384
|
+
default:
|
|
1385
|
+
return getProjectPlatformPathsSkills(root);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
switch (normalizedType) {
|
|
1389
|
+
case "agent":
|
|
1390
|
+
return PLATFORM_PATHS_AGENTS;
|
|
1391
|
+
case "workflow":
|
|
1392
|
+
return PLATFORM_PATHS_WORKFLOWS;
|
|
1393
|
+
default:
|
|
1394
|
+
return PLATFORM_PATHS_SKILLS;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
async function pruneDanglingLinks(platformDirs, entry, removedAgentsPath) {
|
|
1398
|
+
for (const dir of Object.values(platformDirs)) {
|
|
1399
|
+
if (!dir) continue;
|
|
1400
|
+
const link = path7.join(dir, entry);
|
|
1401
|
+
try {
|
|
1402
|
+
const stat = await fs7.lstat(link);
|
|
1403
|
+
if (!stat.isSymbolicLink()) continue;
|
|
1404
|
+
const resolved = await fs7.realpath(link).catch(() => "");
|
|
1405
|
+
if (resolved === removedAgentsPath || resolved === "") {
|
|
1406
|
+
await fs7.remove(link);
|
|
1407
|
+
}
|
|
1408
|
+
} catch {
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1613
1412
|
async function remove(type, options) {
|
|
1614
1413
|
if (!options?.skipIntro) {
|
|
1615
1414
|
intro4(pc5.bgCyan(pc5.black(" AI Manager : Remove ")));
|
|
@@ -1633,6 +1432,7 @@ async function remove(type, options) {
|
|
|
1633
1432
|
currentType = selectedType;
|
|
1634
1433
|
}
|
|
1635
1434
|
const normalizedType = currentType.toLowerCase().endsWith("s") ? currentType.slice(0, -1) : currentType;
|
|
1435
|
+
const subdir = TYPE_SUBDIRS[normalizedType] ?? "skills";
|
|
1636
1436
|
const scopeChoice = await chooseListRemoveScope({
|
|
1637
1437
|
action: "remove",
|
|
1638
1438
|
flag: options?.scope
|
|
@@ -1649,14 +1449,18 @@ async function remove(type, options) {
|
|
|
1649
1449
|
currentType = void 0;
|
|
1650
1450
|
continue;
|
|
1651
1451
|
}
|
|
1652
|
-
const
|
|
1653
|
-
normalizedType,
|
|
1452
|
+
const scopedBases = buildScopedBases(
|
|
1654
1453
|
scopeChoice.scope,
|
|
1454
|
+
scopeChoice.homedir,
|
|
1655
1455
|
scopeChoice.projectRoot
|
|
1656
1456
|
);
|
|
1657
1457
|
const s = spinner3();
|
|
1658
1458
|
s.start("Scanning for installed items...");
|
|
1659
|
-
const scannedItems = await scanInstalledItems(
|
|
1459
|
+
const scannedItems = await scanInstalledItems(
|
|
1460
|
+
scopedBases,
|
|
1461
|
+
subdir,
|
|
1462
|
+
normalizedType
|
|
1463
|
+
);
|
|
1660
1464
|
s.stop(`Found ${scannedItems.length} installed ${normalizedType}s.`);
|
|
1661
1465
|
if (scannedItems.length === 0) {
|
|
1662
1466
|
note3(`No installed ${normalizedType}s found.`, "Information");
|
|
@@ -1678,30 +1482,8 @@ async function remove(type, options) {
|
|
|
1678
1482
|
}
|
|
1679
1483
|
const itemsByKey = new Map(scannedItems.map((i) => [i.key, i]));
|
|
1680
1484
|
const selectedScannedItems = itemSelections.map((key) => itemsByKey.get(key)).filter((i) => !!i);
|
|
1681
|
-
const supportedPlatforms = /* @__PURE__ */ new Set();
|
|
1682
|
-
for (const { paths } of scopedPaths) {
|
|
1683
|
-
for (const platform of Object.keys(paths)) {
|
|
1684
|
-
if (paths[platform])
|
|
1685
|
-
supportedPlatforms.add(platform);
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1688
|
-
const platformOptions = [...supportedPlatforms].map((platform) => ({
|
|
1689
|
-
label: PLATFORM_LABELS[platform],
|
|
1690
|
-
value: platform
|
|
1691
|
-
}));
|
|
1692
|
-
const platformSelections = await multiselect2({
|
|
1693
|
-
message: "Select platforms to remove from:",
|
|
1694
|
-
options: platformOptions,
|
|
1695
|
-
required: true
|
|
1696
|
-
});
|
|
1697
|
-
if (isCancel5(platformSelections)) {
|
|
1698
|
-
if (isSingleShot) break;
|
|
1699
|
-
currentType = void 0;
|
|
1700
|
-
continue;
|
|
1701
|
-
}
|
|
1702
|
-
const selectedPlatforms = platformSelections;
|
|
1703
1485
|
const confirmDelete = await confirm3({
|
|
1704
|
-
message: `Are you sure you want to remove ${selectedScannedItems.length} item(s) from
|
|
1486
|
+
message: `Are you sure you want to remove ${selectedScannedItems.length} item(s) from .agents?`,
|
|
1705
1487
|
initialValue: false
|
|
1706
1488
|
});
|
|
1707
1489
|
if (isCancel5(confirmDelete) || !confirmDelete) {
|
|
@@ -1714,29 +1496,31 @@ async function remove(type, options) {
|
|
|
1714
1496
|
let removedCount = 0;
|
|
1715
1497
|
let notFoundCount = 0;
|
|
1716
1498
|
const errors = [];
|
|
1717
|
-
const
|
|
1718
|
-
for (const
|
|
1719
|
-
|
|
1499
|
+
const basesByScope = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const sb of scopedBases) {
|
|
1501
|
+
basesByScope.set(sb.scope, sb);
|
|
1720
1502
|
}
|
|
1721
|
-
for (const
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1503
|
+
for (const item of selectedScannedItems) {
|
|
1504
|
+
const scoped = basesByScope.get(item.scope);
|
|
1505
|
+
if (!scoped) continue;
|
|
1506
|
+
const agentsPath = path7.join(scoped.base, subdir, item.name);
|
|
1507
|
+
if (await fs7.pathExists(agentsPath)) {
|
|
1508
|
+
sDel.message(`Removing ${pc5.bold(item.label)} from .agents...`);
|
|
1509
|
+
try {
|
|
1510
|
+
await fs7.remove(agentsPath);
|
|
1511
|
+
removedCount++;
|
|
1512
|
+
const platformDirs = getPlatformDirs(
|
|
1513
|
+
item.scope,
|
|
1514
|
+
scoped.root,
|
|
1515
|
+
normalizedType
|
|
1729
1516
|
);
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
}
|
|
1734
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1735
|
-
errors.push(`${item.label} (${platform}): ${msg}`);
|
|
1736
|
-
}
|
|
1737
|
-
} else {
|
|
1738
|
-
notFoundCount++;
|
|
1517
|
+
await pruneDanglingLinks(platformDirs, item.name, agentsPath);
|
|
1518
|
+
} catch (err) {
|
|
1519
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1520
|
+
errors.push(`${item.label}: ${msg}`);
|
|
1739
1521
|
}
|
|
1522
|
+
} else {
|
|
1523
|
+
notFoundCount++;
|
|
1740
1524
|
}
|
|
1741
1525
|
}
|
|
1742
1526
|
if (errors.length > 0) {
|