skill-master 0.1.4 → 0.1.6

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/dist/cli.js CHANGED
@@ -111,6 +111,12 @@ var SkillParseError = class extends SkillManagerError {
111
111
  this.name = "SkillParseError";
112
112
  }
113
113
  };
114
+ var SourceParseError = class extends SkillManagerError {
115
+ constructor(source, detail) {
116
+ super(`Failed to parse source "${source}"${detail ? ": " + detail : ""}`);
117
+ this.name = "SourceParseError";
118
+ }
119
+ };
114
120
 
115
121
  // src/utils/logger.ts
116
122
  import chalk from "chalk";
@@ -152,20 +158,89 @@ function tableRow(...cols) {
152
158
 
153
159
  // src/core/git-source.ts
154
160
  var execFileAsync = promisify(execFile);
161
+ function parseSource(source) {
162
+ if (source.startsWith("git@") || source.startsWith("git://")) {
163
+ return { type: "git", url: source };
164
+ }
165
+ if (source.startsWith("https://") || source.startsWith("http://")) {
166
+ const ghTree = source.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)(?:\/(.+))?/);
167
+ if (ghTree) {
168
+ const url = `https://github.com/${ghTree[1]}/${ghTree[2]}.git`;
169
+ return { type: "git", url, ref: ghTree[3], subpath: ghTree[4] };
170
+ }
171
+ const ghBlob = source.match(/github\.com\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)/);
172
+ if (ghBlob) {
173
+ const url = `https://github.com/${ghBlob[1]}/${ghBlob[2]}.git`;
174
+ const filePath = ghBlob[4];
175
+ const subpath = filePath.includes("/") ? filePath.slice(0, filePath.lastIndexOf("/")) : void 0;
176
+ return { type: "git", url, ref: ghBlob[3], subpath };
177
+ }
178
+ const glTree = source.match(/gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)(?:\/(.+))?/);
179
+ if (glTree) {
180
+ const url = `https://gitlab.com/${glTree[1]}/${glTree[2]}.git`;
181
+ return { type: "git", url, ref: glTree[3], subpath: glTree[4] };
182
+ }
183
+ if (source.includes("github.com/") || source.includes("gitlab.com/")) {
184
+ return { type: "git", url: source };
185
+ }
186
+ return { type: "git", url: source };
187
+ }
188
+ if (source.includes("github.com/") || source.includes("gitlab.com/")) {
189
+ return parseSource("https://" + source);
190
+ }
191
+ if (existsSync2(source) || source.startsWith("/") || source.startsWith("./") || source.startsWith("../")) {
192
+ return { type: "local", path: source };
193
+ }
194
+ let skillFilter;
195
+ let shorthand = source;
196
+ const atIdx = shorthand.indexOf("@");
197
+ if (atIdx > 0 && !shorthand.includes("/") === false) {
198
+ const lastAt = shorthand.lastIndexOf("@");
199
+ if (lastAt > 0) {
200
+ const afterAt = shorthand.slice(lastAt + 1);
201
+ const beforeAt = shorthand.slice(0, lastAt);
202
+ if (afterAt && !afterAt.includes("/")) {
203
+ skillFilter = afterAt;
204
+ shorthand = beforeAt;
205
+ }
206
+ }
207
+ }
208
+ if (/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(shorthand)) {
209
+ return {
210
+ type: "git",
211
+ url: `https://github.com/${shorthand}.git`,
212
+ skillFilter
213
+ };
214
+ }
215
+ const segments = shorthand.split("/");
216
+ if (segments.length >= 3 && /^[a-zA-Z0-9_.-]+$/.test(segments[0]) && /^[a-zA-Z0-9_.-]+$/.test(segments[1])) {
217
+ const owner = segments[0];
218
+ const repo = segments[1];
219
+ const subpath = segments.slice(2).join("/");
220
+ return {
221
+ type: "git",
222
+ url: `https://github.com/${owner}/${repo}.git`,
223
+ subpath,
224
+ skillFilter
225
+ };
226
+ }
227
+ throw new SourceParseError(source, "Unable to determine source type");
228
+ }
155
229
  function isGitUrl(source) {
156
- return source.startsWith("https://") || source.startsWith("http://") || source.startsWith("git@") || source.startsWith("git://") || source.includes("github.com/") || source.includes("gitlab.com/") || /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(source);
230
+ try {
231
+ return parseSource(source).type === "git";
232
+ } catch {
233
+ return false;
234
+ }
157
235
  }
158
236
  function normalizeGitUrl(source) {
159
- if (source.startsWith("https://") || source.startsWith("http://") || source.startsWith("git@") || source.startsWith("git://")) {
160
- if (source.startsWith("https://") && !source.endsWith(".git")) {
161
- return source + ".git";
162
- }
163
- return source;
237
+ const parsed = parseSource(source);
238
+ if (parsed.type !== "git" || !parsed.url) return source;
239
+ let url = parsed.url;
240
+ if (url.startsWith("https://") && !url.endsWith(".git")) {
241
+ url += ".git";
164
242
  }
165
- if (/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(source)) {
166
- return `https://github.com/${source}.git`;
167
- }
168
- return source;
243
+ return url;
169
244
  }
170
245
  function parseGitUrl(url) {
171
246
  const treeMatch = url.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/(.+)/);
@@ -261,8 +336,20 @@ function inferCapabilities(allowedTools) {
261
336
  return [...caps];
262
337
  }
263
338
  async function findSkillDirectory(dir) {
339
+ const dirs = await findAllSkillDirectories(dir);
340
+ return dirs.length > 0 ? dirs[0] : null;
341
+ }
342
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build"]);
343
+ async function findAllSkillDirectories(dir, fullDepth = false) {
344
+ if (fullDepth) {
345
+ const results2 = /* @__PURE__ */ new Set();
346
+ await walkForSkills(dir, 0, 5, results2);
347
+ return [...results2];
348
+ }
349
+ const results = [];
264
350
  if (existsSync3(join2(dir, "SKILL.md"))) {
265
- return dir;
351
+ results.push(dir);
352
+ return results;
266
353
  }
267
354
  const skillsRoot = join2(dir, ".claude", "skills");
268
355
  if (existsSync3(skillsRoot)) {
@@ -272,7 +359,7 @@ async function findSkillDirectory(dir) {
272
359
  if (entry.isDirectory()) {
273
360
  const skillMdPath = join2(skillsRoot, entry.name, "SKILL.md");
274
361
  if (existsSync3(skillMdPath)) {
275
- return join2(skillsRoot, entry.name);
362
+ results.push(join2(skillsRoot, entry.name));
276
363
  }
277
364
  }
278
365
  }
@@ -285,13 +372,28 @@ async function findSkillDirectory(dir) {
285
372
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
286
373
  const skillMdPath = join2(dir, entry.name, "SKILL.md");
287
374
  if (existsSync3(skillMdPath)) {
288
- return join2(dir, entry.name);
375
+ results.push(join2(dir, entry.name));
289
376
  }
290
377
  }
291
378
  }
292
379
  } catch {
293
380
  }
294
- return null;
381
+ return results;
382
+ }
383
+ async function walkForSkills(dir, depth, maxDepth, results) {
384
+ if (depth > maxDepth) return;
385
+ if (existsSync3(join2(dir, "SKILL.md"))) {
386
+ results.add(dir);
387
+ }
388
+ try {
389
+ const entries = await readdir(dir, { withFileTypes: true });
390
+ for (const entry of entries) {
391
+ if (entry.isDirectory() && !entry.name.startsWith(".") && !SKIP_DIRS.has(entry.name)) {
392
+ await walkForSkills(join2(dir, entry.name), depth + 1, maxDepth, results);
393
+ }
394
+ }
395
+ } catch {
396
+ }
295
397
  }
296
398
  async function readSkillMd(dir) {
297
399
  const content = await readTextSafe(join2(dir, "SKILL.md"));
@@ -606,6 +708,9 @@ function detectPlatform(cwd) {
606
708
  function getAgentSkillPath(cwd, agent, name) {
607
709
  return join3(cwd, AGENTS[agent].skillsDir, name);
608
710
  }
711
+ function getAgentGlobalSkillPath(agent, name) {
712
+ return join3(AGENTS[agent].globalSkillsDir, name);
713
+ }
609
714
 
610
715
  // src/utils/paths.ts
611
716
  var AGENTS_HOME = join4(homedir2(), ".agents");
@@ -720,12 +825,38 @@ function getEnvEditPath(skillName) {
720
825
  // src/core/registry.ts
721
826
  import { existsSync as existsSync5 } from "fs";
722
827
  function createEmptyRegistry() {
723
- return { version: 1, skills: {} };
828
+ return { version: 2, skills: {} };
829
+ }
830
+ function migrateEntryV1(v1) {
831
+ return {
832
+ source: v1.source,
833
+ version: v1.version,
834
+ installed_at: v1.installed_at,
835
+ updated_at: v1.updated_at,
836
+ agents: [{
837
+ agent: v1.agent,
838
+ agent_path: v1.agent_path,
839
+ global: v1.agent_path.includes("/.agents/") || v1.agent_path.includes("\\.agents\\")
840
+ }],
841
+ env_keys: v1.env_keys,
842
+ capabilities: v1.capabilities,
843
+ canonical_path: v1.canonical_path
844
+ };
724
845
  }
725
- function validateRegistry(data) {
726
- if (!data || typeof data !== "object") return false;
846
+ function validateAndMigrate(data) {
847
+ if (!data || typeof data !== "object") return null;
727
848
  const reg = data;
728
- return reg.version === 1 && typeof reg.skills === "object" && reg.skills !== null;
849
+ if (typeof reg.skills !== "object" || reg.skills === null) return null;
850
+ if (reg.version === 2) return data;
851
+ if (reg.version === 1) {
852
+ const v1Skills = reg.skills;
853
+ const v2Skills = {};
854
+ for (const [name, entry] of Object.entries(v1Skills)) {
855
+ v2Skills[name] = migrateEntryV1(entry);
856
+ }
857
+ return { version: 2, skills: v2Skills };
858
+ }
859
+ return null;
729
860
  }
730
861
  async function readRegistry() {
731
862
  if (!existsSync5(REGISTRY_PATH)) {
@@ -735,14 +866,46 @@ async function readRegistry() {
735
866
  if (!data) {
736
867
  throw new RegistryCorruptError("Failed to parse registry.json");
737
868
  }
738
- if (!validateRegistry(data)) {
869
+ const registry = validateAndMigrate(data);
870
+ if (!registry) {
739
871
  throw new RegistryCorruptError("Invalid registry structure");
740
872
  }
741
- return data;
873
+ if (data.version !== registry.version) {
874
+ await atomicWriteJson(REGISTRY_PATH, registry);
875
+ }
876
+ return registry;
742
877
  }
743
878
  async function updateRegistry(skillName, entry) {
744
879
  const registry = await readRegistry();
745
- registry.skills[skillName] = entry;
880
+ const existing = registry.skills[skillName];
881
+ if (existing) {
882
+ for (const newAgent of entry.agents) {
883
+ const idx = existing.agents.findIndex((a) => a.agent === newAgent.agent);
884
+ if (idx >= 0) {
885
+ existing.agents[idx] = newAgent;
886
+ } else {
887
+ existing.agents.push(newAgent);
888
+ }
889
+ }
890
+ existing.source = entry.source;
891
+ existing.version = entry.version;
892
+ existing.updated_at = entry.updated_at;
893
+ existing.env_keys = entry.env_keys;
894
+ existing.capabilities = entry.capabilities;
895
+ existing.canonical_path = entry.canonical_path;
896
+ } else {
897
+ registry.skills[skillName] = entry;
898
+ }
899
+ await atomicWriteJson(REGISTRY_PATH, registry);
900
+ }
901
+ async function removeAgentFromRegistry(skillName, agent) {
902
+ const registry = await readRegistry();
903
+ const entry = registry.skills[skillName];
904
+ if (!entry) return;
905
+ entry.agents = entry.agents.filter((a) => a.agent !== agent);
906
+ if (entry.agents.length === 0) {
907
+ delete registry.skills[skillName];
908
+ }
746
909
  await atomicWriteJson(REGISTRY_PATH, registry);
747
910
  }
748
911
  async function removeFromRegistry(skillName) {
@@ -762,7 +925,7 @@ async function getRegistryEntry(skillName) {
762
925
  // src/core/installer.ts
763
926
  var TOTAL_STEPS = 9;
764
927
  async function installSkill(options) {
765
- const { source, cwd, copy = false, force = false } = options;
928
+ const { source, cwd, copy = false, force = false, global: isGlobal = false } = options;
766
929
  step(1, TOTAL_STEPS, "Fetching skill source...");
767
930
  let sourceDir;
768
931
  if (source.type === "git") {
@@ -791,7 +954,7 @@ async function installSkill(options) {
791
954
  const agent = options.agent ?? detectPlatform(cwd);
792
955
  info(`Target platform: ${agent}`);
793
956
  step(5, TOTAL_STEPS, "Backing up .env...");
794
- const agentSkillDir = getAgentSkillPath(cwd, agent, skillName);
957
+ const agentSkillDir = isGlobal ? getAgentGlobalSkillPath(agent, skillName) : getAgentSkillPath(cwd, agent, skillName);
795
958
  const envBackup = await backupEnv(skillName, agentSkillDir);
796
959
  if (envBackup) {
797
960
  success(`Backed up ${Object.keys(envBackup).length} env key(s)`);
@@ -817,7 +980,7 @@ async function installSkill(options) {
817
980
  }
818
981
  }
819
982
  step(8, TOTAL_STEPS, `Linking to ${agent} skills directory...`);
820
- const agentPath = getAgentSkillPath(cwd, agent, skillName);
983
+ const agentPath = isGlobal ? getAgentGlobalSkillPath(agent, skillName) : getAgentSkillPath(cwd, agent, skillName);
821
984
  const linkType = await symlinkOrCopy(canonicalPath, agentPath, copy);
822
985
  success(`${linkType === "symlink" ? "Symlinked" : "Copied"} to ${agentPath}`);
823
986
  step(9, TOTAL_STEPS, "Updating registry...");
@@ -825,16 +988,20 @@ async function installSkill(options) {
825
988
  const envExampleContent = await readTextSafe(join6(canonicalPath, ".env.example"));
826
989
  const envKeys = envExampleContent ? extractEnvKeys(envExampleContent) : [];
827
990
  const now = (/* @__PURE__ */ new Date()).toISOString();
991
+ const agentInstall = {
992
+ agent,
993
+ agent_path: agentPath,
994
+ global: isGlobal
995
+ };
828
996
  const entry = {
829
997
  source: source.type === "git" ? source.url : source.path,
830
998
  version: parsed.frontmatter.version,
831
999
  installed_at: now,
832
1000
  updated_at: now,
833
- agent,
1001
+ agents: [agentInstall],
834
1002
  env_keys: envKeys,
835
1003
  capabilities,
836
- canonical_path: canonicalPath,
837
- agent_path: agentPath
1004
+ canonical_path: canonicalPath
838
1005
  };
839
1006
  await updateRegistry(skillName, entry);
840
1007
  blank();
@@ -842,6 +1009,8 @@ async function installSkill(options) {
842
1009
  }
843
1010
 
844
1011
  // src/commands/add.ts
1012
+ import { existsSync as existsSync7 } from "fs";
1013
+ import { basename as basename2, join as join7 } from "path";
845
1014
  function parseAddFlags(args) {
846
1015
  const flags = {
847
1016
  global: false,
@@ -852,7 +1021,8 @@ function parseAddFlags(args) {
852
1021
  all: false,
853
1022
  fullDepth: false,
854
1023
  copy: false,
855
- force: false
1024
+ force: false,
1025
+ help: false
856
1026
  };
857
1027
  let source = null;
858
1028
  let i = 0;
@@ -870,12 +1040,17 @@ function parseAddFlags(args) {
870
1040
  flags.skill.push(val);
871
1041
  break;
872
1042
  default:
873
- break;
1043
+ throw new Error(`Unknown option: --${key}`);
874
1044
  }
875
1045
  i++;
876
1046
  continue;
877
1047
  }
878
1048
  switch (arg) {
1049
+ case "-h":
1050
+ case "--help":
1051
+ flags.help = true;
1052
+ i++;
1053
+ break;
879
1054
  case "-g":
880
1055
  case "--global":
881
1056
  flags.global = true;
@@ -926,6 +1101,8 @@ function parseAddFlags(args) {
926
1101
  default:
927
1102
  if (!arg.startsWith("-") && source === null) {
928
1103
  source = arg;
1104
+ } else if (arg.startsWith("-")) {
1105
+ throw new Error(`Unknown option: ${arg}`);
929
1106
  }
930
1107
  i++;
931
1108
  break;
@@ -938,40 +1115,117 @@ function parseAddFlags(args) {
938
1115
  }
939
1116
  return { source, flags };
940
1117
  }
1118
+ function printAddHelp() {
1119
+ console.log("Usage: skill-master add <source> [options]");
1120
+ console.log("");
1121
+ console.log("Options:");
1122
+ console.log(" -h, --help Show this help message");
1123
+ console.log(" -g, --global Install globally (~/.agents/)");
1124
+ console.log(" -a, --agent <agents> Target agents (space-separated)");
1125
+ console.log(" -s, --skill <skills> Select skills (space-separated)");
1126
+ console.log(" -y, --yes Skip confirmations");
1127
+ console.log(" -l, --list List available skills without installing");
1128
+ console.log(" --all Install all skills to all agents");
1129
+ console.log(" --full-depth Search all subdirectories");
1130
+ console.log(" --copy Copy instead of symlink");
1131
+ console.log(" --force Force reinstall");
1132
+ }
941
1133
  async function add(args) {
942
1134
  if (args.length === 0) {
943
- error("Usage: skill-master add <source> [options]");
944
- console.log("");
945
- console.log("Options:");
946
- console.log(" -g, --global Install globally (~/.agents/)");
947
- console.log(" -a, --agent <agents> Target agents (space-separated)");
948
- console.log(" -s, --skill <skills> Select skills (space-separated)");
949
- console.log(" -y, --yes Skip confirmations");
950
- console.log(" -l, --list List available skills without installing");
951
- console.log(" --all Install all skills to all agents");
952
- console.log(" --full-depth Search all subdirectories");
953
- console.log(" --copy Copy instead of symlink");
954
- console.log(" --force Force reinstall");
1135
+ printAddHelp();
955
1136
  process.exit(1);
956
1137
  }
957
1138
  const { source, flags } = parseAddFlags(args);
1139
+ if (flags.help) {
1140
+ printAddHelp();
1141
+ process.exit(0);
1142
+ }
958
1143
  if (!source) {
959
1144
  error("No source specified. Provide a GitHub URL, owner/repo, or local path.");
960
1145
  process.exit(1);
961
1146
  }
962
- const skillSource = isGitUrl(source) ? { type: "git", url: source } : { type: "local", path: source };
963
1147
  const cwd = process.cwd();
1148
+ const parsed = parseSource(source);
1149
+ if (parsed.skillFilter && !flags.skill.includes(parsed.skillFilter)) {
1150
+ flags.skill.push(parsed.skillFilter);
1151
+ }
1152
+ let sourceDir;
1153
+ if (parsed.type === "git") {
1154
+ step(1, 9, "Fetching skill source...");
1155
+ sourceDir = await cloneRepo(parsed.url, parsed.ref);
1156
+ if (parsed.subpath) {
1157
+ const sub = join7(sourceDir, parsed.subpath);
1158
+ if (existsSync7(sub)) {
1159
+ sourceDir = sub;
1160
+ }
1161
+ }
1162
+ } else {
1163
+ sourceDir = parsed.path;
1164
+ if (!existsSync7(sourceDir)) {
1165
+ throw new SkillNotFoundError(sourceDir);
1166
+ }
1167
+ }
1168
+ const allSkillDirs = await findAllSkillDirectories(sourceDir, flags.fullDepth);
1169
+ if (allSkillDirs.length === 0) {
1170
+ throw new SkillNotFoundError(`No SKILL.md found in ${sourceDir}`);
1171
+ }
1172
+ if (flags.list) {
1173
+ blank();
1174
+ tableHeader("Skill", "Version", "Description");
1175
+ for (const dir of allSkillDirs) {
1176
+ const sk = await readSkillMd(dir);
1177
+ if (sk) {
1178
+ tableRow(
1179
+ sk.frontmatter.name,
1180
+ sk.frontmatter.version ?? "-",
1181
+ sk.frontmatter.description ?? "-"
1182
+ );
1183
+ }
1184
+ }
1185
+ blank();
1186
+ return;
1187
+ }
1188
+ let targetDirs = allSkillDirs;
1189
+ if (flags.skill.length > 0 && !flags.skill.includes("*")) {
1190
+ const requested = new Set(flags.skill.map((s) => s.toLowerCase()));
1191
+ const filtered = [];
1192
+ for (const dir of allSkillDirs) {
1193
+ const sk = await readSkillMd(dir);
1194
+ if (!sk) continue;
1195
+ const name = sk.frontmatter.name.toLowerCase();
1196
+ const dirName = basename2(dir).toLowerCase();
1197
+ if (requested.has(name) || requested.has(dirName)) {
1198
+ filtered.push(dir);
1199
+ }
1200
+ }
1201
+ if (filtered.length === 0) {
1202
+ const available = [];
1203
+ for (const dir of allSkillDirs) {
1204
+ const sk = await readSkillMd(dir);
1205
+ if (sk) available.push(sk.frontmatter.name);
1206
+ }
1207
+ error(
1208
+ `No matching skills found for: ${flags.skill.join(", ")}
1209
+ Available skills: ${available.join(", ")}`
1210
+ );
1211
+ process.exit(1);
1212
+ }
1213
+ targetDirs = filtered;
1214
+ }
964
1215
  const agents = flags.agent.length > 0 ? flags.agent : [void 0];
965
1216
  try {
966
- for (const agent of agents) {
967
- await installSkill({
968
- source: skillSource,
969
- agent,
970
- cwd,
971
- copy: flags.copy,
972
- force: flags.force,
973
- yes: flags.yes
974
- });
1217
+ for (const dir of targetDirs) {
1218
+ for (const agent of agents) {
1219
+ await installSkill({
1220
+ source: { type: "local", path: dir },
1221
+ agent,
1222
+ cwd,
1223
+ global: flags.global,
1224
+ copy: flags.copy,
1225
+ force: flags.force,
1226
+ yes: flags.yes
1227
+ });
1228
+ }
975
1229
  }
976
1230
  } catch (err) {
977
1231
  error(err.message);
@@ -995,12 +1249,15 @@ async function update(args) {
995
1249
  info(`Updating skill: ${skillName}`);
996
1250
  info(`Source: ${entry.source}`);
997
1251
  const source = isGitUrl(entry.source) ? { type: "git", url: entry.source } : { type: "local", path: entry.source };
998
- await installSkill({
999
- source,
1000
- agent: entry.agent,
1001
- cwd: process.cwd(),
1002
- force: true
1003
- });
1252
+ for (const agentRecord of entry.agents) {
1253
+ await installSkill({
1254
+ source,
1255
+ agent: agentRecord.agent,
1256
+ cwd: process.cwd(),
1257
+ global: agentRecord.global,
1258
+ force: true
1259
+ });
1260
+ }
1004
1261
  success(`Skill "${skillName}" updated successfully!`);
1005
1262
  } catch (err) {
1006
1263
  error(err.message);
@@ -1016,7 +1273,8 @@ function parseRemoveFlags(args) {
1016
1273
  skill: [],
1017
1274
  yes: false,
1018
1275
  all: false,
1019
- purge: false
1276
+ purge: false,
1277
+ help: false
1020
1278
  };
1021
1279
  const names = [];
1022
1280
  let i = 0;
@@ -1034,12 +1292,17 @@ function parseRemoveFlags(args) {
1034
1292
  flags.skill.push(val);
1035
1293
  break;
1036
1294
  default:
1037
- break;
1295
+ throw new Error(`Unknown option: --${key}`);
1038
1296
  }
1039
1297
  i++;
1040
1298
  continue;
1041
1299
  }
1042
1300
  switch (arg) {
1301
+ case "-h":
1302
+ case "--help":
1303
+ flags.help = true;
1304
+ i++;
1305
+ break;
1043
1306
  case "-g":
1044
1307
  case "--global":
1045
1308
  flags.global = true;
@@ -1077,6 +1340,8 @@ function parseRemoveFlags(args) {
1077
1340
  default:
1078
1341
  if (!arg.startsWith("-")) {
1079
1342
  names.push(arg);
1343
+ } else {
1344
+ throw new Error(`Unknown option: ${arg}`);
1080
1345
  }
1081
1346
  i++;
1082
1347
  break;
@@ -1087,20 +1352,28 @@ function parseRemoveFlags(args) {
1087
1352
  }
1088
1353
  return { names, flags };
1089
1354
  }
1355
+ function printRemoveHelp() {
1356
+ console.log("Usage: skill-master remove [skills...] [options]");
1357
+ console.log("");
1358
+ console.log("Options:");
1359
+ console.log(" -h, --help Show this help message");
1360
+ console.log(" -g, --global Remove from global (~/.agents/)");
1361
+ console.log(" -a, --agent <agents> Target agents (space-separated)");
1362
+ console.log(" -s, --skill <skills> Select skills (space-separated)");
1363
+ console.log(" -y, --yes Skip confirmations");
1364
+ console.log(" --all Remove all skills");
1365
+ console.log(" --purge Also remove config data");
1366
+ }
1090
1367
  async function remove(args) {
1091
1368
  if (args.length === 0) {
1092
- error("Usage: skill-master remove [skills...] [options]");
1093
- console.log("");
1094
- console.log("Options:");
1095
- console.log(" -g, --global Remove from global (~/.agents/)");
1096
- console.log(" -a, --agent <agents> Target agents (space-separated)");
1097
- console.log(" -s, --skill <skills> Select skills (space-separated)");
1098
- console.log(" -y, --yes Skip confirmations");
1099
- console.log(" --all Remove all skills");
1100
- console.log(" --purge Also remove config data");
1369
+ printRemoveHelp();
1101
1370
  process.exit(1);
1102
1371
  }
1103
1372
  const { names, flags } = parseRemoveFlags(args);
1373
+ if (flags.help) {
1374
+ printRemoveHelp();
1375
+ process.exit(0);
1376
+ }
1104
1377
  let skillNames;
1105
1378
  if (flags.all) {
1106
1379
  const registry = await listRegistry();
@@ -1121,15 +1394,39 @@ async function remove(args) {
1121
1394
  throw new SkillNotFoundError(skillName);
1122
1395
  }
1123
1396
  info(`Removing skill: ${skillName}`);
1124
- await removePath(entry.agent_path);
1125
- success(`Removed from ${entry.agent_path}`);
1126
- await removePath(entry.canonical_path);
1127
- success(`Removed from ${entry.canonical_path}`);
1128
- if (flags.purge) {
1129
- await removePath(getSkillConfigPath(skillName));
1130
- success("Purged config directory");
1397
+ if (flags.agent.length > 0) {
1398
+ for (const agentName of flags.agent) {
1399
+ const agentRecord = entry.agents.find((a) => a.agent === agentName);
1400
+ if (!agentRecord) {
1401
+ warn(`Agent "${agentName}" not found for skill "${skillName}"`);
1402
+ continue;
1403
+ }
1404
+ await removePath(agentRecord.agent_path);
1405
+ success(`Removed ${agentName} path: ${agentRecord.agent_path}`);
1406
+ await removeAgentFromRegistry(skillName, agentName);
1407
+ }
1408
+ const updated = await getRegistryEntry(skillName);
1409
+ if (!updated) {
1410
+ await removePath(entry.canonical_path);
1411
+ success(`Removed canonical path: ${entry.canonical_path}`);
1412
+ if (flags.purge) {
1413
+ await removePath(getSkillConfigPath(skillName));
1414
+ success("Purged config directory");
1415
+ }
1416
+ }
1417
+ } else {
1418
+ for (const agentRecord of entry.agents) {
1419
+ await removePath(agentRecord.agent_path);
1420
+ success(`Removed ${agentRecord.agent} path: ${agentRecord.agent_path}`);
1421
+ }
1422
+ await removePath(entry.canonical_path);
1423
+ success(`Removed canonical path: ${entry.canonical_path}`);
1424
+ if (flags.purge) {
1425
+ await removePath(getSkillConfigPath(skillName));
1426
+ success("Purged config directory");
1427
+ }
1428
+ await removeFromRegistry(skillName);
1131
1429
  }
1132
- await removeFromRegistry(skillName);
1133
1430
  success(`Skill "${skillName}" removed successfully!`);
1134
1431
  }
1135
1432
  } catch (err) {
@@ -1255,17 +1552,20 @@ async function list(args = []) {
1255
1552
  const skills = await listRegistry();
1256
1553
  let entries = Object.entries(skills);
1257
1554
  if (flags.agent.length > 0) {
1258
- entries = entries.filter(([, entry]) => flags.agent.includes(entry.agent));
1555
+ entries = entries.filter(
1556
+ ([, entry]) => entry.agents.some((a) => flags.agent.includes(a.agent))
1557
+ );
1259
1558
  }
1260
1559
  if (entries.length === 0) {
1261
1560
  info("No skills installed");
1262
1561
  return;
1263
1562
  }
1264
1563
  blank();
1265
- tableHeader("Skill", "Version", "Platform", "Installed");
1564
+ tableHeader("Skill", "Version", "Platform(s)", "Installed");
1266
1565
  for (const [name, entry] of entries) {
1267
1566
  const date = new Date(entry.installed_at).toLocaleDateString();
1268
- tableRow(name, entry.version ?? "-", entry.agent, date);
1567
+ const platforms = entry.agents.map((a) => a.agent).join(", ");
1568
+ tableRow(name, entry.version ?? "-", platforms, date);
1269
1569
  }
1270
1570
  blank();
1271
1571
  }
@@ -1286,12 +1586,14 @@ async function info2(args) {
1286
1586
  blank();
1287
1587
  info(`Skill: ${skillName}`);
1288
1588
  kv("Version", entry.version ?? "-");
1289
- kv("Platform", entry.agent);
1589
+ kv("Platform(s)", entry.agents.map((a) => a.agent).join(", "));
1290
1590
  kv("Source", entry.source);
1291
1591
  kv("Installed", new Date(entry.installed_at).toLocaleString());
1292
1592
  kv("Updated", new Date(entry.updated_at).toLocaleString());
1293
1593
  kv("Canonical Path", entry.canonical_path);
1294
- kv("Agent Path", entry.agent_path);
1594
+ for (const a of entry.agents) {
1595
+ kv(` ${a.agent} Path`, `${a.agent_path}${a.global ? " (global)" : ""}`);
1596
+ }
1295
1597
  kv("Capabilities", entry.capabilities.join(", "));
1296
1598
  kv("Env Keys", entry.env_keys.join(", ") || "none");
1297
1599
  kv("Env Status", envStatus);
@@ -1303,7 +1605,7 @@ async function info2(args) {
1303
1605
  }
1304
1606
 
1305
1607
  // src/commands/doctor.ts
1306
- import { existsSync as existsSync7 } from "fs";
1608
+ import { existsSync as existsSync8 } from "fs";
1307
1609
  async function doctor() {
1308
1610
  blank();
1309
1611
  info("Running diagnostics...");
@@ -1312,7 +1614,7 @@ async function doctor() {
1312
1614
  info("Checking directory structure...");
1313
1615
  const dirs = [AGENTS_HOME, CONFIG_DIR, SKILLS_DIR];
1314
1616
  for (const dir of dirs) {
1315
- if (existsSync7(dir)) {
1617
+ if (existsSync8(dir)) {
1316
1618
  success(`\u2713 ${dir}`);
1317
1619
  } else {
1318
1620
  warn(`\u2717 ${dir} (missing)`);
@@ -1321,7 +1623,7 @@ async function doctor() {
1321
1623
  }
1322
1624
  blank();
1323
1625
  info("Checking registry...");
1324
- if (existsSync7(REGISTRY_PATH)) {
1626
+ if (existsSync8(REGISTRY_PATH)) {
1325
1627
  success(`\u2713 ${REGISTRY_PATH}`);
1326
1628
  try {
1327
1629
  const skills = await listRegistry();
@@ -1339,18 +1641,20 @@ async function doctor() {
1339
1641
  const skills = await listRegistry();
1340
1642
  for (const [name, entry] of Object.entries(skills)) {
1341
1643
  info(`Skill: ${name}`);
1342
- if (existsSync7(entry.canonical_path)) {
1644
+ if (existsSync8(entry.canonical_path)) {
1343
1645
  success(` \u2713 Canonical path exists`);
1344
1646
  } else {
1345
1647
  error(` \u2717 Canonical path missing: ${entry.canonical_path}`);
1346
1648
  issues++;
1347
1649
  }
1348
- if (existsSync7(entry.agent_path)) {
1349
- const isLink = await isSymlink(entry.agent_path);
1350
- success(` \u2713 Agent path exists (${isLink ? "symlink" : "copy"})`);
1351
- } else {
1352
- error(` \u2717 Agent path missing: ${entry.agent_path}`);
1353
- issues++;
1650
+ for (const agentRecord of entry.agents) {
1651
+ if (existsSync8(agentRecord.agent_path)) {
1652
+ const isLink = await isSymlink(agentRecord.agent_path);
1653
+ success(` \u2713 ${agentRecord.agent} path exists (${isLink ? "symlink" : "copy"}${agentRecord.global ? ", global" : ""})`);
1654
+ } else {
1655
+ error(` \u2717 ${agentRecord.agent} path missing: ${agentRecord.agent_path}`);
1656
+ issues++;
1657
+ }
1354
1658
  }
1355
1659
  const envStatus = await getEnvStatus(name, entry.env_keys);
1356
1660
  if (envStatus === "configured") {
@@ -1424,8 +1728,8 @@ async function find(args) {
1424
1728
  }
1425
1729
 
1426
1730
  // src/commands/init.ts
1427
- import { existsSync as existsSync8 } from "fs";
1428
- import { join as join7, basename as basename2 } from "path";
1731
+ import { existsSync as existsSync9 } from "fs";
1732
+ import { join as join8, basename as basename3 } from "path";
1429
1733
  var SKILL_MD_TEMPLATE = `---
1430
1734
  name: {{NAME}}
1431
1735
  version: 0.1.0
@@ -1451,14 +1755,14 @@ async function init(args) {
1451
1755
  let targetDir;
1452
1756
  let skillName;
1453
1757
  if (nameArg) {
1454
- targetDir = join7(cwd, nameArg);
1758
+ targetDir = join8(cwd, nameArg);
1455
1759
  skillName = nameArg;
1456
1760
  } else {
1457
1761
  targetDir = cwd;
1458
- skillName = basename2(cwd);
1762
+ skillName = basename3(cwd);
1459
1763
  }
1460
- const skillMdPath = join7(targetDir, "SKILL.md");
1461
- if (existsSync8(skillMdPath)) {
1764
+ const skillMdPath = join8(targetDir, "SKILL.md");
1765
+ if (existsSync9(skillMdPath)) {
1462
1766
  error(`SKILL.md already exists at ${skillMdPath}`);
1463
1767
  process.exit(1);
1464
1768
  }