skill-atlas-cli 0.1.24 → 0.1.26
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/bin/cli.js +4 -2
- package/lib/index.d.ts +2 -0
- package/lib/index.js +144 -29
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -41,10 +41,12 @@ async function main() {
|
|
|
41
41
|
}
|
|
42
42
|
await runSearch({ keyword: keyword.trim() });
|
|
43
43
|
});
|
|
44
|
-
cli.command("install [name]", "安装 skill(支持 skill 名称)").option("-y, --yes", "非交互模式,默认安装到全局").option("-g, --global", "安装到全局目录").action(async (name, options) => {
|
|
44
|
+
cli.command("install [name]", "安装 skill(支持 skill 名称)").option("-y, --yes", "非交互模式,默认安装到全局").option("-g, --global", "安装到全局目录").option("-p, --path <dir>", "安装到自定义路径(目录),如 -p /path/to/skills 或 -p .qoder/skills").option("-a, --agent <agent...>", "非 TTY 模式下指定目标 agent(如 cursor、openclaw),可传多个").action(async (name, options) => {
|
|
45
45
|
await install.run(name ? [name] : [], {
|
|
46
46
|
yes: options.yes,
|
|
47
|
-
global: options.global
|
|
47
|
+
global: options.global,
|
|
48
|
+
agent: options.agent,
|
|
49
|
+
path: options.path
|
|
48
50
|
});
|
|
49
51
|
});
|
|
50
52
|
cli.help();
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -407,8 +407,8 @@ const agents = {
|
|
|
407
407
|
cline: {
|
|
408
408
|
name: "cline",
|
|
409
409
|
displayName: "Cline",
|
|
410
|
-
skillsDir: ".
|
|
411
|
-
globalSkillsDir: join$1(home, ".
|
|
410
|
+
skillsDir: ".cline/skills",
|
|
411
|
+
globalSkillsDir: join$1(home, ".cline", "skills"),
|
|
412
412
|
detectInstalled: async () => {
|
|
413
413
|
return existsSync$1(join$1(home, ".cline"));
|
|
414
414
|
}
|
|
@@ -452,7 +452,7 @@ const agents = {
|
|
|
452
452
|
"github-copilot": {
|
|
453
453
|
name: "github-copilot",
|
|
454
454
|
displayName: "GitHub Copilot",
|
|
455
|
-
skillsDir: ".
|
|
455
|
+
skillsDir: ".github/skills",
|
|
456
456
|
globalSkillsDir: join$1(home, ".copilot/skills"),
|
|
457
457
|
detectInstalled: async () => {
|
|
458
458
|
return existsSync$1(join$1(home, ".copilot"));
|
|
@@ -503,6 +503,15 @@ const agents = {
|
|
|
503
503
|
return existsSync$1(join$1(home, ".qoder"));
|
|
504
504
|
}
|
|
505
505
|
},
|
|
506
|
+
qoderwork: {
|
|
507
|
+
name: "qoderwork",
|
|
508
|
+
displayName: "QoderWork",
|
|
509
|
+
skillsDir: ".qoderwork/skills",
|
|
510
|
+
globalSkillsDir: join$1(home, ".qoderwork/skills"),
|
|
511
|
+
detectInstalled: async () => {
|
|
512
|
+
return existsSync$1(join$1(home, ".qoderwork"));
|
|
513
|
+
}
|
|
514
|
+
},
|
|
506
515
|
"qwen-code": {
|
|
507
516
|
name: "qwen-code",
|
|
508
517
|
displayName: "Qwen Code",
|
|
@@ -530,6 +539,15 @@ const agents = {
|
|
|
530
539
|
return existsSync$1(join$1(home, ".trae-cn"));
|
|
531
540
|
}
|
|
532
541
|
},
|
|
542
|
+
windsurf: {
|
|
543
|
+
name: "windsurf",
|
|
544
|
+
displayName: "Windsurf",
|
|
545
|
+
skillsDir: ".windsurf/skills",
|
|
546
|
+
globalSkillsDir: join$1(home, ".codeium/windsurf/skills"),
|
|
547
|
+
detectInstalled: async () => {
|
|
548
|
+
return existsSync$1(join$1(home, ".codeium/windsurf"));
|
|
549
|
+
}
|
|
550
|
+
},
|
|
533
551
|
universal: {
|
|
534
552
|
name: "universal",
|
|
535
553
|
displayName: "Universal",
|
|
@@ -816,17 +834,45 @@ async function installWellKnownSkillForAgent(skill, agentType, options = {}) {
|
|
|
816
834
|
const isGlobal = options.global ?? false;
|
|
817
835
|
const cwd = options.cwd || process.cwd();
|
|
818
836
|
const installMode = options.mode ?? "symlink";
|
|
837
|
+
const customPath = options.path?.trim();
|
|
838
|
+
const skillName = sanitizeName(skill.slug);
|
|
839
|
+
let agentDir;
|
|
840
|
+
if (customPath) {
|
|
841
|
+
const resolvedPath = resolve(customPath);
|
|
842
|
+
agentDir = join$1(resolvedPath, skillName);
|
|
843
|
+
if (!isPathSafe(resolvedPath, agentDir)) return {
|
|
844
|
+
success: false,
|
|
845
|
+
path: agentDir,
|
|
846
|
+
mode: "copy",
|
|
847
|
+
error: "Invalid path: potential path traversal detected"
|
|
848
|
+
};
|
|
849
|
+
try {
|
|
850
|
+
await cleanAndCreateDirectory(agentDir);
|
|
851
|
+
await downloadAndExtractPackage(skill, agentDir);
|
|
852
|
+
return {
|
|
853
|
+
success: true,
|
|
854
|
+
path: agentDir,
|
|
855
|
+
mode: "copy"
|
|
856
|
+
};
|
|
857
|
+
} catch (error) {
|
|
858
|
+
return {
|
|
859
|
+
success: false,
|
|
860
|
+
path: agentDir,
|
|
861
|
+
mode: "copy",
|
|
862
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
}
|
|
819
866
|
if (isGlobal && agent.globalSkillsDir === void 0) return {
|
|
820
867
|
success: false,
|
|
821
868
|
path: "",
|
|
822
869
|
mode: installMode,
|
|
823
870
|
error: `${agent.displayName} does not support global skill installation`
|
|
824
871
|
};
|
|
825
|
-
const skillName = sanitizeName(skill.slug);
|
|
826
872
|
const canonicalBase = getCanonicalSkillsDir(isGlobal, cwd);
|
|
827
873
|
const canonicalDir = join$1(canonicalBase, skillName);
|
|
828
874
|
const agentBase = getAgentBaseDir(agentType, isGlobal, cwd);
|
|
829
|
-
|
|
875
|
+
agentDir = join$1(agentBase, skillName);
|
|
830
876
|
if (!isPathSafe(canonicalBase, canonicalDir)) return {
|
|
831
877
|
success: false,
|
|
832
878
|
path: agentDir,
|
|
@@ -953,6 +999,10 @@ ${asset.readme || ""}
|
|
|
953
999
|
}
|
|
954
1000
|
//#endregion
|
|
955
1001
|
//#region src/commands/install.ts
|
|
1002
|
+
/** 非 TTY 环境(如 OpenClaw、CI、管道)下自动启用非交互模式,避免阻塞 */
|
|
1003
|
+
function isNonInteractiveEnv() {
|
|
1004
|
+
return !process.stdin.isTTY;
|
|
1005
|
+
}
|
|
956
1006
|
async function promptForAgents(message, choices, initialValues) {
|
|
957
1007
|
return await searchMultiselect({
|
|
958
1008
|
message,
|
|
@@ -980,8 +1030,12 @@ function buildAgentChoices(globalInstall) {
|
|
|
980
1030
|
function getDefaultAgents(choices) {
|
|
981
1031
|
return ["claude-code", "openclaw"].filter((agent) => choices.some((choice) => choice.value === agent));
|
|
982
1032
|
}
|
|
983
|
-
async function resolveSkillName(inputSkill) {
|
|
1033
|
+
async function resolveSkillName(inputSkill, nonInteractive) {
|
|
984
1034
|
if (inputSkill?.trim()) return inputSkill.trim();
|
|
1035
|
+
if (nonInteractive) errorAndExit("Please provide skill name", {
|
|
1036
|
+
title: "Usage",
|
|
1037
|
+
body: "skill-atlas install <skillName>\n\nExample: skill-atlas install my-skill"
|
|
1038
|
+
});
|
|
985
1039
|
const input = await p.text({
|
|
986
1040
|
message: "Enter skill to install",
|
|
987
1041
|
placeholder: "my-skill",
|
|
@@ -992,20 +1046,52 @@ async function resolveSkillName(inputSkill) {
|
|
|
992
1046
|
if (p.isCancel(input)) cancelAndExit("Cancelled");
|
|
993
1047
|
return input.trim();
|
|
994
1048
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1049
|
+
/** 校验并解析传入的 agent 名称(纯函数,可单独测试) */
|
|
1050
|
+
function parseAgentNames(input) {
|
|
1051
|
+
const names = Array.isArray(input) ? input : input ? [input] : [];
|
|
1052
|
+
if (!names.length) return [];
|
|
1053
|
+
const validKeys = new Set(Object.keys(agents));
|
|
1054
|
+
const result = [];
|
|
1055
|
+
const invalid = [];
|
|
1056
|
+
for (const name of names) {
|
|
1057
|
+
const normalized = name.trim().toLowerCase();
|
|
1058
|
+
if (validKeys.has(normalized)) result.push(normalized);
|
|
1059
|
+
else invalid.push(name);
|
|
1060
|
+
}
|
|
1061
|
+
if (invalid.length > 0) return { invalid };
|
|
1062
|
+
return result;
|
|
1063
|
+
}
|
|
1064
|
+
function resolveAgentNamesFromOptions(input) {
|
|
1065
|
+
const parsed = parseAgentNames(input);
|
|
1066
|
+
if (Array.isArray(parsed)) return parsed;
|
|
1067
|
+
errorAndExit(`Unknown agent(s): ${parsed.invalid.join(", ")}`, {
|
|
1068
|
+
title: "Supported agents",
|
|
1069
|
+
body: `Examples: ${[
|
|
1070
|
+
"cursor",
|
|
1071
|
+
"openclaw",
|
|
1072
|
+
"claude-code",
|
|
1073
|
+
"cline",
|
|
1074
|
+
"codex"
|
|
1075
|
+
].join(", ")}\nFull list: ${Object.keys(agents).join(", ")}`
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
async function resolveTargetAgents(options, nonInteractive) {
|
|
1079
|
+
const allAgentChoices = buildAgentChoices(Boolean(options.global ?? options.yes ?? nonInteractive));
|
|
1080
|
+
if (options.yes || nonInteractive) {
|
|
1081
|
+
const specified = resolveAgentNamesFromOptions(options.agent);
|
|
1082
|
+
if (specified.length > 0) return specified;
|
|
998
1083
|
const defaults = getDefaultAgents(allAgentChoices);
|
|
999
1084
|
if (defaults.length === 0) cancelAndExit("No default agents available");
|
|
1085
|
+
p.log.message(chalk.dim("未指定 --agent,使用默认 agents。可用 --agent cursor 等指定安装目标"));
|
|
1000
1086
|
return defaults;
|
|
1001
1087
|
}
|
|
1002
1088
|
const selected = await promptForAgents("Which agents do you want to install to?", allAgentChoices, getDefaultAgents(allAgentChoices));
|
|
1003
1089
|
if (p.isCancel(selected) || selected.length === 0) cancelAndExit("Installation cancelled");
|
|
1004
1090
|
return selected;
|
|
1005
1091
|
}
|
|
1006
|
-
async function resolveInstallScope(options, targetAgents) {
|
|
1092
|
+
async function resolveInstallScope(options, targetAgents, nonInteractive) {
|
|
1007
1093
|
const supportsGlobal = targetAgents.some((agent) => agents[agent].globalSkillsDir !== void 0);
|
|
1008
|
-
if (options.global !== void 0 || options.yes || !supportsGlobal) return options.global ?? (options.yes ? true : false);
|
|
1094
|
+
if (options.global !== void 0 || options.yes || nonInteractive || !supportsGlobal) return options.global ?? (options.yes || nonInteractive ? true : false);
|
|
1009
1095
|
const scope = await p.select({
|
|
1010
1096
|
message: "Installation scope",
|
|
1011
1097
|
options: [{
|
|
@@ -1021,15 +1107,30 @@ async function resolveInstallScope(options, targetAgents) {
|
|
|
1021
1107
|
if (p.isCancel(scope)) cancelAndExit("Installation cancelled");
|
|
1022
1108
|
return scope;
|
|
1023
1109
|
}
|
|
1110
|
+
/** 非 TTY 下使用静态输出替代 spinner,避免动画帧被逐行打印造成刷屏 */
|
|
1111
|
+
function createProgressReporter(nonInteractive) {
|
|
1112
|
+
if (nonInteractive) return {
|
|
1113
|
+
start: (msg) => p.log.message(msg),
|
|
1114
|
+
stop: (msg) => {
|
|
1115
|
+
if (msg) p.log.message(msg);
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
const s = p.spinner();
|
|
1119
|
+
return {
|
|
1120
|
+
start: (msg) => s.start(msg),
|
|
1121
|
+
stop: (msg) => s.stop(msg ?? "")
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1024
1124
|
const run = async (args, options = {}) => {
|
|
1125
|
+
const nonInteractive = options.yes || isNonInteractiveEnv();
|
|
1025
1126
|
p.intro(chalk.bold("skill-atlas install"));
|
|
1026
|
-
const skill = await resolveSkillName(args[0]);
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1127
|
+
const skill = await resolveSkillName(args[0], nonInteractive);
|
|
1128
|
+
const progress = createProgressReporter(nonInteractive);
|
|
1129
|
+
progress.start(`Searching for ${chalk.bold(skill)}...`);
|
|
1029
1130
|
try {
|
|
1030
1131
|
const asset = await findAsset(skill);
|
|
1031
1132
|
if (!asset) {
|
|
1032
|
-
|
|
1133
|
+
progress.stop();
|
|
1033
1134
|
errorAndExit(`Not found: ${chalk.bold(skill)}`, {
|
|
1034
1135
|
title: "Suggest",
|
|
1035
1136
|
body: `Try: skill-atlas search ${skill}`
|
|
@@ -1037,32 +1138,46 @@ const run = async (args, options = {}) => {
|
|
|
1037
1138
|
}
|
|
1038
1139
|
const displayName = asset.displayName || asset.slug;
|
|
1039
1140
|
const version = asset.currentVersion.version ?? "0.0.0";
|
|
1040
|
-
|
|
1041
|
-
const
|
|
1042
|
-
const installGlobally = await resolveInstallScope(options, targetAgents);
|
|
1043
|
-
const selectedLabels = targetAgents.map((a) => agents[a].displayName);
|
|
1044
|
-
p.log.message(chalk.green("Selected:") + " " + selectedLabels.join(", "));
|
|
1045
|
-
s.start("Installing skills...");
|
|
1141
|
+
progress.stop(`${chalk.bold(displayName)} ${chalk.dim(`v${version}`)}`);
|
|
1142
|
+
const customPath = options.path?.trim();
|
|
1046
1143
|
const installMode = options.copy ? "copy" : "symlink";
|
|
1047
1144
|
const installResults = [];
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1145
|
+
if (customPath) {
|
|
1146
|
+
progress.start(`${chalk.dim("Installing to")} ${customPath}...`);
|
|
1147
|
+
const result = await installWellKnownSkillForAgent(asset, "openclaw", {
|
|
1148
|
+
path: customPath,
|
|
1051
1149
|
mode: installMode
|
|
1052
1150
|
});
|
|
1053
|
-
if (!result.success) throw new Error(`Failed to install to ${
|
|
1151
|
+
if (!result.success) throw new Error(`Failed to install to ${customPath}: ${result.error || "Unknown error"}`);
|
|
1054
1152
|
installResults.push({
|
|
1055
|
-
agent,
|
|
1153
|
+
agent: "openclaw",
|
|
1056
1154
|
path: result.path
|
|
1057
1155
|
});
|
|
1156
|
+
} else {
|
|
1157
|
+
const targetAgents = await resolveTargetAgents(options, nonInteractive);
|
|
1158
|
+
const installGlobally = await resolveInstallScope(options, targetAgents, nonInteractive);
|
|
1159
|
+
const selectedLabels = targetAgents.map((a) => agents[a].displayName);
|
|
1160
|
+
p.log.message(chalk.green("Selected:") + " " + selectedLabels.join(", "));
|
|
1161
|
+
progress.start("Installing skills...");
|
|
1162
|
+
for (const agent of targetAgents) {
|
|
1163
|
+
const result = await installWellKnownSkillForAgent(asset, agent, {
|
|
1164
|
+
global: installGlobally,
|
|
1165
|
+
mode: installMode
|
|
1166
|
+
});
|
|
1167
|
+
if (!result.success) throw new Error(`Failed to install to ${agents[agent].displayName}: ${result.error || "Unknown error"}`);
|
|
1168
|
+
installResults.push({
|
|
1169
|
+
agent,
|
|
1170
|
+
path: result.path
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1058
1173
|
}
|
|
1059
|
-
|
|
1174
|
+
progress.stop("Skills installed successfully");
|
|
1060
1175
|
const title = import_picocolors.default.green("Installed 1 skill");
|
|
1061
|
-
const resultLines = installResults.map((r) => ` ${agents[r.agent].displayName}: ${r.path}`);
|
|
1176
|
+
const resultLines = customPath ? [` ${customPath}: ${installResults[0].path}`] : installResults.map((r) => ` ${agents[r.agent].displayName}: ${r.path}`);
|
|
1062
1177
|
p.note(resultLines.join("\n"), title);
|
|
1063
1178
|
p.outro(import_picocolors.default.green("Done!") + import_picocolors.default.dim(" Skill ready. Review before use."));
|
|
1064
1179
|
} catch (err) {
|
|
1065
|
-
|
|
1180
|
+
progress.stop();
|
|
1066
1181
|
p.log.error(`Install failed: ${err.message}`);
|
|
1067
1182
|
process.exit(1);
|
|
1068
1183
|
}
|