@kurokeita/add-skill 1.7.0 → 1.9.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 +2 -2
- package/dist/bin/cli.js +1020 -27
- package/dist/skills/skill-creator/scripts/init_skill.cjs +31 -31
- package/dist/skills/skill-creator/scripts/package_skill.cjs +29 -29
- package/dist/skills/skill-creator/scripts/validate_skill.cjs +41 -41
- package/package.json +11 -4
- package/dist/src/commands/add.js +0 -253
- package/dist/src/commands/import.js +0 -116
- package/dist/src/commands/list.js +0 -92
- package/dist/src/commands/remove.js +0 -118
- package/dist/src/utils/github.js +0 -65
- package/dist/src/utils/paths.js +0 -45
- package/dist/src/utils/toml.js +0 -60
package/dist/src/commands/add.js
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { cancel, confirm, intro, isCancel, multiselect, note, outro, spinner, } from "@clack/prompts";
|
|
4
|
-
import fs from "fs-extra";
|
|
5
|
-
import pc from "picocolors";
|
|
6
|
-
import { fetchSkillFromGitHub } from "../utils/github.js";
|
|
7
|
-
import { getTargetPaths, PLATFORM_LABELS, TYPE_DIRS, } from "../utils/paths.js";
|
|
8
|
-
import { convertToGeminiCommandTOML } from "../utils/toml.js";
|
|
9
|
-
function getPlatformOptions(type) {
|
|
10
|
-
const paths = getTargetPaths(type);
|
|
11
|
-
return Object.entries(paths).map(([platform, pathStr]) => ({
|
|
12
|
-
label: PLATFORM_LABELS[platform],
|
|
13
|
-
value: platform,
|
|
14
|
-
hint: pathStr.replace(os.homedir(), "~"),
|
|
15
|
-
}));
|
|
16
|
-
}
|
|
17
|
-
async function installItem(itemName, overwrite, sourcePath, targetBaseDir) {
|
|
18
|
-
const targetPath = path.join(targetBaseDir, itemName);
|
|
19
|
-
if (!(await fs.pathExists(sourcePath))) {
|
|
20
|
-
throw new Error(`Item '${itemName}' not found at ${sourcePath}`);
|
|
21
|
-
}
|
|
22
|
-
if (!overwrite && (await fs.pathExists(targetPath))) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
await fs.ensureDir(targetBaseDir);
|
|
26
|
-
await fs.copy(sourcePath, targetPath, { overwrite });
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
export async function add(type, url) {
|
|
30
|
-
intro(pc.bgCyan(pc.black(` AI Manager : Add ${type} `)));
|
|
31
|
-
// Normalize type
|
|
32
|
-
const normalizedType = type.toLowerCase().endsWith("s")
|
|
33
|
-
? type.slice(0, -1)
|
|
34
|
-
: type;
|
|
35
|
-
if (!TYPE_DIRS[normalizedType]) {
|
|
36
|
-
cancel(`Unknown type: ${type}. Supported: skill, agent, workflow`);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
let tempDir = null;
|
|
40
|
-
let selectedItems = [];
|
|
41
|
-
try {
|
|
42
|
-
// 1. Determine Source (Local vs GitHub)
|
|
43
|
-
if (url) {
|
|
44
|
-
const s = spinner();
|
|
45
|
-
s.start(`Fetching ${normalizedType} from GitHub...`);
|
|
46
|
-
try {
|
|
47
|
-
const result = await fetchSkillFromGitHub(url);
|
|
48
|
-
tempDir = result.tempDir;
|
|
49
|
-
selectedItems = [result.skillName];
|
|
50
|
-
s.stop(pc.green(`Fetched ${normalizedType}: ${result.skillName}`));
|
|
51
|
-
}
|
|
52
|
-
catch (e) {
|
|
53
|
-
s.stop(pc.red("Failed to fetch"));
|
|
54
|
-
throw e;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
// Local Selection Logic
|
|
59
|
-
const sourceDir = TYPE_DIRS[normalizedType];
|
|
60
|
-
if (!(await fs.pathExists(sourceDir))) {
|
|
61
|
-
cancel(`${normalizedType} directory not found!`);
|
|
62
|
-
process.exit(1);
|
|
63
|
-
}
|
|
64
|
-
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
65
|
-
const availableItems = entries
|
|
66
|
-
.filter((entry) => entry.isDirectory() ||
|
|
67
|
-
((normalizedType === "workflow" || normalizedType === "agent") &&
|
|
68
|
-
entry.isFile() &&
|
|
69
|
-
entry.name.endsWith(".md")))
|
|
70
|
-
.map((entry) => ({ label: entry.name, value: entry.name }))
|
|
71
|
-
.sort((a, b) => a.label.localeCompare(b.label));
|
|
72
|
-
if (availableItems.length === 0) {
|
|
73
|
-
cancel(`No ${normalizedType}s found in directory.`);
|
|
74
|
-
process.exit(0);
|
|
75
|
-
}
|
|
76
|
-
// Select Items
|
|
77
|
-
const items = await multiselect({
|
|
78
|
-
message: `Select ${normalizedType}s to install:`,
|
|
79
|
-
options: availableItems,
|
|
80
|
-
required: true,
|
|
81
|
-
});
|
|
82
|
-
if (isCancel(items)) {
|
|
83
|
-
cancel("Operation cancelled.");
|
|
84
|
-
process.exit(0);
|
|
85
|
-
}
|
|
86
|
-
selectedItems = items;
|
|
87
|
-
}
|
|
88
|
-
// 2. Select Platforms
|
|
89
|
-
// Filter platform options based on support for the type
|
|
90
|
-
const supportedPaths = getTargetPaths(normalizedType);
|
|
91
|
-
const currentPlatformOptions = getPlatformOptions(normalizedType);
|
|
92
|
-
if (currentPlatformOptions.length === 0) {
|
|
93
|
-
cancel(`No supported platforms found for type '${normalizedType}'.`);
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
const platforms = await multiselect({
|
|
97
|
-
message: "Select target platforms:",
|
|
98
|
-
options: currentPlatformOptions,
|
|
99
|
-
required: true,
|
|
100
|
-
});
|
|
101
|
-
if (isCancel(platforms)) {
|
|
102
|
-
console.log("Cleaning up...");
|
|
103
|
-
if (tempDir)
|
|
104
|
-
await fs.remove(tempDir);
|
|
105
|
-
cancel("Operation cancelled.");
|
|
106
|
-
process.exit(0);
|
|
107
|
-
}
|
|
108
|
-
const selectedPlatforms = platforms;
|
|
109
|
-
// 3. Check for existing items
|
|
110
|
-
const existingItems = [];
|
|
111
|
-
for (const platform of selectedPlatforms) {
|
|
112
|
-
const targetBase = supportedPaths[platform];
|
|
113
|
-
if (!targetBase)
|
|
114
|
-
continue;
|
|
115
|
-
for (const item of selectedItems) {
|
|
116
|
-
const targetPath = path.join(targetBase, item);
|
|
117
|
-
if (await fs.pathExists(targetPath)) {
|
|
118
|
-
existingItems.push({ item, platform });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
let overwrite = true;
|
|
123
|
-
if (existingItems.length > 0) {
|
|
124
|
-
const limit = 5;
|
|
125
|
-
const displayList = existingItems
|
|
126
|
-
.slice(0, limit)
|
|
127
|
-
.map((e) => `${pc.bold(e.item)} (${e.platform})`)
|
|
128
|
-
.join(", ");
|
|
129
|
-
const remainder = existingItems.length - limit;
|
|
130
|
-
const message = remainder > 0
|
|
131
|
-
? `The following items already exist: ${displayList}, and ${remainder} others.`
|
|
132
|
-
: `The following items already exist: ${displayList}.`;
|
|
133
|
-
note(message, "Attention");
|
|
134
|
-
const shouldOverwrite = await confirm({
|
|
135
|
-
message: "Do you want to overwrite existing items?",
|
|
136
|
-
initialValue: false,
|
|
137
|
-
});
|
|
138
|
-
if (isCancel(shouldOverwrite)) {
|
|
139
|
-
if (tempDir)
|
|
140
|
-
await fs.remove(tempDir);
|
|
141
|
-
cancel("Operation cancelled.");
|
|
142
|
-
process.exit(0);
|
|
143
|
-
}
|
|
144
|
-
overwrite = shouldOverwrite;
|
|
145
|
-
}
|
|
146
|
-
// 4. Confirmation Note
|
|
147
|
-
note(`Installing ${selectedItems.length} ${normalizedType}s to ${selectedPlatforms.length} platforms...`, "Summary");
|
|
148
|
-
// 5. Installation Loop with Spinner
|
|
149
|
-
const s = spinner();
|
|
150
|
-
s.start("Installing...");
|
|
151
|
-
const errors = [];
|
|
152
|
-
let installedCount = 0;
|
|
153
|
-
let skippedCount = 0;
|
|
154
|
-
for (const platform of selectedPlatforms) {
|
|
155
|
-
const targetBase = supportedPaths[platform];
|
|
156
|
-
if (!targetBase)
|
|
157
|
-
continue;
|
|
158
|
-
for (const item of selectedItems) {
|
|
159
|
-
const message = `Installing ${pc.bold(item)} to ${pc.cyan(platform)}...`;
|
|
160
|
-
s.message(message);
|
|
161
|
-
try {
|
|
162
|
-
if (url && tempDir) {
|
|
163
|
-
// We already fetched it, check if it was a file
|
|
164
|
-
// Wait, fetchSkillFromGitHub returned isFile, but add command logic separated fetch from install loop.
|
|
165
|
-
// We need to store isFile state from fetch if using URL.
|
|
166
|
-
// However, selectedItems only stores strings.
|
|
167
|
-
// Let's assume if it is a URL, we re-verify or change logic slightly.
|
|
168
|
-
// Better: modify fetch logic above to store isFile.
|
|
169
|
-
}
|
|
170
|
-
// Re-implementing logic to be consistent with import.ts
|
|
171
|
-
// But wait, add command handles multiple items if LOCAL.
|
|
172
|
-
// If URL, it is single item `selectedItems = [result.skillName]`.
|
|
173
|
-
let currentSourcePath;
|
|
174
|
-
if (url && tempDir) {
|
|
175
|
-
// For URL fetch, we need to know if it was a file.
|
|
176
|
-
// We can check if tempDir/item exists as file.
|
|
177
|
-
const potentialFile = path.join(tempDir, item);
|
|
178
|
-
if ((await fs.pathExists(potentialFile)) &&
|
|
179
|
-
(await fs.stat(potentialFile)).isFile()) {
|
|
180
|
-
currentSourcePath = potentialFile;
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
currentSourcePath = tempDir;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
// Local source
|
|
188
|
-
const src = path.join(TYPE_DIRS[normalizedType], item);
|
|
189
|
-
if ((await fs.stat(src)).isFile()) {
|
|
190
|
-
currentSourcePath = src;
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
currentSourcePath = src; // direct path to dir
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
let targetItemName = item;
|
|
197
|
-
if (platform === "windsurf" && normalizedType === "agent") {
|
|
198
|
-
targetItemName = path.join(path.parse(item).name, "AGENTS.md");
|
|
199
|
-
}
|
|
200
|
-
// Gemini Workflow Conversion
|
|
201
|
-
if (platform === "gemini" &&
|
|
202
|
-
normalizedType === "workflow" &&
|
|
203
|
-
currentSourcePath.endsWith(".md")) {
|
|
204
|
-
const targetPath = path.join(targetBase, item.replace(/\.md$/, ".toml"));
|
|
205
|
-
if (!overwrite && (await fs.pathExists(targetPath))) {
|
|
206
|
-
skippedCount++;
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
const content = await fs.readFile(currentSourcePath, "utf-8");
|
|
210
|
-
const tomlContent = convertToGeminiCommandTOML(content);
|
|
211
|
-
await fs.ensureDir(targetBase);
|
|
212
|
-
await fs.writeFile(targetPath, tomlContent);
|
|
213
|
-
installedCount++;
|
|
214
|
-
}
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
const installed = await installItem(targetItemName, overwrite, currentSourcePath, targetBase);
|
|
218
|
-
if (installed) {
|
|
219
|
-
installedCount++;
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
skippedCount++;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
catch (err) {
|
|
226
|
-
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
227
|
-
errors.push(`${item} -> ${platform}: ${errorMessage}`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
// Cleanup
|
|
232
|
-
if (tempDir) {
|
|
233
|
-
await fs.remove(tempDir);
|
|
234
|
-
}
|
|
235
|
-
if (errors.length > 0) {
|
|
236
|
-
s.stop(pc.yellow(`Completed with errors. Installed: ${installedCount}, Skipped: ${skippedCount}, Errors: ${errors.length}`));
|
|
237
|
-
console.error(pc.red("\nErrors encountered:"));
|
|
238
|
-
for (const e of errors) {
|
|
239
|
-
console.error(pc.red(`- ${e}`));
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
else {
|
|
243
|
-
s.stop(pc.green(`Successfully installed ${installedCount} ${normalizedType}s. (${skippedCount} skipped)`));
|
|
244
|
-
}
|
|
245
|
-
outro("You're all set!");
|
|
246
|
-
}
|
|
247
|
-
catch (error) {
|
|
248
|
-
if (tempDir)
|
|
249
|
-
await fs.remove(tempDir);
|
|
250
|
-
cancel(`An error occurred: ${error}`);
|
|
251
|
-
process.exit(1);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { cancel, confirm, intro, isCancel, outro, spinner, } from "@clack/prompts";
|
|
3
|
-
import fs from "fs-extra";
|
|
4
|
-
import pc from "picocolors";
|
|
5
|
-
import { fetchSkillFromGitHub } from "../utils/github.js";
|
|
6
|
-
import { TYPE_DIRS } from "../utils/paths.js";
|
|
7
|
-
export async function importItem(type, url) {
|
|
8
|
-
intro(pc.bgCyan(pc.black(` AI Manager : Import ${type} `)));
|
|
9
|
-
// Normalize type
|
|
10
|
-
const normalizedType = type.toLowerCase().endsWith("s")
|
|
11
|
-
? type.slice(0, -1)
|
|
12
|
-
: type;
|
|
13
|
-
const targetBaseDir = TYPE_DIRS[normalizedType];
|
|
14
|
-
if (!targetBaseDir) {
|
|
15
|
-
cancel(`Unknown type: ${type}. Supported: skill, agent, workflow`);
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
if (!url) {
|
|
19
|
-
cancel("GitHub URL is required.");
|
|
20
|
-
process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
let tempDir = null;
|
|
23
|
-
try {
|
|
24
|
-
// 1. Fetch Item (using fetchSkillFromGitHub as generic fetcher)
|
|
25
|
-
const s = spinner();
|
|
26
|
-
s.start(`Fetching ${normalizedType} from GitHub...`);
|
|
27
|
-
let itemName = "";
|
|
28
|
-
let isFile = false;
|
|
29
|
-
try {
|
|
30
|
-
const result = await fetchSkillFromGitHub(url);
|
|
31
|
-
tempDir = result.tempDir;
|
|
32
|
-
if (!tempDir)
|
|
33
|
-
throw new Error("Failed to create temp directory");
|
|
34
|
-
itemName = result.skillName; // reuse property or rename if possible. assuming generic repo structure
|
|
35
|
-
isFile = result.isFile;
|
|
36
|
-
s.stop(pc.green(`Fetched: ${itemName}`));
|
|
37
|
-
}
|
|
38
|
-
catch (e) {
|
|
39
|
-
s.stop(pc.red("Failed to fetch"));
|
|
40
|
-
throw e;
|
|
41
|
-
}
|
|
42
|
-
// 2. Determine Target
|
|
43
|
-
const shouldFlatten = (normalizedType === "agent" || normalizedType === "workflow") && !isFile;
|
|
44
|
-
const targetPath = shouldFlatten
|
|
45
|
-
? targetBaseDir
|
|
46
|
-
: path.join(targetBaseDir, itemName);
|
|
47
|
-
// 3. Check Existence & Confirm Overwrite
|
|
48
|
-
if (shouldFlatten) {
|
|
49
|
-
const srcFiles = await fs.readdir(tempDir);
|
|
50
|
-
const conflicts = [];
|
|
51
|
-
for (const file of srcFiles) {
|
|
52
|
-
if (await fs.pathExists(path.join(targetBaseDir, file))) {
|
|
53
|
-
conflicts.push(file);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (conflicts.length > 0) {
|
|
57
|
-
const limit = 5;
|
|
58
|
-
const displayList = conflicts.slice(0, limit).join(", ");
|
|
59
|
-
const remainder = conflicts.length - limit;
|
|
60
|
-
const message = remainder > 0
|
|
61
|
-
? `Files ${displayList} (and ${remainder} others) already exist in ${targetBaseDir}. Overwrite?`
|
|
62
|
-
: `Files ${displayList} already exist in ${targetBaseDir}. Overwrite?`;
|
|
63
|
-
const shouldOverwrite = await confirm({
|
|
64
|
-
message,
|
|
65
|
-
initialValue: false,
|
|
66
|
-
});
|
|
67
|
-
if (isCancel(shouldOverwrite) || !shouldOverwrite) {
|
|
68
|
-
if (tempDir)
|
|
69
|
-
await fs.remove(tempDir);
|
|
70
|
-
cancel("Operation cancelled.");
|
|
71
|
-
process.exit(0);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
else if (await fs.pathExists(targetPath)) {
|
|
76
|
-
const shouldOverwrite = await confirm({
|
|
77
|
-
message: `${normalizedType} '${itemName}' already exists in the repo. Overwrite?`,
|
|
78
|
-
initialValue: false,
|
|
79
|
-
});
|
|
80
|
-
if (isCancel(shouldOverwrite) || !shouldOverwrite) {
|
|
81
|
-
if (tempDir)
|
|
82
|
-
await fs.remove(tempDir);
|
|
83
|
-
cancel("Operation cancelled.");
|
|
84
|
-
process.exit(0);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// 4. Move/Copy to Target Directory
|
|
88
|
-
s.start(`Importing ${itemName} to ${targetBaseDir}...`);
|
|
89
|
-
await fs.ensureDir(targetBaseDir);
|
|
90
|
-
if (isFile) {
|
|
91
|
-
await fs.copy(path.join(tempDir, itemName), targetPath, {
|
|
92
|
-
overwrite: true,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
else {
|
|
96
|
-
if (shouldFlatten) {
|
|
97
|
-
await fs.copy(tempDir, targetBaseDir, { overwrite: true });
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
await fs.copy(tempDir, targetPath, { overwrite: true });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
s.stop(pc.green(`Successfully imported ${itemName}!`));
|
|
104
|
-
// Cleanup
|
|
105
|
-
if (tempDir) {
|
|
106
|
-
await fs.remove(tempDir);
|
|
107
|
-
}
|
|
108
|
-
outro(`${normalizedType} available at: ${targetPath}`);
|
|
109
|
-
}
|
|
110
|
-
catch (error) {
|
|
111
|
-
if (tempDir)
|
|
112
|
-
await fs.remove(tempDir);
|
|
113
|
-
cancel(`An error occurred: ${error}`);
|
|
114
|
-
process.exit(1);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import fs from "fs-extra";
|
|
2
|
-
import pc from "picocolors";
|
|
3
|
-
import { getTargetPaths, TYPE_DIRS } from "../utils/paths.js";
|
|
4
|
-
export async function list(type, options) {
|
|
5
|
-
try {
|
|
6
|
-
const typesToList = type
|
|
7
|
-
? [type.toLowerCase()]
|
|
8
|
-
: ["skill", "agent", "workflow"];
|
|
9
|
-
for (const t of typesToList) {
|
|
10
|
-
// Handle plural/singular input for convenience
|
|
11
|
-
const normalizedType = t.endsWith("s") ? t.slice(0, -1) : t;
|
|
12
|
-
if (options?.local) {
|
|
13
|
-
// List installed items locally
|
|
14
|
-
console.log(pc.bold(pc.blue(`Installed ${normalizedType}s (Local):`)));
|
|
15
|
-
const targetPaths = getTargetPaths(normalizedType);
|
|
16
|
-
let foundAny = false;
|
|
17
|
-
for (const [platform, pathStr] of Object.entries(targetPaths)) {
|
|
18
|
-
const fullPath = pathStr;
|
|
19
|
-
if (await fs.pathExists(fullPath)) {
|
|
20
|
-
const entries = await fs.readdir(fullPath, {
|
|
21
|
-
withFileTypes: true,
|
|
22
|
-
});
|
|
23
|
-
const items = entries
|
|
24
|
-
.filter((entry) => entry.isDirectory() ||
|
|
25
|
-
// For local items, agents/workflows might be directories OR .md files depending on how they were installed/flattened.
|
|
26
|
-
// Usually flattened -> directory (if unpacked) or file (if generic).
|
|
27
|
-
// Existing add logic:
|
|
28
|
-
// Agents/Workflows can be flattened (unpacked to base) or not.
|
|
29
|
-
// If flattened, they are files in the base dir? No, unpacked to base means they are files/dirs IN the base dir.
|
|
30
|
-
// Wait, if I import "agent-a" (dir) and flatten, it copies contents of agent-a to agents/.
|
|
31
|
-
// So "agents/skill.md" might exist.
|
|
32
|
-
// The user requirement says "list all installed agents/skills/workflows".
|
|
33
|
-
// In local global_skills (antigravity), they are directories.
|
|
34
|
-
// In local global_agents, they might be directories or .md files.
|
|
35
|
-
// Let's list directories and .md files.
|
|
36
|
-
(entry.isFile() && entry.name.endsWith(".md")))
|
|
37
|
-
.map((entry) => entry.name)
|
|
38
|
-
.sort();
|
|
39
|
-
if (items.length > 0) {
|
|
40
|
-
foundAny = true;
|
|
41
|
-
console.log(pc.cyan(` ${platform}:`));
|
|
42
|
-
for (const item of items) {
|
|
43
|
-
console.log(` - ${item}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (!foundAny) {
|
|
49
|
-
console.log(pc.dim(` No installed ${normalizedType}s found.`));
|
|
50
|
-
}
|
|
51
|
-
console.log(""); // Newline for separation
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
// List available items in repo (existing logic)
|
|
55
|
-
if (!TYPE_DIRS[normalizedType]) {
|
|
56
|
-
if (type) {
|
|
57
|
-
console.error(pc.red(`Unknown type: ${type}. Supported types: skill, agent, workflow`));
|
|
58
|
-
}
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
const dirPath = TYPE_DIRS[normalizedType];
|
|
62
|
-
if (!(await fs.pathExists(dirPath))) {
|
|
63
|
-
if (type) {
|
|
64
|
-
console.log(pc.yellow(`No ${t} directory found at: ${dirPath}`));
|
|
65
|
-
}
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
69
|
-
const items = entries
|
|
70
|
-
.filter((entry) => entry.isDirectory() ||
|
|
71
|
-
(normalizedType !== "skill" &&
|
|
72
|
-
entry.isFile() &&
|
|
73
|
-
entry.name.endsWith(".md")))
|
|
74
|
-
.map((entry) => entry.name)
|
|
75
|
-
.sort();
|
|
76
|
-
if (items.length > 0) {
|
|
77
|
-
console.log(pc.bold(pc.blue(`Available ${normalizedType}s:`)));
|
|
78
|
-
for (const item of items) {
|
|
79
|
-
console.log(`- ${item}`);
|
|
80
|
-
}
|
|
81
|
-
console.log(pc.dim(`Total: ${items.length} ${normalizedType}s\n`));
|
|
82
|
-
}
|
|
83
|
-
else if (type) {
|
|
84
|
-
console.log(pc.dim(`No ${normalizedType}s found.`));
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
console.error(pc.red("Error listing items:"), error);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { cancel, confirm, intro, isCancel, multiselect, outro, spinner, } from "@clack/prompts";
|
|
3
|
-
import fs from "fs-extra";
|
|
4
|
-
import pc from "picocolors";
|
|
5
|
-
import { getTargetPaths, PLATFORM_LABELS, } from "../utils/paths.js";
|
|
6
|
-
export async function remove(type) {
|
|
7
|
-
intro(pc.bgCyan(pc.black(` AI Manager : Remove ${type} `)));
|
|
8
|
-
// Normalize type
|
|
9
|
-
const normalizedType = type.toLowerCase().endsWith("s")
|
|
10
|
-
? type.slice(0, -1)
|
|
11
|
-
: type;
|
|
12
|
-
const targetPaths = getTargetPaths(normalizedType);
|
|
13
|
-
// 1. Collect unique installed items across all platforms
|
|
14
|
-
const uniqueItems = new Set();
|
|
15
|
-
const s = spinner();
|
|
16
|
-
s.start("Scanning for installed items...");
|
|
17
|
-
for (const pathStr of Object.values(targetPaths)) {
|
|
18
|
-
if (await fs.pathExists(pathStr)) {
|
|
19
|
-
const entries = await fs.readdir(pathStr, {
|
|
20
|
-
withFileTypes: true,
|
|
21
|
-
});
|
|
22
|
-
for (const entry of entries) {
|
|
23
|
-
if (entry.isDirectory() ||
|
|
24
|
-
(normalizedType === "workflow" &&
|
|
25
|
-
entry.isFile() &&
|
|
26
|
-
entry.name.endsWith(".md")) ||
|
|
27
|
-
(normalizedType === "agent" &&
|
|
28
|
-
entry.isFile() &&
|
|
29
|
-
entry.name.endsWith(".md"))) {
|
|
30
|
-
uniqueItems.add(entry.name);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
s.stop(`Found ${uniqueItems.size} unique installed ${normalizedType}s.`);
|
|
36
|
-
if (uniqueItems.size === 0) {
|
|
37
|
-
cancel(`No installed ${normalizedType}s found.`);
|
|
38
|
-
process.exit(0);
|
|
39
|
-
}
|
|
40
|
-
// Sort explicitly
|
|
41
|
-
const sortedItems = Array.from(uniqueItems).sort();
|
|
42
|
-
// 2. Select Items
|
|
43
|
-
const itemSelections = await multiselect({
|
|
44
|
-
message: `Select ${normalizedType}s to remove:`,
|
|
45
|
-
options: sortedItems.map((item) => ({ value: item, label: item })),
|
|
46
|
-
required: true,
|
|
47
|
-
});
|
|
48
|
-
if (isCancel(itemSelections)) {
|
|
49
|
-
cancel("Operation cancelled.");
|
|
50
|
-
process.exit(0);
|
|
51
|
-
}
|
|
52
|
-
const selectedItems = itemSelections;
|
|
53
|
-
// 3. Select Platforms
|
|
54
|
-
// Only show platforms relevant to the type
|
|
55
|
-
const platformOptions = Object.entries(targetPaths).map(([platform]) => ({
|
|
56
|
-
label: PLATFORM_LABELS[platform],
|
|
57
|
-
value: platform,
|
|
58
|
-
}));
|
|
59
|
-
const platformSelections = await multiselect({
|
|
60
|
-
message: "Select platforms to remove from:",
|
|
61
|
-
options: platformOptions,
|
|
62
|
-
required: true,
|
|
63
|
-
});
|
|
64
|
-
if (isCancel(platformSelections)) {
|
|
65
|
-
cancel("Operation cancelled.");
|
|
66
|
-
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
const selectedPlatforms = platformSelections;
|
|
69
|
-
// 4. Confirmation
|
|
70
|
-
const confirmDelete = await confirm({
|
|
71
|
-
message: `Are you sure you want to remove ${selectedItems.length} item(s) from ${selectedPlatforms.length} platform(s)?`,
|
|
72
|
-
initialValue: false,
|
|
73
|
-
});
|
|
74
|
-
if (isCancel(confirmDelete) || !confirmDelete) {
|
|
75
|
-
cancel("Operation cancelled.");
|
|
76
|
-
process.exit(0);
|
|
77
|
-
}
|
|
78
|
-
// 5. Deletion Loop
|
|
79
|
-
const sDel = spinner();
|
|
80
|
-
sDel.start("Removing items...");
|
|
81
|
-
let removedCount = 0;
|
|
82
|
-
let notFoundCount = 0;
|
|
83
|
-
const errors = [];
|
|
84
|
-
for (const platform of selectedPlatforms) {
|
|
85
|
-
const targetBase = targetPaths[platform];
|
|
86
|
-
if (!targetBase)
|
|
87
|
-
continue;
|
|
88
|
-
for (const item of selectedItems) {
|
|
89
|
-
const targetPath = path.join(targetBase, item);
|
|
90
|
-
// Check if exists before trying to remove, to correctly count "removed" vs "not found"
|
|
91
|
-
if (await fs.pathExists(targetPath)) {
|
|
92
|
-
sDel.message(`Removing ${pc.bold(item)} from ${pc.cyan(platform)}...`);
|
|
93
|
-
try {
|
|
94
|
-
await fs.remove(targetPath);
|
|
95
|
-
removedCount++;
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
99
|
-
errors.push(`${item} (${platform}): ${msg}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
notFoundCount++;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (errors.length > 0) {
|
|
108
|
-
sDel.stop(pc.yellow(`Completed with errors. Removed: ${removedCount}, Not Found: ${notFoundCount}, Errors: ${errors.length}`));
|
|
109
|
-
console.error(pc.red("\nErrors encountered:"));
|
|
110
|
-
for (const e of errors) {
|
|
111
|
-
console.error(pc.red(`- ${e}`));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
sDel.stop(pc.green(`Successfully removed ${removedCount} items. (${notFoundCount} not found)`));
|
|
116
|
-
}
|
|
117
|
-
outro("Done!");
|
|
118
|
-
}
|
package/dist/src/utils/github.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import os from "node:os";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import fs from "fs-extra";
|
|
4
|
-
export async function fetchSkillFromGitHub(url) {
|
|
5
|
-
const regex = /github\.com\/([^/]+)\/([^/]+)\/(?:tree|blob)\/([^/]+)\/(.+)/;
|
|
6
|
-
const match = url.match(regex);
|
|
7
|
-
if (!match) {
|
|
8
|
-
throw new Error("Invalid GitHub URL. Format: https://github.com/owner/repo/tree/branch/path/to/skill or blob/branch/path/to/file");
|
|
9
|
-
}
|
|
10
|
-
const [, owner, repo, ref, skillPath] = match;
|
|
11
|
-
const skillName = path.basename(skillPath);
|
|
12
|
-
const initialApiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${skillPath}?ref=${ref}`;
|
|
13
|
-
const tempDir = path.join(os.tmpdir(), "ai-agents-install", skillName);
|
|
14
|
-
await fs.ensureDir(tempDir);
|
|
15
|
-
await fs.emptyDir(tempDir);
|
|
16
|
-
let isFile = false;
|
|
17
|
-
async function downloadDirectory(apiUrl, localDir) {
|
|
18
|
-
const response = await fetch(apiUrl, {
|
|
19
|
-
headers: {
|
|
20
|
-
"User-Agent": "ai-agents-cli",
|
|
21
|
-
Accept: "application/vnd.github.v3+json",
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
if (!response.ok) {
|
|
25
|
-
throw new Error(`Failed to fetch from GitHub (${response.status}): ${response.statusText}`);
|
|
26
|
-
}
|
|
27
|
-
const data = await response.json();
|
|
28
|
-
// Handle single file response
|
|
29
|
-
if (!Array.isArray(data) && data.type === "file") {
|
|
30
|
-
isFile = true;
|
|
31
|
-
const item = data;
|
|
32
|
-
if (item.download_url) {
|
|
33
|
-
const fileContentResponse = await fetch(item.download_url);
|
|
34
|
-
if (!fileContentResponse.ok) {
|
|
35
|
-
throw new Error(`Failed to download ${item.name}: ${fileContentResponse.statusText}`);
|
|
36
|
-
}
|
|
37
|
-
const content = await fileContentResponse.text();
|
|
38
|
-
await fs.writeFile(path.join(localDir, item.name), content);
|
|
39
|
-
}
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
if (!Array.isArray(data)) {
|
|
43
|
-
throw new Error("Invalid response from GitHub API. Expected directory listing or file object.");
|
|
44
|
-
}
|
|
45
|
-
const files = data;
|
|
46
|
-
for (const item of files) {
|
|
47
|
-
if (item.type === "file" && item.download_url) {
|
|
48
|
-
const fileContentResponse = await fetch(item.download_url);
|
|
49
|
-
if (!fileContentResponse.ok) {
|
|
50
|
-
console.warn(`Failed to download ${item.name}: ${fileContentResponse.statusText}`);
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
const content = await fileContentResponse.text();
|
|
54
|
-
await fs.writeFile(path.join(localDir, item.name), content);
|
|
55
|
-
}
|
|
56
|
-
else if (item.type === "dir") {
|
|
57
|
-
const newLocalDir = path.join(localDir, item.name);
|
|
58
|
-
await fs.ensureDir(newLocalDir);
|
|
59
|
-
await downloadDirectory(item.url, newLocalDir);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
await downloadDirectory(initialApiUrl, tempDir);
|
|
64
|
-
return { tempDir, skillName, isFile };
|
|
65
|
-
}
|