skill-atlas-cli 0.1.4
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 +82 -0
- package/bin/cli.js +56 -0
- package/lib/index.d.ts +63 -0
- package/lib/index.js +1128 -0
- package/package.json +56 -0
- package/setup.sh +397 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path, { join } from "node:path";
|
|
3
|
+
import os, { homedir } from "node:os";
|
|
4
|
+
import semver from "semver";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import path$1, { basename, dirname, join as join$1, normalize, relative, resolve, sep } from "path";
|
|
8
|
+
import { existsSync as existsSync$1, writeFileSync as writeFileSync$1 } from "fs";
|
|
9
|
+
import os$1, { homedir as homedir$1, platform, tmpdir } from "os";
|
|
10
|
+
import * as readline from "readline";
|
|
11
|
+
import { Writable } from "stream";
|
|
12
|
+
import { cp, lstat, mkdir, readdir, readlink, realpath, rename, rm, stat, symlink, unlink, writeFile } from "fs/promises";
|
|
13
|
+
import { execSync } from "child_process";
|
|
14
|
+
import { info } from "console";
|
|
15
|
+
import { createConsola } from "consola";
|
|
16
|
+
//#region \0rolldown/runtime.js
|
|
17
|
+
var __create = Object.create;
|
|
18
|
+
var __defProp = Object.defineProperty;
|
|
19
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
20
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
21
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
22
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
23
|
+
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
24
|
+
var __copyProps = (to, from, except, desc) => {
|
|
25
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
26
|
+
key = keys[i];
|
|
27
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
28
|
+
get: ((k) => from[k]).bind(null, key),
|
|
29
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return to;
|
|
33
|
+
};
|
|
34
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
35
|
+
value: mod,
|
|
36
|
+
enumerable: true
|
|
37
|
+
}) : target, mod));
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/core/update-notifier.ts
|
|
40
|
+
/**
|
|
41
|
+
* npm 包更新检查:有新版本时提示无需重装,直接 npm update 即可
|
|
42
|
+
*/
|
|
43
|
+
const CACHE_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config", "skill-atlas");
|
|
44
|
+
const CACHE_FILE = join(CACHE_DIR, "update-check.json");
|
|
45
|
+
const CHECK_INTERVAL = 1e3 * 60 * 60 * 24;
|
|
46
|
+
async function fetchLatestVersion(pkgName) {
|
|
47
|
+
return (await (await fetch(`https://registry.npmjs.org/-/package/${pkgName}/dist-tags`)).json()).latest || "0.0.0";
|
|
48
|
+
}
|
|
49
|
+
function getLastCheck() {
|
|
50
|
+
try {
|
|
51
|
+
if (!existsSync(CACHE_FILE)) return void 0;
|
|
52
|
+
return JSON.parse(readFileSync(CACHE_FILE, "utf8")).lastCheck;
|
|
53
|
+
} catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function saveLastCheck() {
|
|
58
|
+
try {
|
|
59
|
+
if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
|
|
60
|
+
writeFileSync(CACHE_FILE, JSON.stringify({ lastCheck: Date.now() }) + "\n");
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 检查更新,有新版本时输出提示(无需重装,直接 update)
|
|
65
|
+
*/
|
|
66
|
+
function checkForUpdate(pkg) {
|
|
67
|
+
if (!process.stdout.isTTY) return;
|
|
68
|
+
const lastCheck = getLastCheck();
|
|
69
|
+
if (lastCheck && Date.now() - lastCheck < CHECK_INTERVAL) return;
|
|
70
|
+
fetchLatestVersion(pkg.name).then((latest) => {
|
|
71
|
+
saveLastCheck();
|
|
72
|
+
if (semver.gt(latest, pkg.version)) console.error(`\n Update available: ${pkg.version} → ${latest}\n No need to reinstall. Run: npm update -g ${pkg.name}\n`);
|
|
73
|
+
}).catch(() => {});
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region src/core/config.ts
|
|
77
|
+
var import_picocolors = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
78
|
+
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
79
|
+
let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
80
|
+
let formatter = (open, close, replace = open) => (input) => {
|
|
81
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
82
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
83
|
+
};
|
|
84
|
+
let replaceClose = (string, close, replace, index) => {
|
|
85
|
+
let result = "", cursor = 0;
|
|
86
|
+
do {
|
|
87
|
+
result += string.substring(cursor, index) + replace;
|
|
88
|
+
cursor = index + close.length;
|
|
89
|
+
index = string.indexOf(close, cursor);
|
|
90
|
+
} while (~index);
|
|
91
|
+
return result + string.substring(cursor);
|
|
92
|
+
};
|
|
93
|
+
let createColors = (enabled = isColorSupported) => {
|
|
94
|
+
let f = enabled ? formatter : () => String;
|
|
95
|
+
return {
|
|
96
|
+
isColorSupported: enabled,
|
|
97
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
98
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
99
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
100
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
101
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
102
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
103
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
104
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
105
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
106
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
107
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
108
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
109
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
110
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
111
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
112
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
113
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
114
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
115
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
116
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
117
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
118
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
119
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
120
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
121
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
122
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
123
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
124
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
125
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
126
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
127
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
128
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
129
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
130
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
131
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
132
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
133
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
134
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
135
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
136
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
137
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
module.exports = createColors();
|
|
141
|
+
module.exports.createColors = createColors;
|
|
142
|
+
})))(), 1);
|
|
143
|
+
const CONFIG_DIR = path.join(os.homedir(), ".openclawmp");
|
|
144
|
+
const AUTH_FILE = path.join(CONFIG_DIR, "auth.json");
|
|
145
|
+
const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
146
|
+
let API_BASE = "https://pre-skillhub.aliyun-inc.com";
|
|
147
|
+
const OPENCLAW_STATE_DIR = process.env.OPENCLAW_STATE_DIR || path.join(os.homedir(), ".openclaw");
|
|
148
|
+
const LOCKFILE = path.join(OPENCLAW_STATE_DIR, "seafood-lock.json");
|
|
149
|
+
const DEVICE_JSON = path.join(OPENCLAW_STATE_DIR, "identity", "device.json");
|
|
150
|
+
const ASSET_TYPES = {
|
|
151
|
+
skill: "skills",
|
|
152
|
+
plugin: "plugins",
|
|
153
|
+
trigger: "triggers",
|
|
154
|
+
channel: "channels",
|
|
155
|
+
experience: "experiences"
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Ensure config directory exists
|
|
159
|
+
*/
|
|
160
|
+
function ensureConfigDir() {
|
|
161
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get the install directory for a given asset type
|
|
165
|
+
*/
|
|
166
|
+
function installDirForType(type) {
|
|
167
|
+
const subdir = ASSET_TYPES[type];
|
|
168
|
+
if (!subdir) throw new Error(`Unknown asset type: ${type}. Valid types: ${Object.keys(ASSET_TYPES).join(", ")}`);
|
|
169
|
+
return path.join(OPENCLAW_STATE_DIR, subdir);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get/set API base URL
|
|
173
|
+
*/
|
|
174
|
+
function getApiBase() {
|
|
175
|
+
return API_BASE;
|
|
176
|
+
}
|
|
177
|
+
function setApiBase(url) {
|
|
178
|
+
API_BASE = url.replace(/\/+$/, "");
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Read auth token (priority: env var > auth.json > credentials.json)
|
|
182
|
+
*/
|
|
183
|
+
function getAuthToken() {
|
|
184
|
+
if (process.env.OPENCLAWMP_TOKEN) return process.env.OPENCLAWMP_TOKEN;
|
|
185
|
+
ensureConfigDir();
|
|
186
|
+
if (fs.existsSync(AUTH_FILE)) try {
|
|
187
|
+
const data = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
|
|
188
|
+
if (data.token) return data.token;
|
|
189
|
+
} catch {}
|
|
190
|
+
if (fs.existsSync(CREDENTIALS_FILE)) try {
|
|
191
|
+
const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
192
|
+
if (data.api_key) return data.api_key;
|
|
193
|
+
} catch {}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Save auth token
|
|
198
|
+
*/
|
|
199
|
+
function saveAuthToken(token, extra = {}) {
|
|
200
|
+
ensureConfigDir();
|
|
201
|
+
const data = {
|
|
202
|
+
token,
|
|
203
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
204
|
+
...extra
|
|
205
|
+
};
|
|
206
|
+
fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2) + "\n");
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Read the OpenClaw device ID
|
|
210
|
+
*/
|
|
211
|
+
function getDeviceId() {
|
|
212
|
+
if (!fs.existsSync(DEVICE_JSON)) return null;
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(fs.readFileSync(DEVICE_JSON, "utf-8")).deviceId || null;
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Initialize lockfile if it doesn't exist
|
|
221
|
+
*/
|
|
222
|
+
function initLockfile() {
|
|
223
|
+
if (!fs.existsSync(LOCKFILE)) {
|
|
224
|
+
const dir = path.dirname(LOCKFILE);
|
|
225
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
226
|
+
fs.writeFileSync(LOCKFILE, JSON.stringify({
|
|
227
|
+
version: 1,
|
|
228
|
+
installed: {}
|
|
229
|
+
}, null, 2) + "\n");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Read the lockfile
|
|
234
|
+
*/
|
|
235
|
+
function readLockfile() {
|
|
236
|
+
initLockfile();
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(fs.readFileSync(LOCKFILE, "utf-8"));
|
|
239
|
+
} catch {
|
|
240
|
+
return {
|
|
241
|
+
version: 1,
|
|
242
|
+
installed: {}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Update a lockfile entry
|
|
248
|
+
*/
|
|
249
|
+
function updateLockfile(key, version, location) {
|
|
250
|
+
const lock = readLockfile();
|
|
251
|
+
lock.installed[key] = {
|
|
252
|
+
version,
|
|
253
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
254
|
+
location
|
|
255
|
+
};
|
|
256
|
+
fs.writeFileSync(LOCKFILE, JSON.stringify(lock, null, 2) + "\n");
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Remove a lockfile entry
|
|
260
|
+
*/
|
|
261
|
+
function removeLockfile(key) {
|
|
262
|
+
const lock = readLockfile();
|
|
263
|
+
delete lock.installed[key];
|
|
264
|
+
fs.writeFileSync(LOCKFILE, JSON.stringify(lock, null, 2) + "\n");
|
|
265
|
+
}
|
|
266
|
+
var config_default = {
|
|
267
|
+
CONFIG_DIR,
|
|
268
|
+
OPENCLAW_STATE_DIR,
|
|
269
|
+
LOCKFILE,
|
|
270
|
+
DEVICE_JSON,
|
|
271
|
+
ASSET_TYPES,
|
|
272
|
+
ensureConfigDir,
|
|
273
|
+
installDirForType,
|
|
274
|
+
getApiBase,
|
|
275
|
+
setApiBase,
|
|
276
|
+
getAuthToken,
|
|
277
|
+
saveAuthToken,
|
|
278
|
+
getDeviceId,
|
|
279
|
+
initLockfile,
|
|
280
|
+
readLockfile,
|
|
281
|
+
updateLockfile,
|
|
282
|
+
removeLockfile
|
|
283
|
+
};
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/core/api.ts
|
|
286
|
+
/**
|
|
287
|
+
* Make a GET request to the API
|
|
288
|
+
* @param {string} apiPath - API path (e.g., '/api/assets')
|
|
289
|
+
* @param {object} [params] - Query parameters
|
|
290
|
+
* @returns {Promise<object>} Parsed JSON response
|
|
291
|
+
*/
|
|
292
|
+
async function get(apiPath, params = {}) {
|
|
293
|
+
const url = new URL(apiPath, config_default.getApiBase());
|
|
294
|
+
for (const [k, v] of Object.entries(params)) if (v !== void 0 && v !== null && v !== "") url.searchParams.set(k, String(v));
|
|
295
|
+
const res = await fetch(url.toString(), {});
|
|
296
|
+
if (!res.ok) {
|
|
297
|
+
const body = await res.text().catch(() => "");
|
|
298
|
+
throw new Error(`API error ${res.status}: ${body || res.statusText}`);
|
|
299
|
+
}
|
|
300
|
+
return res.json();
|
|
301
|
+
}
|
|
302
|
+
function normalizeSearchSkillsResponse(result) {
|
|
303
|
+
const payload = result && typeof result === "object" && "data" in result ? result.data : result;
|
|
304
|
+
const normalized = payload && typeof payload === "object" ? payload : {};
|
|
305
|
+
return {
|
|
306
|
+
items: Array.isArray(normalized.items) ? normalized.items : [],
|
|
307
|
+
page: typeof normalized.page === "number" ? normalized.page : 1,
|
|
308
|
+
pageSize: typeof normalized.pageSize === "number" ? normalized.pageSize : 20,
|
|
309
|
+
total: typeof normalized.total === "number" ? normalized.total : 0
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async function searchSkills(params = {}) {
|
|
313
|
+
return normalizeSearchSkillsResponse(await get("/api/v1/skills", {
|
|
314
|
+
page: 1,
|
|
315
|
+
pageSize: 20,
|
|
316
|
+
...params
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
async function getSkillDetail(slug) {
|
|
320
|
+
try {
|
|
321
|
+
return await get(`/api/v1/skills/${slug}`);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
err instanceof Error ? err.message : String(err);
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function findAsset(slug) {
|
|
328
|
+
return await getSkillDetail(slug);
|
|
329
|
+
}
|
|
330
|
+
async function downloadSkillPackage(slug, version) {
|
|
331
|
+
const url = new URL("/api/v1/download", config_default.getApiBase());
|
|
332
|
+
url.searchParams.set("slug", slug);
|
|
333
|
+
url.searchParams.set("version", version);
|
|
334
|
+
const res = await fetch(url.toString(), {});
|
|
335
|
+
if (!res.ok) {
|
|
336
|
+
const body = await res.text().catch(() => "");
|
|
337
|
+
throw new Error(`下载失败 ${res.status}: ${body || res.statusText}。`);
|
|
338
|
+
}
|
|
339
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
340
|
+
return Buffer.from(arrayBuffer);
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
343
|
+
//#region node_modules/.pnpm/xdg-basedir@5.1.0/node_modules/xdg-basedir/index.js
|
|
344
|
+
const homeDirectory = os$1.homedir();
|
|
345
|
+
const { env } = process;
|
|
346
|
+
const xdgData = env.XDG_DATA_HOME || (homeDirectory ? path$1.join(homeDirectory, ".local", "share") : void 0);
|
|
347
|
+
const xdgConfig = env.XDG_CONFIG_HOME || (homeDirectory ? path$1.join(homeDirectory, ".config") : void 0);
|
|
348
|
+
env.XDG_STATE_HOME || homeDirectory && path$1.join(homeDirectory, ".local", "state");
|
|
349
|
+
env.XDG_CACHE_HOME || homeDirectory && path$1.join(homeDirectory, ".cache");
|
|
350
|
+
env.XDG_RUNTIME_DIR;
|
|
351
|
+
const xdgDataDirectories = (env.XDG_DATA_DIRS || "/usr/local/share/:/usr/share/").split(":");
|
|
352
|
+
if (xdgData) xdgDataDirectories.unshift(xdgData);
|
|
353
|
+
const xdgConfigDirectories = (env.XDG_CONFIG_DIRS || "/etc/xdg").split(":");
|
|
354
|
+
if (xdgConfig) xdgConfigDirectories.unshift(xdgConfig);
|
|
355
|
+
//#endregion
|
|
356
|
+
//#region src/core/agents.ts
|
|
357
|
+
const home = homedir$1();
|
|
358
|
+
const configHome = xdgConfig ?? join$1(home, ".config");
|
|
359
|
+
const codexHome = process.env.CODEX_HOME?.trim() || join$1(home, ".codex");
|
|
360
|
+
const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join$1(home, ".claude");
|
|
361
|
+
function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync$1) {
|
|
362
|
+
if (pathExists(join$1(homeDir, ".openclaw"))) return join$1(homeDir, ".openclaw/skills");
|
|
363
|
+
if (pathExists(join$1(homeDir, ".clawdbot"))) return join$1(homeDir, ".clawdbot/skills");
|
|
364
|
+
if (pathExists(join$1(homeDir, ".moltbot"))) return join$1(homeDir, ".moltbot/skills");
|
|
365
|
+
return join$1(homeDir, ".openclaw/skills");
|
|
366
|
+
}
|
|
367
|
+
const agents = {
|
|
368
|
+
"claude-code": {
|
|
369
|
+
name: "claude-code",
|
|
370
|
+
displayName: "Claude Code",
|
|
371
|
+
skillsDir: ".claude/skills",
|
|
372
|
+
globalSkillsDir: join$1(claudeHome, "skills"),
|
|
373
|
+
detectInstalled: async () => {
|
|
374
|
+
return existsSync$1(claudeHome);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
openclaw: {
|
|
378
|
+
name: "openclaw",
|
|
379
|
+
displayName: "OpenClaw",
|
|
380
|
+
skillsDir: "skills",
|
|
381
|
+
globalSkillsDir: getOpenClawGlobalSkillsDir(),
|
|
382
|
+
detectInstalled: async () => {
|
|
383
|
+
return existsSync$1(join$1(home, ".openclaw")) || existsSync$1(join$1(home, ".clawdbot")) || existsSync$1(join$1(home, ".moltbot"));
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
cline: {
|
|
387
|
+
name: "cline",
|
|
388
|
+
displayName: "Cline",
|
|
389
|
+
skillsDir: ".agents/skills",
|
|
390
|
+
globalSkillsDir: join$1(home, ".agents", "skills"),
|
|
391
|
+
detectInstalled: async () => {
|
|
392
|
+
return existsSync$1(join$1(home, ".cline"));
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
antigravity: {
|
|
396
|
+
name: "antigravity",
|
|
397
|
+
displayName: "Antigravity",
|
|
398
|
+
skillsDir: ".agent/skills",
|
|
399
|
+
globalSkillsDir: join$1(home, ".gemini/antigravity/skills"),
|
|
400
|
+
detectInstalled: async () => {
|
|
401
|
+
return existsSync$1(join$1(home, ".gemini/antigravity"));
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
codex: {
|
|
405
|
+
name: "codex",
|
|
406
|
+
displayName: "Codex",
|
|
407
|
+
skillsDir: ".agents/skills",
|
|
408
|
+
globalSkillsDir: join$1(codexHome, "skills"),
|
|
409
|
+
detectInstalled: async () => {
|
|
410
|
+
return existsSync$1(codexHome) || existsSync$1("/etc/codex");
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
cursor: {
|
|
414
|
+
name: "cursor",
|
|
415
|
+
displayName: "Cursor",
|
|
416
|
+
skillsDir: ".agents/skills",
|
|
417
|
+
globalSkillsDir: join$1(home, ".cursor/skills"),
|
|
418
|
+
detectInstalled: async () => {
|
|
419
|
+
return existsSync$1(join$1(home, ".cursor"));
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
"gemini-cli": {
|
|
423
|
+
name: "gemini-cli",
|
|
424
|
+
displayName: "Gemini CLI",
|
|
425
|
+
skillsDir: ".agents/skills",
|
|
426
|
+
globalSkillsDir: join$1(home, ".gemini/skills"),
|
|
427
|
+
detectInstalled: async () => {
|
|
428
|
+
return existsSync$1(join$1(home, ".gemini"));
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
"github-copilot": {
|
|
432
|
+
name: "github-copilot",
|
|
433
|
+
displayName: "GitHub Copilot",
|
|
434
|
+
skillsDir: ".agents/skills",
|
|
435
|
+
globalSkillsDir: join$1(home, ".copilot/skills"),
|
|
436
|
+
detectInstalled: async () => {
|
|
437
|
+
return existsSync$1(join$1(home, ".copilot"));
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
"iflow-cli": {
|
|
441
|
+
name: "iflow-cli",
|
|
442
|
+
displayName: "iFlow CLI",
|
|
443
|
+
skillsDir: ".iflow/skills",
|
|
444
|
+
globalSkillsDir: join$1(home, ".iflow/skills"),
|
|
445
|
+
detectInstalled: async () => {
|
|
446
|
+
return existsSync$1(join$1(home, ".iflow"));
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
"kimi-cli": {
|
|
450
|
+
name: "kimi-cli",
|
|
451
|
+
displayName: "Kimi Code CLI",
|
|
452
|
+
skillsDir: ".agents/skills",
|
|
453
|
+
globalSkillsDir: join$1(home, ".config/agents/skills"),
|
|
454
|
+
detectInstalled: async () => {
|
|
455
|
+
return existsSync$1(join$1(home, ".kimi"));
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
"kiro-cli": {
|
|
459
|
+
name: "kiro-cli",
|
|
460
|
+
displayName: "Kiro CLI",
|
|
461
|
+
skillsDir: ".kiro/skills",
|
|
462
|
+
globalSkillsDir: join$1(home, ".kiro/skills"),
|
|
463
|
+
detectInstalled: async () => {
|
|
464
|
+
return existsSync$1(join$1(home, ".kiro"));
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
opencode: {
|
|
468
|
+
name: "opencode",
|
|
469
|
+
displayName: "OpenCode",
|
|
470
|
+
skillsDir: ".agents/skills",
|
|
471
|
+
globalSkillsDir: join$1(configHome, "opencode/skills"),
|
|
472
|
+
detectInstalled: async () => {
|
|
473
|
+
return existsSync$1(join$1(configHome, "opencode"));
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
qoder: {
|
|
477
|
+
name: "qoder",
|
|
478
|
+
displayName: "Qoder",
|
|
479
|
+
skillsDir: ".qoder/skills",
|
|
480
|
+
globalSkillsDir: join$1(home, ".qoder/skills"),
|
|
481
|
+
detectInstalled: async () => {
|
|
482
|
+
return existsSync$1(join$1(home, ".qoder"));
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
"qwen-code": {
|
|
486
|
+
name: "qwen-code",
|
|
487
|
+
displayName: "Qwen Code",
|
|
488
|
+
skillsDir: ".qwen/skills",
|
|
489
|
+
globalSkillsDir: join$1(home, ".qwen/skills"),
|
|
490
|
+
detectInstalled: async () => {
|
|
491
|
+
return existsSync$1(join$1(home, ".qwen"));
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
trae: {
|
|
495
|
+
name: "trae",
|
|
496
|
+
displayName: "Trae",
|
|
497
|
+
skillsDir: ".trae/skills",
|
|
498
|
+
globalSkillsDir: join$1(home, ".trae/skills"),
|
|
499
|
+
detectInstalled: async () => {
|
|
500
|
+
return existsSync$1(join$1(home, ".trae"));
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
"trae-cn": {
|
|
504
|
+
name: "trae-cn",
|
|
505
|
+
displayName: "Trae CN",
|
|
506
|
+
skillsDir: ".trae/skills",
|
|
507
|
+
globalSkillsDir: join$1(home, ".trae-cn/skills"),
|
|
508
|
+
detectInstalled: async () => {
|
|
509
|
+
return existsSync$1(join$1(home, ".trae-cn"));
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
universal: {
|
|
513
|
+
name: "universal",
|
|
514
|
+
displayName: "Universal",
|
|
515
|
+
skillsDir: ".agents/skills",
|
|
516
|
+
globalSkillsDir: join$1(configHome, "agents/skills"),
|
|
517
|
+
showInUniversalList: false,
|
|
518
|
+
detectInstalled: async () => false
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/core/search-multiselect.ts
|
|
523
|
+
const silentOutput = new Writable({ write(_chunk, _encoding, callback) {
|
|
524
|
+
callback();
|
|
525
|
+
} });
|
|
526
|
+
const S_STEP_ACTIVE = import_picocolors.default.green("◆");
|
|
527
|
+
const S_STEP_CANCEL = import_picocolors.default.red("■");
|
|
528
|
+
const S_STEP_SUBMIT = import_picocolors.default.green("◇");
|
|
529
|
+
const S_RADIO_ACTIVE = import_picocolors.default.green("●");
|
|
530
|
+
const S_RADIO_INACTIVE = import_picocolors.default.dim("○");
|
|
531
|
+
import_picocolors.default.green("✓");
|
|
532
|
+
const S_BULLET = import_picocolors.default.green("•");
|
|
533
|
+
const S_BAR = import_picocolors.default.dim("│");
|
|
534
|
+
const S_BAR_H = import_picocolors.default.dim("─");
|
|
535
|
+
const cancelSymbol = Symbol("cancel");
|
|
536
|
+
/**
|
|
537
|
+
* Interactive search multiselect prompt.
|
|
538
|
+
* Allows users to filter a long list by typing and select multiple items.
|
|
539
|
+
* Optionally supports a "locked" section that displays always-selected items.
|
|
540
|
+
*/
|
|
541
|
+
async function searchMultiselect(options) {
|
|
542
|
+
const { message, items, maxVisible = 8, initialSelected = [], required = false, lockedSection, leadingSpacer = 0 } = options;
|
|
543
|
+
return new Promise((resolve) => {
|
|
544
|
+
const rl = readline.createInterface({
|
|
545
|
+
input: process.stdin,
|
|
546
|
+
output: silentOutput,
|
|
547
|
+
terminal: !!process.stdin.isTTY
|
|
548
|
+
});
|
|
549
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
550
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
551
|
+
let query = "";
|
|
552
|
+
let cursor = 0;
|
|
553
|
+
const selected = new Set(initialSelected);
|
|
554
|
+
let lastRenderHeight = 0;
|
|
555
|
+
const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : [];
|
|
556
|
+
const filter = (item, q) => {
|
|
557
|
+
if (!q) return true;
|
|
558
|
+
const lowerQ = q.toLowerCase();
|
|
559
|
+
return item.label.toLowerCase().includes(lowerQ) || String(item.value).toLowerCase().includes(lowerQ);
|
|
560
|
+
};
|
|
561
|
+
const getFiltered = () => {
|
|
562
|
+
return items.filter((item) => filter(item, query));
|
|
563
|
+
};
|
|
564
|
+
const out = process.stderr.isTTY ? process.stderr : process.stdout;
|
|
565
|
+
const clearRender = () => {
|
|
566
|
+
if (lastRenderHeight > 0 && out.isTTY) for (let i = 0; i < lastRenderHeight; i++) out.write("\x1B[1A\x1B[2K");
|
|
567
|
+
};
|
|
568
|
+
const render = (state = "active") => {
|
|
569
|
+
clearRender();
|
|
570
|
+
const lines = [];
|
|
571
|
+
const filtered = getFiltered();
|
|
572
|
+
const icon = state === "active" ? S_STEP_ACTIVE : state === "cancel" ? S_STEP_CANCEL : S_STEP_SUBMIT;
|
|
573
|
+
for (let i = 0; i < leadingSpacer; i++) lines.push(`${S_BAR}`);
|
|
574
|
+
lines.push(`${icon} ${import_picocolors.default.bold(message)}`);
|
|
575
|
+
if (state === "active") {
|
|
576
|
+
if (lockedSection && lockedSection.items.length > 0) {
|
|
577
|
+
lines.push(`${S_BAR}`);
|
|
578
|
+
const lockedTitle = `${import_picocolors.default.bold(lockedSection.title)} ${import_picocolors.default.dim("── always included")}`;
|
|
579
|
+
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${lockedTitle} ${S_BAR_H.repeat(12)}`);
|
|
580
|
+
for (const item of lockedSection.items) lines.push(`${S_BAR} ${S_BULLET} ${import_picocolors.default.bold(item.label)}`);
|
|
581
|
+
lines.push(`${S_BAR}`);
|
|
582
|
+
lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`);
|
|
583
|
+
}
|
|
584
|
+
const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
|
|
585
|
+
lines.push(searchLine);
|
|
586
|
+
lines.push(`${S_BAR} ${import_picocolors.default.dim("↑↓ move, space select, enter confirm")}`);
|
|
587
|
+
lines.push(`${S_BAR}`);
|
|
588
|
+
const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible));
|
|
589
|
+
const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible);
|
|
590
|
+
const visibleItems = filtered.slice(visibleStart, visibleEnd);
|
|
591
|
+
if (filtered.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("No matches found")}`);
|
|
592
|
+
else {
|
|
593
|
+
for (let i = 0; i < visibleItems.length; i++) {
|
|
594
|
+
const item = visibleItems[i];
|
|
595
|
+
const actualIndex = visibleStart + i;
|
|
596
|
+
const isSelected = selected.has(item.value);
|
|
597
|
+
const isCursor = actualIndex === cursor;
|
|
598
|
+
const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
|
|
599
|
+
const label = isCursor ? import_picocolors.default.underline(item.label) : item.label;
|
|
600
|
+
const hint = item.hint ? import_picocolors.default.dim(` (${item.hint})`) : "";
|
|
601
|
+
const prefix = isCursor ? import_picocolors.default.cyan("❯") : " ";
|
|
602
|
+
lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`);
|
|
603
|
+
}
|
|
604
|
+
const hiddenBefore = visibleStart;
|
|
605
|
+
const hiddenAfter = filtered.length - visibleEnd;
|
|
606
|
+
if (hiddenBefore > 0 || hiddenAfter > 0) {
|
|
607
|
+
const parts = [];
|
|
608
|
+
if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`);
|
|
609
|
+
if (hiddenAfter > 0) parts.push(`↓ ${hiddenAfter} more`);
|
|
610
|
+
lines.push(`${S_BAR} ${import_picocolors.default.dim(parts.join(" "))}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
lines.push(`${S_BAR}`);
|
|
614
|
+
const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
|
|
615
|
+
if (allSelectedLabels.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("Selected: (none)")}`);
|
|
616
|
+
else {
|
|
617
|
+
const summary = allSelectedLabels.length <= 3 ? allSelectedLabels.join(", ") : `${allSelectedLabels.slice(0, 3).join(", ")} +${allSelectedLabels.length - 3} more`;
|
|
618
|
+
lines.push(`${S_BAR} ${import_picocolors.default.green("Selected:")} ${summary}`);
|
|
619
|
+
}
|
|
620
|
+
lines.push(`${import_picocolors.default.dim("└")}`);
|
|
621
|
+
} else if (state === "submit") {
|
|
622
|
+
const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
|
|
623
|
+
lines.push(`${S_BAR} ${import_picocolors.default.dim(allSelectedLabels.join(", "))}`);
|
|
624
|
+
} else if (state === "cancel") lines.push(`${S_BAR} ${import_picocolors.default.strikethrough(import_picocolors.default.dim("Cancelled"))}`);
|
|
625
|
+
out.write(lines.join("\n") + "\n");
|
|
626
|
+
lastRenderHeight = lines.length;
|
|
627
|
+
};
|
|
628
|
+
const cleanup = () => {
|
|
629
|
+
process.stdin.removeListener("keypress", keypressHandler);
|
|
630
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
631
|
+
rl.close();
|
|
632
|
+
};
|
|
633
|
+
const submit = () => {
|
|
634
|
+
if (required && selected.size === 0 && lockedValues.length === 0) return;
|
|
635
|
+
render("submit");
|
|
636
|
+
cleanup();
|
|
637
|
+
resolve([...lockedValues, ...Array.from(selected)]);
|
|
638
|
+
};
|
|
639
|
+
const cancel = () => {
|
|
640
|
+
render("cancel");
|
|
641
|
+
cleanup();
|
|
642
|
+
resolve(cancelSymbol);
|
|
643
|
+
};
|
|
644
|
+
const keypressHandler = (_str, key) => {
|
|
645
|
+
if (!key) return;
|
|
646
|
+
const filtered = getFiltered();
|
|
647
|
+
if (key.name === "return") {
|
|
648
|
+
submit();
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
652
|
+
cancel();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (key.name === "up") {
|
|
656
|
+
cursor = Math.max(0, cursor - 1);
|
|
657
|
+
render();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (key.name === "down") {
|
|
661
|
+
cursor = Math.min(filtered.length - 1, cursor + 1);
|
|
662
|
+
render();
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (key.name === "space") {
|
|
666
|
+
const item = filtered[cursor];
|
|
667
|
+
if (item) if (selected.has(item.value)) selected.delete(item.value);
|
|
668
|
+
else selected.add(item.value);
|
|
669
|
+
render();
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (key.name === "backspace") {
|
|
673
|
+
query = query.slice(0, -1);
|
|
674
|
+
cursor = 0;
|
|
675
|
+
render();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
|
|
679
|
+
query += key.sequence;
|
|
680
|
+
cursor = 0;
|
|
681
|
+
render();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
process.stdin.on("keypress", keypressHandler);
|
|
686
|
+
render();
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
//#endregion
|
|
690
|
+
//#region src/core/constants.ts
|
|
691
|
+
const AGENTS_DIR = ".agents";
|
|
692
|
+
const SKILLS_SUBDIR = "skills";
|
|
693
|
+
//#endregion
|
|
694
|
+
//#region src/core/installer.ts
|
|
695
|
+
/**
|
|
696
|
+
* Validates that a path is within an expected base directory
|
|
697
|
+
* @param basePath - The expected base directory
|
|
698
|
+
* @param targetPath - The path to validate
|
|
699
|
+
* @returns true if targetPath is within basePath
|
|
700
|
+
*/
|
|
701
|
+
function isPathSafe(basePath, targetPath) {
|
|
702
|
+
const normalizedBase = normalize(resolve(basePath));
|
|
703
|
+
const normalizedTarget = normalize(resolve(targetPath));
|
|
704
|
+
return normalizedTarget.startsWith(normalizedBase + sep) || normalizedTarget === normalizedBase;
|
|
705
|
+
}
|
|
706
|
+
async function resolveParentSymlinks(path) {
|
|
707
|
+
const resolved = resolve(path);
|
|
708
|
+
const dir = dirname(resolved);
|
|
709
|
+
const base = basename(resolved);
|
|
710
|
+
try {
|
|
711
|
+
return join$1(await realpath(dir), base);
|
|
712
|
+
} catch {
|
|
713
|
+
return resolved;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function resolveSymlinkTarget(linkPath, linkTarget) {
|
|
717
|
+
return resolve(dirname(linkPath), linkTarget);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Creates a symlink, handling cross-platform differences
|
|
721
|
+
* Returns true if symlink was created, false if fallback to copy is needed
|
|
722
|
+
*/
|
|
723
|
+
async function createSymlink(target, linkPath) {
|
|
724
|
+
try {
|
|
725
|
+
const resolvedTarget = resolve(target);
|
|
726
|
+
const resolvedLinkPath = resolve(linkPath);
|
|
727
|
+
const [realTarget, realLinkPath] = await Promise.all([realpath(resolvedTarget).catch(() => resolvedTarget), realpath(resolvedLinkPath).catch(() => resolvedLinkPath)]);
|
|
728
|
+
if (realTarget === realLinkPath) return true;
|
|
729
|
+
if (await resolveParentSymlinks(target) === await resolveParentSymlinks(linkPath)) return true;
|
|
730
|
+
try {
|
|
731
|
+
if ((await lstat(linkPath)).isSymbolicLink()) {
|
|
732
|
+
if (resolveSymlinkTarget(linkPath, await readlink(linkPath)) === resolvedTarget) return true;
|
|
733
|
+
await rm(linkPath);
|
|
734
|
+
} else await rm(linkPath, { recursive: true });
|
|
735
|
+
} catch (err) {
|
|
736
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ELOOP") try {
|
|
737
|
+
await rm(linkPath, { force: true });
|
|
738
|
+
} catch {}
|
|
739
|
+
}
|
|
740
|
+
const linkDir = dirname(linkPath);
|
|
741
|
+
await mkdir(linkDir, { recursive: true });
|
|
742
|
+
await symlink(relative(await resolveParentSymlinks(linkDir), target), linkPath, platform() === "win32" ? "junction" : void 0);
|
|
743
|
+
return true;
|
|
744
|
+
} catch {
|
|
745
|
+
return false;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* 清理并创建目录
|
|
750
|
+
* @param path - 目录路径
|
|
751
|
+
* @returns 创建成功返回 true
|
|
752
|
+
*/
|
|
753
|
+
async function cleanAndCreateDirectory(path) {
|
|
754
|
+
try {
|
|
755
|
+
await rm(path, {
|
|
756
|
+
recursive: true,
|
|
757
|
+
force: true
|
|
758
|
+
});
|
|
759
|
+
} catch {}
|
|
760
|
+
await mkdir(path, { recursive: true });
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* 下载并解压包
|
|
764
|
+
* @param asset - 包信息
|
|
765
|
+
* @param targetDir - 解压目标目录
|
|
766
|
+
* @returns 解压成功返回 true
|
|
767
|
+
*/
|
|
768
|
+
async function downloadAndExtractPackage(asset, targetDir) {
|
|
769
|
+
let hasPackage = false;
|
|
770
|
+
const version = asset.currentVersion?.version ?? "";
|
|
771
|
+
if (!asset.slug || !version) {
|
|
772
|
+
generateSkillMd(asset, targetDir);
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
const pkgBuffer = await downloadSkillPackage(asset.slug, version);
|
|
776
|
+
if (pkgBuffer && pkgBuffer.length > 0) hasPackage = await extractPackage(pkgBuffer, targetDir);
|
|
777
|
+
if (!hasPackage) {
|
|
778
|
+
info("No package available, generating from metadata...");
|
|
779
|
+
generateSkillMd(asset, targetDir);
|
|
780
|
+
console.log(" Generated: SKILL.md from metadata");
|
|
781
|
+
}
|
|
782
|
+
return hasPackage;
|
|
783
|
+
}
|
|
784
|
+
function getAgentBaseDir(agentType, global, cwd) {
|
|
785
|
+
const agent = agents[agentType];
|
|
786
|
+
const baseDir = global ? homedir$1() : cwd || process.cwd();
|
|
787
|
+
if (global) {
|
|
788
|
+
if (agent.globalSkillsDir === void 0) return join$1(baseDir, agent.skillsDir);
|
|
789
|
+
return agent.globalSkillsDir;
|
|
790
|
+
}
|
|
791
|
+
return join$1(baseDir, agent.skillsDir);
|
|
792
|
+
}
|
|
793
|
+
async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
794
|
+
const agent = agents[agentType];
|
|
795
|
+
const isGlobal = options.global ?? false;
|
|
796
|
+
const cwd = options.cwd || process.cwd();
|
|
797
|
+
const installMode = options.mode ?? "symlink";
|
|
798
|
+
if (isGlobal && agent.globalSkillsDir === void 0) return {
|
|
799
|
+
success: false,
|
|
800
|
+
path: "",
|
|
801
|
+
mode: installMode,
|
|
802
|
+
error: `${agent.displayName} does not support global skill installation`
|
|
803
|
+
};
|
|
804
|
+
const skillName = sanitizeName(skill.slug);
|
|
805
|
+
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
806
|
+
const canonicalDir = join$1(canonicalBase, skillName);
|
|
807
|
+
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
|
|
808
|
+
const agentDir = join$1(agentBase, skillName);
|
|
809
|
+
if (!isPathSafe(canonicalBase, canonicalDir)) return {
|
|
810
|
+
success: false,
|
|
811
|
+
path: agentDir,
|
|
812
|
+
mode: installMode,
|
|
813
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
814
|
+
};
|
|
815
|
+
if (!isPathSafe(agentBase, agentDir)) return {
|
|
816
|
+
success: false,
|
|
817
|
+
path: agentDir,
|
|
818
|
+
mode: installMode,
|
|
819
|
+
error: "Invalid skill name: potential path traversal detected"
|
|
820
|
+
};
|
|
821
|
+
async function populateSkillDir(targetDir) {
|
|
822
|
+
await downloadAndExtractPackage(skill, targetDir);
|
|
823
|
+
}
|
|
824
|
+
try {
|
|
825
|
+
if (installMode === "copy") {
|
|
826
|
+
await cleanAndCreateDirectory(agentDir);
|
|
827
|
+
await populateSkillDir(agentDir);
|
|
828
|
+
return {
|
|
829
|
+
success: true,
|
|
830
|
+
path: agentDir,
|
|
831
|
+
mode: "copy"
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
await cleanAndCreateDirectory(canonicalDir);
|
|
835
|
+
await populateSkillDir(canonicalDir);
|
|
836
|
+
if (!await createSymlink(canonicalDir, agentDir)) {
|
|
837
|
+
await cleanAndCreateDirectory(agentDir);
|
|
838
|
+
await cp(canonicalDir, agentDir, { recursive: true });
|
|
839
|
+
return {
|
|
840
|
+
success: true,
|
|
841
|
+
path: agentDir,
|
|
842
|
+
canonicalPath: canonicalDir,
|
|
843
|
+
mode: "symlink",
|
|
844
|
+
symlinkFailed: true
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
success: true,
|
|
849
|
+
path: agentDir,
|
|
850
|
+
canonicalPath: canonicalDir,
|
|
851
|
+
mode: "symlink"
|
|
852
|
+
};
|
|
853
|
+
} catch (error) {
|
|
854
|
+
return {
|
|
855
|
+
success: false,
|
|
856
|
+
path: agentDir,
|
|
857
|
+
mode: installMode,
|
|
858
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function sanitizeName(name) {
|
|
863
|
+
return name.toLowerCase().replace(/[^a-z0-9._]+/g, "-").replace(/^[.\-]+|[.\-]+$/g, "").substring(0, 255) || "unnamed-skill";
|
|
864
|
+
}
|
|
865
|
+
function getCanonicalSkillsDir(global, cwd) {
|
|
866
|
+
return join$1(global ? homedir$1() : cwd || process.cwd(), AGENTS_DIR, SKILLS_SUBDIR);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* 将包从 buffer 解压到目标目录
|
|
870
|
+
* 接口返回 zip 格式,兼容 tar.gz
|
|
871
|
+
* @param buffer - 待解压的 buffer
|
|
872
|
+
* @param targetDir - 解压目标目录
|
|
873
|
+
* @returns 解压成功返回 true
|
|
874
|
+
*/
|
|
875
|
+
async function extractPackage(buffer, targetDir) {
|
|
876
|
+
await mkdir(targetDir, { recursive: true });
|
|
877
|
+
const tmpFile = join$1(tmpdir(), `openclawmp-pkg-${process.pid}-${Date.now()}`);
|
|
878
|
+
await writeFile(tmpFile, buffer);
|
|
879
|
+
try {
|
|
880
|
+
try {
|
|
881
|
+
execSync(`unzip -o -q "${tmpFile}" -d "${targetDir}" 2>/dev/null`, { stdio: "pipe" });
|
|
882
|
+
const entries = await readdir(targetDir);
|
|
883
|
+
const dirs = [];
|
|
884
|
+
for (const e of entries) if ((await stat(join$1(targetDir, e))).isDirectory()) dirs.push(e);
|
|
885
|
+
if (dirs.length === 1 && entries.length === 1) {
|
|
886
|
+
const subdir = join$1(targetDir, dirs[0]);
|
|
887
|
+
for (const f of await readdir(subdir)) await rename(join$1(subdir, f), join$1(targetDir, f));
|
|
888
|
+
await rm(subdir, { recursive: true });
|
|
889
|
+
}
|
|
890
|
+
return true;
|
|
891
|
+
} catch {
|
|
892
|
+
try {
|
|
893
|
+
execSync(`tar xzf "${tmpFile}" -C "${targetDir}" --strip-components=1 2>/dev/null`, { stdio: "pipe" });
|
|
894
|
+
return true;
|
|
895
|
+
} catch {
|
|
896
|
+
try {
|
|
897
|
+
execSync(`tar xzf "${tmpFile}" -C "${targetDir}" 2>/dev/null`, { stdio: "pipe" });
|
|
898
|
+
return true;
|
|
899
|
+
} catch {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
} finally {
|
|
905
|
+
try {
|
|
906
|
+
await unlink(tmpFile);
|
|
907
|
+
} catch {}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function generateSkillMd(asset, targetDir) {
|
|
911
|
+
const tags = (asset.tags || []).join(", ");
|
|
912
|
+
const version = asset.currentVersion ?? asset.version ?? "";
|
|
913
|
+
const description = asset.summary || asset.description || "";
|
|
914
|
+
const content = `---
|
|
915
|
+
name: ${asset.slug || asset.name || ""}
|
|
916
|
+
display-name: ${asset.displayName || ""}
|
|
917
|
+
description: ${description}
|
|
918
|
+
version: ${version}
|
|
919
|
+
author: ${asset.author?.name || ""}
|
|
920
|
+
author-id: ${asset.author?.id || ""}
|
|
921
|
+
tags: ${tags}
|
|
922
|
+
category: ${asset.category || ""}
|
|
923
|
+
---
|
|
924
|
+
|
|
925
|
+
# ${asset.displayName || asset.slug || asset.name || "Skill"}
|
|
926
|
+
|
|
927
|
+
${description}
|
|
928
|
+
|
|
929
|
+
${asset.readme || ""}
|
|
930
|
+
`;
|
|
931
|
+
writeFileSync$1(path$1.join(targetDir, "SKILL.md"), content);
|
|
932
|
+
}
|
|
933
|
+
//#endregion
|
|
934
|
+
//#region src/commands/install.ts
|
|
935
|
+
async function promptForAgents(message, choices, initialValues) {
|
|
936
|
+
return await searchMultiselect({
|
|
937
|
+
message,
|
|
938
|
+
items: choices,
|
|
939
|
+
leadingSpacer: 1,
|
|
940
|
+
initialSelected: initialValues
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
function cancelAndExit(message) {
|
|
944
|
+
p.cancel(message);
|
|
945
|
+
process.exit(0);
|
|
946
|
+
}
|
|
947
|
+
function errorAndExit(message, note) {
|
|
948
|
+
p.log.error(message);
|
|
949
|
+
if (note) p.note(note.body, note.title);
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
function buildAgentChoices(globalInstall) {
|
|
953
|
+
return Object.entries(agents).map(([key, config]) => ({
|
|
954
|
+
value: key,
|
|
955
|
+
label: config.displayName,
|
|
956
|
+
hint: globalInstall ? config.globalSkillsDir ?? config.skillsDir : config.skillsDir
|
|
957
|
+
}));
|
|
958
|
+
}
|
|
959
|
+
function getDefaultAgents(choices) {
|
|
960
|
+
return ["claude-code", "openclaw"].filter((agent) => choices.some((choice) => choice.value === agent));
|
|
961
|
+
}
|
|
962
|
+
async function resolveSkillName(inputSkill) {
|
|
963
|
+
if (inputSkill?.trim()) return inputSkill.trim();
|
|
964
|
+
const input = await p.text({
|
|
965
|
+
message: "Enter skill to install",
|
|
966
|
+
placeholder: "my-skill",
|
|
967
|
+
validate: (value) => {
|
|
968
|
+
if (!value?.trim()) return "Please enter skill name";
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
if (p.isCancel(input)) cancelAndExit("Cancelled");
|
|
972
|
+
return input.trim();
|
|
973
|
+
}
|
|
974
|
+
async function resolveTargetAgents(options) {
|
|
975
|
+
const allAgentChoices = buildAgentChoices(Boolean(options.global ?? options.yes));
|
|
976
|
+
if (options.yes) {
|
|
977
|
+
const defaults = getDefaultAgents(allAgentChoices);
|
|
978
|
+
if (defaults.length === 0) cancelAndExit("No default agents available");
|
|
979
|
+
return defaults;
|
|
980
|
+
}
|
|
981
|
+
const selected = await promptForAgents("Which agents do you want to install to?", allAgentChoices, getDefaultAgents(allAgentChoices));
|
|
982
|
+
if (p.isCancel(selected) || selected.length === 0) cancelAndExit("Installation cancelled");
|
|
983
|
+
return selected;
|
|
984
|
+
}
|
|
985
|
+
async function resolveInstallScope(options, targetAgents) {
|
|
986
|
+
const supportsGlobal = targetAgents.some((agent) => agents[agent].globalSkillsDir !== void 0);
|
|
987
|
+
if (options.global !== void 0 || options.yes || !supportsGlobal) return options.global ?? (options.yes ? true : false);
|
|
988
|
+
const scope = await p.select({
|
|
989
|
+
message: "Installation scope",
|
|
990
|
+
options: [{
|
|
991
|
+
value: false,
|
|
992
|
+
label: "Project",
|
|
993
|
+
hint: "Install in current directory (committed with your project)"
|
|
994
|
+
}, {
|
|
995
|
+
value: true,
|
|
996
|
+
label: "Global",
|
|
997
|
+
hint: "Install in home directory (available across all projects)"
|
|
998
|
+
}]
|
|
999
|
+
});
|
|
1000
|
+
if (p.isCancel(scope)) cancelAndExit("Installation cancelled");
|
|
1001
|
+
return scope;
|
|
1002
|
+
}
|
|
1003
|
+
const run = async (args, options = {}) => {
|
|
1004
|
+
p.intro(chalk.bold("skill-atlas install"));
|
|
1005
|
+
const skill = await resolveSkillName(args[0]);
|
|
1006
|
+
const s = p.spinner();
|
|
1007
|
+
s.start(`Searching for ${chalk.bold(skill)}...`);
|
|
1008
|
+
try {
|
|
1009
|
+
const asset = await findAsset(skill);
|
|
1010
|
+
if (!asset) {
|
|
1011
|
+
s.stop();
|
|
1012
|
+
errorAndExit(`Not found: ${chalk.bold(skill)}`, {
|
|
1013
|
+
title: "Suggest",
|
|
1014
|
+
body: `Try: skill-atlas search ${skill}`
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
const displayName = asset.displayName || asset.slug;
|
|
1018
|
+
const version = asset.currentVersion.version ?? "0.0.0";
|
|
1019
|
+
s.stop(`${chalk.bold(displayName)} ${chalk.dim(`v${version}`)}`);
|
|
1020
|
+
const targetAgents = await resolveTargetAgents(options);
|
|
1021
|
+
const installGlobally = await resolveInstallScope(options, targetAgents);
|
|
1022
|
+
const selectedLabels = targetAgents.map((a) => agents[a].displayName);
|
|
1023
|
+
p.log.message(chalk.green("Selected:") + " " + selectedLabels.join(", "));
|
|
1024
|
+
s.start("Installing skills...");
|
|
1025
|
+
const installMode = options.copy ? "copy" : "symlink";
|
|
1026
|
+
const installResults = [];
|
|
1027
|
+
for (const agent of targetAgents) {
|
|
1028
|
+
const result = await installWellKnownSkillForAgent(asset, agent, {
|
|
1029
|
+
global: installGlobally,
|
|
1030
|
+
mode: installMode
|
|
1031
|
+
});
|
|
1032
|
+
if (!result.success) throw new Error(`Failed to install to ${agents[agent].displayName}: ${result.error || "Unknown error"}`);
|
|
1033
|
+
installResults.push({
|
|
1034
|
+
agent,
|
|
1035
|
+
path: result.path
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
s.stop("Skills installed successfully");
|
|
1039
|
+
const title = import_picocolors.default.green("Installed 1 skill");
|
|
1040
|
+
const resultLines = installResults.map((r) => ` ${agents[r.agent].displayName}: ${r.path}`);
|
|
1041
|
+
p.note(resultLines.join("\n"), title);
|
|
1042
|
+
p.outro(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Skill ready. Review before use."));
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
s.stop();
|
|
1045
|
+
p.log.error(`Install failed: ${err.message}`);
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
var install_default = { run };
|
|
1050
|
+
//#endregion
|
|
1051
|
+
//#region src/commands/search.ts
|
|
1052
|
+
function getVersion(skill) {
|
|
1053
|
+
const cv = skill.currentVersion;
|
|
1054
|
+
if (typeof cv === "string") return cv;
|
|
1055
|
+
if (cv && typeof cv === "object" && "version" in cv) return cv.version ?? "—";
|
|
1056
|
+
return "—";
|
|
1057
|
+
}
|
|
1058
|
+
function printSkillList(items, total, page, pageSize) {
|
|
1059
|
+
const maxSlugLen = Math.max(...items.map((s) => (s.slug || "").length), 6);
|
|
1060
|
+
const maxDescLen = Math.max(...items.map((s) => Math.min((s.displayName || "—").length, 30)), 8);
|
|
1061
|
+
const header = ` ${chalk.bold("Description".padEnd(maxDescLen))} ${chalk.bold("SkillName".padEnd(maxSlugLen))} ${chalk.bold("Version")}`;
|
|
1062
|
+
const separator = " " + "-".repeat(maxSlugLen + maxDescLen + 12);
|
|
1063
|
+
const resultLines = items.map((s) => {
|
|
1064
|
+
const slug = (s.slug || "—").padEnd(maxSlugLen);
|
|
1065
|
+
const descPadded = ((s.displayName || "—").length > 30 ? (s.displayName || "—").slice(0, 27) + "..." : s.displayName || "—").padEnd(maxDescLen);
|
|
1066
|
+
const version = getVersion(s);
|
|
1067
|
+
return ` ${chalk.white(descPadded)} ${chalk.cyan.bold(slug)} ${chalk.dim(`v${version}`)}`;
|
|
1068
|
+
});
|
|
1069
|
+
p.note([
|
|
1070
|
+
header,
|
|
1071
|
+
separator,
|
|
1072
|
+
...resultLines
|
|
1073
|
+
].join("\n"), chalk.green(`Found ${total} skill(s)`));
|
|
1074
|
+
const start = (page - 1) * pageSize + 1;
|
|
1075
|
+
const end = Math.min(page * pageSize, total);
|
|
1076
|
+
p.log.message(chalk.dim(`Showing ${start}-${end} of ${total}`));
|
|
1077
|
+
p.log.message(chalk.green(`Install: npx skill-atlas install <skillName>`));
|
|
1078
|
+
}
|
|
1079
|
+
async function runSearch(options) {
|
|
1080
|
+
const { keyword } = options;
|
|
1081
|
+
try {
|
|
1082
|
+
const result = await searchSkills({ q: keyword });
|
|
1083
|
+
const { items, total } = result;
|
|
1084
|
+
if (items.length === 0) {
|
|
1085
|
+
p.log.error(`No skills found matching "${chalk.bold(keyword)}"`);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
printSkillList(items, total, result.page, result.pageSize);
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
p.log.error(`Search failed: ${err.message}`);
|
|
1091
|
+
process.exit(1);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
//#endregion
|
|
1095
|
+
//#region src/core/logger.ts
|
|
1096
|
+
const consola = createConsola();
|
|
1097
|
+
/**
|
|
1098
|
+
* 设置 verbose 模式(启用 debug 输出)
|
|
1099
|
+
* consola level: 3=info, 4=debug
|
|
1100
|
+
*/
|
|
1101
|
+
function setVerbose(enabled) {
|
|
1102
|
+
consola.level = enabled ? 4 : 3;
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* 检查是否启用 verbose
|
|
1106
|
+
*/
|
|
1107
|
+
function isVerbose() {
|
|
1108
|
+
return consola.level >= 4;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* 错误别名(兼容 err 调用)
|
|
1112
|
+
*/
|
|
1113
|
+
function err(...args) {
|
|
1114
|
+
consola.error.apply(consola, args);
|
|
1115
|
+
}
|
|
1116
|
+
var logger_default = {
|
|
1117
|
+
setVerbose,
|
|
1118
|
+
isVerbose,
|
|
1119
|
+
info: ((...a) => consola.info(...a)),
|
|
1120
|
+
success: ((...a) => consola.success(...a)),
|
|
1121
|
+
warn: ((...a) => consola.warn(...a)),
|
|
1122
|
+
error: ((...a) => consola.error(...a)),
|
|
1123
|
+
err,
|
|
1124
|
+
debug: ((...a) => consola.debug(...a)),
|
|
1125
|
+
log: ((...a) => consola.log(...a))
|
|
1126
|
+
};
|
|
1127
|
+
//#endregion
|
|
1128
|
+
export { checkForUpdate, install_default as install, logger_default as logger, runSearch };
|