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 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
@@ -17,6 +17,8 @@ interface AddOptions {
17
17
  all?: boolean;
18
18
  fullDepth?: boolean;
19
19
  copy?: boolean;
20
+ /** 安装到自定义路径(目录) */
21
+ path?: string;
20
22
  }
21
23
  declare const _default: {
22
24
  run: (args: string[], options?: AddOptions) => Promise<void>;
package/lib/index.js CHANGED
@@ -407,8 +407,8 @@ const agents = {
407
407
  cline: {
408
408
  name: "cline",
409
409
  displayName: "Cline",
410
- skillsDir: ".agents/skills",
411
- globalSkillsDir: join$1(home, ".agents", "skills"),
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: ".agents/skills",
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
- const agentDir = join$1(agentBase, skillName);
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
- async function resolveTargetAgents(options) {
996
- const allAgentChoices = buildAgentChoices(Boolean(options.global ?? options.yes));
997
- if (options.yes) {
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 s = p.spinner();
1028
- s.start(`Searching for ${chalk.bold(skill)}...`);
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
- s.stop();
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
- s.stop(`${chalk.bold(displayName)} ${chalk.dim(`v${version}`)}`);
1041
- const targetAgents = await resolveTargetAgents(options);
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
- for (const agent of targetAgents) {
1049
- const result = await installWellKnownSkillForAgent(asset, agent, {
1050
- global: installGlobally,
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 ${agents[agent].displayName}: ${result.error || "Unknown error"}`);
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
- s.stop("Skills installed successfully");
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
- s.stop();
1180
+ progress.stop();
1066
1181
  p.log.error(`Install failed: ${err.message}`);
1067
1182
  process.exit(1);
1068
1183
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skill-atlas-cli",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "skill-atlas CLI - 虾小宝 命令行工具",
5
5
  "homepage": "https://ai.skillatlas.cn/",
6
6
  "type": "module",