@claude-collective/cli 0.26.1 → 0.29.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.
Files changed (233) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +47 -13
  3. package/config/stacks.yaml +330 -93
  4. package/dist/chunk-56ERY7H7.js +29 -0
  5. package/dist/chunk-56ERY7H7.js.map +1 -0
  6. package/dist/{chunk-OBXAY23Y.js → chunk-5I6VY2E7.js} +5 -5
  7. package/dist/chunk-5I6VY2E7.js.map +1 -0
  8. package/dist/{chunk-324DM2L6.js → chunk-5WIHSJRO.js} +230 -65
  9. package/dist/chunk-5WIHSJRO.js.map +1 -0
  10. package/dist/{chunk-GGFOD5PK.js → chunk-6F3ZKDVE.js} +122 -66
  11. package/dist/chunk-6F3ZKDVE.js.map +1 -0
  12. package/dist/{chunk-DBRUQQUF.js → chunk-7GHTQSWI.js} +5 -1
  13. package/dist/{chunk-DBRUQQUF.js.map → chunk-7GHTQSWI.js.map} +1 -1
  14. package/dist/chunk-7ICBJZV2.js +63 -0
  15. package/dist/chunk-7ICBJZV2.js.map +1 -0
  16. package/dist/{chunk-3X5D7RM5.js → chunk-7UKQZSWT.js} +15 -4
  17. package/dist/chunk-7UKQZSWT.js.map +1 -0
  18. package/dist/{chunk-F4RD5FYM.js → chunk-A4T4YSV4.js} +5 -2
  19. package/dist/chunk-A4T4YSV4.js.map +1 -0
  20. package/dist/{chunk-VVYNZZUX.js → chunk-AG5YGYJT.js} +9 -5
  21. package/dist/chunk-AG5YGYJT.js.map +1 -0
  22. package/dist/chunk-AJFSCLJ7.js +81 -0
  23. package/dist/chunk-AJFSCLJ7.js.map +1 -0
  24. package/dist/{chunk-NQJ47R4N.js → chunk-CQZAKMPJ.js} +66 -14
  25. package/dist/chunk-CQZAKMPJ.js.map +1 -0
  26. package/dist/chunk-DIRH4PDF.js +24 -0
  27. package/dist/chunk-DIRH4PDF.js.map +1 -0
  28. package/dist/{chunk-HIQGK5XJ.js → chunk-DUIYVKFK.js} +123 -86
  29. package/dist/chunk-DUIYVKFK.js.map +1 -0
  30. package/dist/chunk-EP6J44I4.js +142 -0
  31. package/dist/chunk-EP6J44I4.js.map +1 -0
  32. package/dist/{chunk-2YMMJP4Z.js → chunk-EUPMWSM3.js} +92 -29
  33. package/dist/chunk-EUPMWSM3.js.map +1 -0
  34. package/dist/chunk-FUPBGSRA.js +66 -0
  35. package/dist/chunk-FUPBGSRA.js.map +1 -0
  36. package/dist/{chunk-U7HFKR74.js → chunk-FY5D4KIC.js} +5 -2
  37. package/dist/chunk-FY5D4KIC.js.map +1 -0
  38. package/dist/chunk-G5WXKKQM.js +233 -0
  39. package/dist/chunk-G5WXKKQM.js.map +1 -0
  40. package/dist/{chunk-KWYO3M5Q.js → chunk-GVVEPVR7.js} +25 -24
  41. package/dist/chunk-GVVEPVR7.js.map +1 -0
  42. package/dist/chunk-IFODQTCX.js +162 -0
  43. package/dist/chunk-IFODQTCX.js.map +1 -0
  44. package/dist/chunk-IQUBOWWU.js +366 -0
  45. package/dist/chunk-IQUBOWWU.js.map +1 -0
  46. package/dist/{chunk-ETCVEV3S.js → chunk-IRG52AN5.js} +242 -155
  47. package/dist/chunk-IRG52AN5.js.map +1 -0
  48. package/dist/chunk-MQAYAISQ.js +88 -0
  49. package/dist/chunk-MQAYAISQ.js.map +1 -0
  50. package/dist/{chunk-G35SYE6Z.js → chunk-N73GQTCK.js} +37 -104
  51. package/dist/chunk-N73GQTCK.js.map +1 -0
  52. package/dist/{chunk-H7SSBSPR.js → chunk-OA5RCL2L.js} +8 -5
  53. package/dist/chunk-OA5RCL2L.js.map +1 -0
  54. package/dist/{chunk-CZBNDP5B.js → chunk-PNXFJPXF.js} +3 -3
  55. package/dist/{chunk-MCTSHLAF.js → chunk-RI5QEK5W.js} +41 -14
  56. package/dist/chunk-RI5QEK5W.js.map +1 -0
  57. package/dist/chunk-RXC7AF7N.js +31 -0
  58. package/dist/chunk-RXC7AF7N.js.map +1 -0
  59. package/dist/{chunk-CQ7657GA.js → chunk-SSHG7MEE.js} +1248 -735
  60. package/dist/chunk-SSHG7MEE.js.map +1 -0
  61. package/dist/{chunk-4S4FCAA2.js → chunk-VTUPUKFD.js} +26 -31
  62. package/dist/chunk-VTUPUKFD.js.map +1 -0
  63. package/dist/{chunk-XENOESJZ.js → chunk-WLQUQXWO.js} +10 -67
  64. package/dist/chunk-WLQUQXWO.js.map +1 -0
  65. package/dist/chunk-WPED6CL3.js +105 -0
  66. package/dist/chunk-WPED6CL3.js.map +1 -0
  67. package/dist/{chunk-NT4K647L.js → chunk-XPMEGGJK.js} +97 -76
  68. package/dist/chunk-XPMEGGJK.js.map +1 -0
  69. package/dist/chunk-XZKVOPCR.js +75 -0
  70. package/dist/chunk-XZKVOPCR.js.map +1 -0
  71. package/dist/{chunk-ZW2PELOH.js → chunk-ZX5DM4D5.js} +106 -69
  72. package/dist/chunk-ZX5DM4D5.js.map +1 -0
  73. package/dist/commands/build/marketplace.js +21 -20
  74. package/dist/commands/build/marketplace.js.map +1 -1
  75. package/dist/commands/build/plugins.js +7 -11
  76. package/dist/commands/build/plugins.js.map +1 -1
  77. package/dist/commands/build/stack.js +8 -13
  78. package/dist/commands/build/stack.js.map +1 -1
  79. package/dist/commands/compile.js +109 -135
  80. package/dist/commands/compile.js.map +1 -1
  81. package/dist/commands/config/get.js +4 -5
  82. package/dist/commands/config/get.js.map +1 -1
  83. package/dist/commands/config/index.js +5 -6
  84. package/dist/commands/config/index.js.map +1 -1
  85. package/dist/commands/config/path.js +4 -5
  86. package/dist/commands/config/path.js.map +1 -1
  87. package/dist/commands/config/set-project.js +4 -5
  88. package/dist/commands/config/set-project.js.map +1 -1
  89. package/dist/commands/config/show.js +5 -6
  90. package/dist/commands/config/unset-project.js +4 -5
  91. package/dist/commands/config/unset-project.js.map +1 -1
  92. package/dist/commands/diff.js +26 -11
  93. package/dist/commands/diff.js.map +1 -1
  94. package/dist/commands/doctor.js +13 -16
  95. package/dist/commands/doctor.js.map +1 -1
  96. package/dist/commands/edit.js +71 -42
  97. package/dist/commands/edit.js.map +1 -1
  98. package/dist/commands/eject.js +34 -14
  99. package/dist/commands/eject.js.map +1 -1
  100. package/dist/commands/import/skill.js +93 -52
  101. package/dist/commands/import/skill.js.map +1 -1
  102. package/dist/commands/info.js +27 -9
  103. package/dist/commands/info.js.map +1 -1
  104. package/dist/commands/init.js +98 -48
  105. package/dist/commands/init.js.map +1 -1
  106. package/dist/commands/list.js +10 -5
  107. package/dist/commands/list.js.map +1 -1
  108. package/dist/commands/new/agent.js +8 -11
  109. package/dist/commands/new/agent.js.map +1 -1
  110. package/dist/commands/new/skill.js +17 -18
  111. package/dist/commands/new/skill.js.map +1 -1
  112. package/dist/commands/outdated.js +23 -9
  113. package/dist/commands/outdated.js.map +1 -1
  114. package/dist/commands/search.js +23 -20
  115. package/dist/commands/search.js.map +1 -1
  116. package/dist/commands/uninstall.js +28 -21
  117. package/dist/commands/uninstall.js.map +1 -1
  118. package/dist/commands/update.js +30 -22
  119. package/dist/commands/update.js.map +1 -1
  120. package/dist/commands/validate.js +103 -39
  121. package/dist/commands/validate.js.map +1 -1
  122. package/dist/commands/version/bump.js +4 -5
  123. package/dist/commands/version/bump.js.map +1 -1
  124. package/dist/commands/version/index.js +4 -5
  125. package/dist/commands/version/index.js.map +1 -1
  126. package/dist/commands/version/set.js +4 -5
  127. package/dist/commands/version/set.js.map +1 -1
  128. package/dist/commands/version/show.js +4 -5
  129. package/dist/commands/version/show.js.map +1 -1
  130. package/dist/components/common/confirm.test.js +2 -2
  131. package/dist/components/common/confirm.test.js.map +1 -1
  132. package/dist/components/skill-search/skill-search.js +4 -2
  133. package/dist/components/wizard/category-grid.js +3 -1
  134. package/dist/components/wizard/category-grid.test.js +63 -64
  135. package/dist/components/wizard/category-grid.test.js.map +1 -1
  136. package/dist/components/wizard/domain-selection.js +13 -0
  137. package/dist/components/wizard/help-modal.js +10 -0
  138. package/dist/components/wizard/help-modal.js.map +1 -0
  139. package/dist/components/wizard/menu-item.js +2 -1
  140. package/dist/components/wizard/search-modal.js +3 -1
  141. package/dist/components/wizard/search-modal.test.js +4 -2
  142. package/dist/components/wizard/search-modal.test.js.map +1 -1
  143. package/dist/components/wizard/section-progress.js +2 -1
  144. package/dist/components/wizard/section-progress.test.js +2 -1
  145. package/dist/components/wizard/section-progress.test.js.map +1 -1
  146. package/dist/components/wizard/source-grid.js +6 -2
  147. package/dist/components/wizard/source-grid.test.js +49 -45
  148. package/dist/components/wizard/source-grid.test.js.map +1 -1
  149. package/dist/components/wizard/stack-selection.js +15 -0
  150. package/dist/components/wizard/stack-selection.js.map +1 -0
  151. package/dist/components/wizard/step-approach.js +8 -6
  152. package/dist/components/wizard/step-approach.test.js +11 -9
  153. package/dist/components/wizard/step-approach.test.js.map +1 -1
  154. package/dist/components/wizard/step-build.js +9 -13
  155. package/dist/components/wizard/step-build.test.js +27 -45
  156. package/dist/components/wizard/step-build.test.js.map +1 -1
  157. package/dist/components/wizard/step-confirm.js +2 -1
  158. package/dist/components/wizard/step-confirm.test.js +6 -5
  159. package/dist/components/wizard/step-confirm.test.js.map +1 -1
  160. package/dist/components/wizard/step-refine.js +2 -1
  161. package/dist/components/wizard/step-refine.test.js +3 -2
  162. package/dist/components/wizard/step-refine.test.js.map +1 -1
  163. package/dist/components/wizard/step-settings.js +8 -6
  164. package/dist/components/wizard/step-settings.test.js +12 -10
  165. package/dist/components/wizard/step-settings.test.js.map +1 -1
  166. package/dist/components/wizard/step-sources.js +11 -9
  167. package/dist/components/wizard/step-sources.test.js +16 -15
  168. package/dist/components/wizard/step-sources.test.js.map +1 -1
  169. package/dist/components/wizard/step-stack.js +9 -6
  170. package/dist/components/wizard/step-stack.test.js +15 -12
  171. package/dist/components/wizard/step-stack.test.js.map +1 -1
  172. package/dist/components/wizard/view-title.js +2 -1
  173. package/dist/components/wizard/wizard-layout.js +7 -9
  174. package/dist/components/wizard/wizard-tabs.js +2 -1
  175. package/dist/components/wizard/wizard-tabs.test.js +2 -1
  176. package/dist/components/wizard/wizard-tabs.test.js.map +1 -1
  177. package/dist/components/wizard/wizard.js +26 -20
  178. package/dist/config/stacks.yaml +330 -93
  179. package/dist/hooks/init.js +3 -4
  180. package/dist/hooks/init.js.map +1 -1
  181. package/dist/source-manager-TV2YGPAN.js +15 -0
  182. package/dist/source-manager-TV2YGPAN.js.map +1 -0
  183. package/dist/stores/wizard-store.js +4 -3
  184. package/dist/stores/wizard-store.test.js +272 -25
  185. package/dist/stores/wizard-store.test.js.map +1 -1
  186. package/package.json +2 -1
  187. package/src/schemas/agent-frontmatter.schema.json +84 -0
  188. package/src/schemas/agent.schema.json +93 -0
  189. package/src/schemas/hooks.schema.json +47 -0
  190. package/src/schemas/marketplace.schema.json +119 -0
  191. package/src/schemas/metadata.schema.json +113 -0
  192. package/src/schemas/plugin.schema.json +130 -0
  193. package/src/schemas/project-config.schema.json +125 -0
  194. package/src/schemas/project-source-config.schema.json +81 -0
  195. package/src/schemas/skill-frontmatter.schema.json +42 -0
  196. package/src/schemas/skills-matrix.schema.json +467 -0
  197. package/src/schemas/stack.schema.json +191 -0
  198. package/src/schemas/stacks.schema.json +111 -0
  199. package/dist/chunk-2OW7FCIF.js +0 -197
  200. package/dist/chunk-2OW7FCIF.js.map +0 -1
  201. package/dist/chunk-2YMMJP4Z.js.map +0 -1
  202. package/dist/chunk-324DM2L6.js.map +0 -1
  203. package/dist/chunk-3X5D7RM5.js.map +0 -1
  204. package/dist/chunk-4S4FCAA2.js.map +0 -1
  205. package/dist/chunk-CQ7657GA.js.map +0 -1
  206. package/dist/chunk-ETCVEV3S.js.map +0 -1
  207. package/dist/chunk-F4RD5FYM.js.map +0 -1
  208. package/dist/chunk-G35SYE6Z.js.map +0 -1
  209. package/dist/chunk-GGFOD5PK.js.map +0 -1
  210. package/dist/chunk-H7SSBSPR.js.map +0 -1
  211. package/dist/chunk-HIQGK5XJ.js.map +0 -1
  212. package/dist/chunk-HWD32NP7.js +0 -19
  213. package/dist/chunk-HWD32NP7.js.map +0 -1
  214. package/dist/chunk-KWYO3M5Q.js.map +0 -1
  215. package/dist/chunk-MCTSHLAF.js.map +0 -1
  216. package/dist/chunk-NQJ47R4N.js.map +0 -1
  217. package/dist/chunk-NT4K647L.js.map +0 -1
  218. package/dist/chunk-O6ZTD7ZI.js +0 -70
  219. package/dist/chunk-O6ZTD7ZI.js.map +0 -1
  220. package/dist/chunk-OBXAY23Y.js.map +0 -1
  221. package/dist/chunk-TMED5DQ2.js +0 -210
  222. package/dist/chunk-TMED5DQ2.js.map +0 -1
  223. package/dist/chunk-U7HFKR74.js.map +0 -1
  224. package/dist/chunk-UEMRJI2K.js +0 -146
  225. package/dist/chunk-UEMRJI2K.js.map +0 -1
  226. package/dist/chunk-UNN7523L.js +0 -78
  227. package/dist/chunk-UNN7523L.js.map +0 -1
  228. package/dist/chunk-VVYNZZUX.js.map +0 -1
  229. package/dist/chunk-XENOESJZ.js.map +0 -1
  230. package/dist/chunk-ZW2PELOH.js.map +0 -1
  231. package/dist/source-manager-VWIIDTK5.js +0 -16
  232. /package/dist/{chunk-CZBNDP5B.js.map → chunk-PNXFJPXF.js.map} +0 -0
  233. /package/dist/{source-manager-VWIIDTK5.js.map → components/wizard/domain-selection.js.map} +0 -0
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ SKILL_ID_PATTERN,
3
4
  agentFrontmatterValidationSchema,
4
5
  agentYamlConfigSchema,
5
6
  categoryPathSchema,
@@ -7,11 +8,14 @@ import {
7
8
  directoryExists,
8
9
  ensureDir,
9
10
  fileExists,
11
+ formatZodErrors,
12
+ getErrorMessage,
10
13
  glob,
11
14
  hooksRecordSchema,
12
15
  listDirectories,
13
16
  localRawMetadataSchema,
14
17
  localSkillMetadataSchema,
18
+ log,
15
19
  marketplaceSchema,
16
20
  pluginAuthorSchema,
17
21
  pluginManifestSchema,
@@ -19,6 +23,7 @@ import {
19
23
  projectSourceConfigSchema,
20
24
  readFile,
21
25
  readFileOptional,
26
+ readFileSafe,
22
27
  remove,
23
28
  skillDisplayNameSchema,
24
29
  skillFrontmatterLoaderSchema,
@@ -27,31 +32,43 @@ import {
27
32
  skillMetadataLoaderSchema,
28
33
  skillsMatrixConfigSchema,
29
34
  stacksConfigSchema,
35
+ validateNestingDepth,
30
36
  verbose,
31
37
  warn,
38
+ warnUnknownFields,
32
39
  writeFile
33
- } from "./chunk-324DM2L6.js";
34
- import {
35
- typedEntries,
36
- typedKeys
37
- } from "./chunk-HWD32NP7.js";
40
+ } from "./chunk-5WIHSJRO.js";
38
41
  import {
39
42
  ARCHIVED_SKILLS_DIR_NAME,
40
43
  CACHE_DIR,
44
+ CACHE_HASH_LENGTH,
45
+ CACHE_READABLE_PREFIX_LENGTH,
41
46
  CLAUDE_DIR,
42
47
  CLAUDE_SRC_DIR,
43
48
  DEFAULT_DISPLAY_VERSION,
49
+ DEFAULT_PLUGIN_NAME,
44
50
  DEFAULT_VERSION,
45
51
  DIRS,
46
- KEY_SUBCATEGORIES,
52
+ GITHUB_SOURCE,
53
+ HASH_PREFIX_LENGTH,
47
54
  LOCAL_SKILLS_PATH,
55
+ MAX_CONFIG_FILE_SIZE,
56
+ MAX_JSON_NESTING_DEPTH,
57
+ MAX_MARKETPLACE_FILE_SIZE,
58
+ MAX_MARKETPLACE_PLUGINS,
59
+ MAX_PLUGIN_FILE_SIZE,
48
60
  PLUGINS_SUBDIR,
49
61
  PLUGIN_MANIFEST_DIR,
50
62
  PLUGIN_MANIFEST_FILE,
51
63
  PROJECT_ROOT,
64
+ SCHEMA_PATHS,
52
65
  SKILLS_DIR_PATH,
53
- SKILLS_MATRIX_PATH
54
- } from "./chunk-O6ZTD7ZI.js";
66
+ SKILLS_MATRIX_PATH,
67
+ STANDARD_DIRS,
68
+ STANDARD_FILES,
69
+ YAML_FORMATTING,
70
+ yamlSchemaComment
71
+ } from "./chunk-IFODQTCX.js";
55
72
  import {
56
73
  init_esm_shims
57
74
  } from "./chunk-AWKZ5BDL.js";
@@ -62,16 +79,37 @@ init_esm_shims();
62
79
  // src/cli/lib/configuration/config.ts
63
80
  init_esm_shims();
64
81
  import path from "path";
65
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
66
- var DEFAULT_SOURCE = "github:claude-collective/skills";
82
+ import { stringify as stringifyYaml } from "yaml";
83
+
84
+ // src/cli/utils/yaml.ts
85
+ init_esm_shims();
86
+ import { parse as parseYaml } from "yaml";
87
+ async function safeLoadYamlFile(filePath, schema, maxSizeBytes = MAX_CONFIG_FILE_SIZE) {
88
+ try {
89
+ const content = await readFileSafe(filePath, maxSizeBytes);
90
+ const parsed = parseYaml(content);
91
+ const result = schema.safeParse(parsed);
92
+ if (!result.success) {
93
+ warn(`Invalid YAML at '${filePath}': ${result.error.message}`);
94
+ return null;
95
+ }
96
+ return result.data;
97
+ } catch (error) {
98
+ warn(`Failed to parse YAML at '${filePath}': ${error}`);
99
+ return null;
100
+ }
101
+ }
102
+
103
+ // src/cli/lib/configuration/config.ts
104
+ var DEFAULT_SOURCE = `${GITHUB_SOURCE.GITHUB_PREFIX}claude-collective/skills`;
67
105
  var SOURCE_ENV_VAR = "CC_SOURCE";
68
- var PROJECT_CONFIG_FILE = "config.yaml";
106
+ var PROJECT_CONFIG_FILE = STANDARD_FILES.CONFIG_YAML;
69
107
  function getProjectConfigPath(projectDir) {
70
108
  return path.join(projectDir, CLAUDE_SRC_DIR, PROJECT_CONFIG_FILE);
71
109
  }
72
110
  async function loadProjectSourceConfig(projectDir) {
73
111
  const srcConfigPath = getProjectConfigPath(projectDir);
74
- const legacyConfigPath = path.join(projectDir, CLAUDE_DIR, "config.yaml");
112
+ const legacyConfigPath = path.join(projectDir, CLAUDE_DIR, STANDARD_FILES.CONFIG_YAML);
75
113
  let configPath = srcConfigPath;
76
114
  if (!await fileExists(srcConfigPath)) {
77
115
  if (await fileExists(legacyConfigPath)) {
@@ -82,26 +120,18 @@ async function loadProjectSourceConfig(projectDir) {
82
120
  return null;
83
121
  }
84
122
  }
85
- try {
86
- const content = await readFile(configPath);
87
- const parsed = parseYaml(content);
88
- const result = projectSourceConfigSchema.safeParse(parsed);
89
- if (!result.success) {
90
- warn(`Invalid project config at ${configPath}: ${result.error.message}`);
91
- return null;
92
- }
93
- verbose(`Loaded project config from ${configPath}`);
94
- return result.data;
95
- } catch (error) {
96
- warn(`Failed to parse project config at ${configPath}: ${error}`);
97
- return null;
98
- }
123
+ const data = await safeLoadYamlFile(configPath, projectSourceConfigSchema);
124
+ if (!data) return null;
125
+ verbose(`Loaded project config from ${configPath}`);
126
+ return data;
99
127
  }
100
128
  async function saveProjectConfig(projectDir, config) {
101
129
  const configPath = getProjectConfigPath(projectDir);
102
130
  await ensureDir(path.join(projectDir, CLAUDE_SRC_DIR));
103
- const content = stringifyYaml(config, { lineWidth: 0 });
104
- await writeFile(configPath, content);
131
+ const schemaComment = `${yamlSchemaComment(SCHEMA_PATHS.projectSourceConfig)}
132
+ `;
133
+ const content = stringifyYaml(config, { lineWidth: YAML_FORMATTING.LINE_WIDTH_NONE });
134
+ await writeFile(configPath, `${schemaComment}${content}`);
105
135
  verbose(`Saved project config to ${configPath}`);
106
136
  }
107
137
  async function resolveSource(flagValue, projectDir) {
@@ -109,15 +139,32 @@ async function resolveSource(flagValue, projectDir) {
109
139
  const marketplace = projectConfig?.marketplace;
110
140
  if (flagValue !== void 0) {
111
141
  if (flagValue === "" || flagValue.trim() === "") {
112
- throw new Error("--source flag cannot be empty");
142
+ throw new Error(
143
+ "--source flag cannot be empty. Provide a valid source: a local directory path or a git repository URL (e.g., './my-skills' or 'https://github.com/user/repo')"
144
+ );
113
145
  }
146
+ validateSourceFormat(flagValue.trim(), "--source");
114
147
  verbose(`Source from --source flag: ${flagValue}`);
115
148
  return { source: flagValue, sourceOrigin: "flag", marketplace };
116
149
  }
117
150
  const envValue = process.env[SOURCE_ENV_VAR];
118
151
  if (envValue) {
119
- verbose(`Source from ${SOURCE_ENV_VAR} env var: ${envValue}`);
120
- return { source: envValue, sourceOrigin: "env", marketplace };
152
+ const trimmed = envValue.trim();
153
+ if (trimmed === "") {
154
+ warn(`${SOURCE_ENV_VAR} is set but empty \u2014 ignoring and falling back to next source.`);
155
+ } else {
156
+ try {
157
+ validateSourceFormat(trimmed, SOURCE_ENV_VAR);
158
+ verbose(`Source from ${SOURCE_ENV_VAR} env var: ${trimmed}`);
159
+ return { source: trimmed, sourceOrigin: "env", marketplace };
160
+ } catch (error) {
161
+ const message = error instanceof Error ? error.message : String(error);
162
+ warn(
163
+ `${SOURCE_ENV_VAR} has an invalid value \u2014 ignoring and falling back to next source.
164
+ ${message}`
165
+ );
166
+ }
167
+ }
121
168
  }
122
169
  if (projectConfig?.source) {
123
170
  verbose(`Source from project config: ${projectConfig.source}`);
@@ -133,8 +180,11 @@ async function resolveSource(flagValue, projectDir) {
133
180
  async function resolveAgentsSource(flagValue, projectDir) {
134
181
  if (flagValue !== void 0) {
135
182
  if (flagValue === "" || flagValue.trim() === "") {
136
- throw new Error("--agent-source flag cannot be empty");
183
+ throw new Error(
184
+ "--agent-source flag cannot be empty. Provide a valid source: a local directory path or a git repository URL (e.g., './my-agents' or 'https://github.com/user/repo')"
185
+ );
137
186
  }
187
+ validateSourceFormat(flagValue.trim(), "--agent-source");
138
188
  verbose(`Agents source from --agent-source flag: ${flagValue}`);
139
189
  return { agentsSource: flagValue, agentsSourceOrigin: "flag" };
140
190
  }
@@ -160,6 +210,8 @@ function formatOrigin(type, origin) {
160
210
  return `${SOURCE_ENV_VAR} environment variable`;
161
211
  case "default":
162
212
  return "default";
213
+ default:
214
+ break;
163
215
  }
164
216
  }
165
217
  switch (origin) {
@@ -167,6 +219,8 @@ function formatOrigin(type, origin) {
167
219
  return "--agent-source flag";
168
220
  case "default":
169
221
  return "default (local CLI)";
222
+ default:
223
+ break;
170
224
  }
171
225
  return origin;
172
226
  }
@@ -194,23 +248,155 @@ async function resolveAllSources(projectDir) {
194
248
  }
195
249
  return { primary, extras };
196
250
  }
251
+ var REMOTE_PROTOCOLS = [
252
+ GITHUB_SOURCE.GITHUB_PREFIX,
253
+ // "github:"
254
+ GITHUB_SOURCE.GH_PREFIX,
255
+ // "gh:"
256
+ "gitlab:",
257
+ "bitbucket:",
258
+ "sourcehut:",
259
+ "https://",
260
+ "http://"
261
+ ];
262
+ var MIN_REMOTE_PATH_LENGTH = 3;
263
+ var MAX_SOURCE_LENGTH = 512;
264
+ var NULL_BYTE_PATTERN = /\0/;
265
+ var PATH_TRAVERSAL_PATTERN = /\.\./;
266
+ var UNC_PATH_PATTERN = /^(?:\/\/|\\\\)/;
267
+ var PRIVATE_IPV4_PATTERN = /^(?:127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|0\.0\.0\.0|169\.254\.\d+\.\d+)$/;
268
+ var PRIVATE_IPV6_PATTERN = /^\[(?:::1|::ffff:(?:127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+)|fd[0-9a-f]{2}:.*|fe80:.*)\]$/i;
269
+ function validateSourceFormat(source, flagName) {
270
+ if (NULL_BYTE_PATTERN.test(source)) {
271
+ throw new Error(
272
+ `${flagName} contains invalid characters.
273
+
274
+ Source values must not contain null bytes.
275
+ Examples:
276
+ ${flagName} ./my-skills
277
+ ${flagName} github:user/repo`
278
+ );
279
+ }
280
+ if (source.length > MAX_SOURCE_LENGTH) {
281
+ throw new Error(
282
+ `${flagName} value is too long (${source.length} characters, max ${MAX_SOURCE_LENGTH}).
283
+
284
+ Provide a shorter source path or URL.
285
+ Examples:
286
+ ${flagName} ./my-skills
287
+ ${flagName} github:user/repo`
288
+ );
289
+ }
290
+ const matchedProtocol = REMOTE_PROTOCOLS.find((prefix) => source.startsWith(prefix));
291
+ if (matchedProtocol) {
292
+ validateRemoteSource(source, matchedProtocol, flagName);
293
+ } else {
294
+ validateLocalPath(source, flagName);
295
+ }
296
+ }
297
+ function validateRemoteSource(source, protocol, flagName) {
298
+ const pathAfterProtocol = source.slice(protocol.length).trim();
299
+ if (pathAfterProtocol.length < MIN_REMOTE_PATH_LENGTH) {
300
+ throw new Error(
301
+ `${flagName} has an incomplete URL: "${source}"
302
+
303
+ A repository path is required after the protocol prefix.
304
+ Examples:
305
+ ${flagName} github:user/repo
306
+ ${flagName} https://github.com/user/repo`
307
+ );
308
+ }
309
+ if (PATH_TRAVERSAL_PATTERN.test(pathAfterProtocol)) {
310
+ throw new Error(
311
+ `${flagName} contains path traversal in URL: "${source}"
312
+
313
+ Remote source URLs must not contain '..' sequences.
314
+ Examples:
315
+ ${flagName} github:user/repo
316
+ ${flagName} https://github.com/user/repo`
317
+ );
318
+ }
319
+ if (protocol === "https://" || protocol === "http://") {
320
+ validateHttpUrl(source, flagName);
321
+ }
322
+ if (protocol !== "https://" && protocol !== "http://") {
323
+ validateGitShorthand(source, pathAfterProtocol, flagName);
324
+ }
325
+ }
326
+ function validateHttpUrl(source, flagName) {
327
+ const afterProtocol = source.replace(/^https?:\/\//, "");
328
+ const hostnameWithPort = afterProtocol.split("/")[0] ?? "";
329
+ const hostname = hostnameWithPort.split(":")[0] ?? "";
330
+ const isBracketedIPv6 = hostnameWithPort.startsWith("[") && hostnameWithPort.includes("]");
331
+ if (!hostname || !hostname.includes(".") && hostname !== "localhost" && !isBracketedIPv6) {
332
+ throw new Error(
333
+ `${flagName} has an invalid URL: "${source}"
334
+
335
+ The URL must include a valid hostname.
336
+ Examples:
337
+ ${flagName} https://github.com/user/repo
338
+ ${flagName} https://gitlab.company.com/team/skills`
339
+ );
340
+ }
341
+ if (PRIVATE_IPV4_PATTERN.test(hostname) || PRIVATE_IPV6_PATTERN.test(hostnameWithPort)) {
342
+ throw new Error(
343
+ `${flagName} points to a private or reserved IP address: "${source}"
344
+
345
+ Source URLs must not target private network addresses.
346
+ Use a public hostname instead.
347
+ Examples:
348
+ ${flagName} https://github.com/user/repo
349
+ ${flagName} https://gitlab.company.com/team/skills`
350
+ );
351
+ }
352
+ }
353
+ function validateGitShorthand(source, repoPath, flagName) {
354
+ if (!repoPath.includes("/")) {
355
+ throw new Error(
356
+ `${flagName} has an invalid repository reference: "${source}"
357
+
358
+ Git shorthand sources require an owner/repo format.
359
+ Examples:
360
+ ${flagName} github:user/repo
361
+ ${flagName} gh:organization/skills`
362
+ );
363
+ }
364
+ }
365
+ function validateLocalPath(source, flagName) {
366
+ const CONTROL_CHAR_PATTERN2 = /[\x00-\x08\x0E-\x1F\x7F]/u;
367
+ if (CONTROL_CHAR_PATTERN2.test(source)) {
368
+ throw new Error(
369
+ `${flagName} contains invalid characters: "${source}"
370
+
371
+ Source paths must not contain control characters.
372
+ Examples:
373
+ ${flagName} ./my-skills
374
+ ${flagName} /home/user/skills`
375
+ );
376
+ }
377
+ if (UNC_PATH_PATTERN.test(source)) {
378
+ throw new Error(
379
+ `${flagName} contains a UNC network path: "${source}"
380
+
381
+ Network paths (\\\\server\\share or //server/share) are not allowed for security reasons.
382
+ Use a local directory path or a remote URL instead.
383
+ Examples:
384
+ ${flagName} ./my-skills
385
+ ${flagName} /home/user/skills
386
+ ${flagName} https://github.com/user/repo`
387
+ );
388
+ }
389
+ }
197
390
  function isLocalSource(source) {
198
391
  if (source.startsWith("/") || source.startsWith(".")) {
199
392
  return true;
200
393
  }
201
- const remoteProtocols = [
202
- "github:",
203
- "gh:",
204
- "gitlab:",
205
- "bitbucket:",
206
- "sourcehut:",
207
- "https://",
208
- "http://"
209
- ];
210
- const hasRemoteProtocol = remoteProtocols.some((prefix) => source.startsWith(prefix));
394
+ const hasRemoteProtocol = REMOTE_PROTOCOLS.some((prefix) => source.startsWith(prefix));
211
395
  if (!hasRemoteProtocol) {
212
396
  if (source.includes("..") || source.includes("~")) {
213
- throw new Error(`Invalid source path: ${source}. Path traversal patterns are not allowed.`);
397
+ throw new Error(
398
+ `Invalid source path: ${source}. Path traversal patterns like '..' and '~' are not allowed for security reasons. Use absolute paths or remote URLs instead (e.g., '/home/user/skills' or 'https://github.com/user/repo').`
399
+ );
214
400
  }
215
401
  }
216
402
  return !hasRemoteProtocol;
@@ -218,8 +404,9 @@ function isLocalSource(source) {
218
404
 
219
405
  // src/cli/lib/loading/source-fetcher.ts
220
406
  init_esm_shims();
221
- import path25 from "path";
407
+ import { createHash as createHash2 } from "crypto";
222
408
  import { downloadTemplate } from "giget";
409
+ import path24 from "path";
223
410
 
224
411
  // src/cli/lib/configuration/index.ts
225
412
  init_esm_shims();
@@ -227,45 +414,73 @@ init_esm_shims();
227
414
  // src/cli/lib/configuration/config-generator.ts
228
415
  init_esm_shims();
229
416
 
417
+ // src/cli/utils/typed-object.ts
418
+ init_esm_shims();
419
+ function typedEntries(obj) {
420
+ return Object.entries(obj);
421
+ }
422
+ function typedKeys(obj) {
423
+ return Object.keys(obj);
424
+ }
425
+
230
426
  // src/cli/lib/skills/index.ts
231
427
  init_esm_shims();
232
428
 
233
429
  // src/cli/lib/skills/skill-metadata.ts
234
430
  init_esm_shims();
235
431
  import path3 from "path";
236
- import { parse as parseYaml3, stringify as stringifyYaml3 } from "yaml";
237
432
  import { sortBy } from "remeda";
433
+ import { parse as parseYaml2, stringify as stringifyYaml2 } from "yaml";
238
434
 
239
435
  // src/cli/lib/versioning.ts
240
436
  init_esm_shims();
241
437
  import { createHash } from "crypto";
242
438
  import path2 from "path";
243
- import { stringify as stringifyYaml2, parse as parseYaml2 } from "yaml";
244
- var HASH_PREFIX_LENGTH = 7;
245
- var HASHABLE_FILES = ["SKILL.md", "reference.md"];
246
- var HASHABLE_DIRS = ["examples", "scripts"];
439
+
440
+ // src/cli/lib/metadata-keys.ts
441
+ init_esm_shims();
442
+ var METADATA_KEYS = {
443
+ CLI_NAME: "cli_name",
444
+ CLI_DESCRIPTION: "cli_description",
445
+ CATEGORY: "category",
446
+ FORKED_FROM: "forked_from",
447
+ CONTENT_HASH: "content_hash",
448
+ USAGE_GUIDANCE: "usage_guidance"
449
+ };
450
+ var IMPORT_DEFAULTS = {
451
+ CATEGORY: "imported",
452
+ AUTHOR: "@imported"
453
+ };
454
+ var LOCAL_DEFAULTS = {
455
+ CATEGORY: "local",
456
+ AUTHOR: "@local"
457
+ };
458
+ var SKILL_CONTENT_FILES = [STANDARD_FILES.SKILL_MD, STANDARD_FILES.REFERENCE_MD];
459
+ var SKILL_CONTENT_DIRS = [STANDARD_DIRS.EXAMPLES, STANDARD_DIRS.SCRIPTS];
460
+
461
+ // src/cli/lib/versioning.ts
247
462
  function getCurrentDate() {
248
463
  return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
249
464
  }
250
- function hashString(content) {
465
+ function computeStringHash(content) {
251
466
  const hash = createHash("sha256");
252
467
  hash.update(content);
253
468
  return hash.digest("hex").slice(0, HASH_PREFIX_LENGTH);
254
469
  }
255
- async function hashFile(filePath) {
470
+ async function computeFileHash(filePath) {
256
471
  const content = await readFile(filePath);
257
- return hashString(content);
472
+ return computeStringHash(content);
258
473
  }
259
- async function hashSkillFolder(skillPath) {
474
+ async function computeSkillFolderHash(skillPath) {
260
475
  const contents = [];
261
- for (const fileName of HASHABLE_FILES) {
476
+ for (const fileName of SKILL_CONTENT_FILES) {
262
477
  const filePath = path2.join(skillPath, fileName);
263
478
  if (await fileExists(filePath)) {
264
479
  const content = await readFile(filePath);
265
480
  contents.push(`${fileName}:${content}`);
266
481
  }
267
482
  }
268
- for (const dirName of HASHABLE_DIRS) {
483
+ for (const dirName of SKILL_CONTENT_DIRS) {
269
484
  const dirPath = path2.join(skillPath, dirName);
270
485
  if (await fileExists(dirPath)) {
271
486
  const files = await glob("**/*", dirPath);
@@ -277,7 +492,7 @@ async function hashSkillFolder(skillPath) {
277
492
  }
278
493
  }
279
494
  const combined = contents.join("\n---\n");
280
- return hashString(combined);
495
+ return computeStringHash(combined);
281
496
  }
282
497
  var CONTENT_HASH_FILE = ".content-hash";
283
498
  function parseMajorVersion(version) {
@@ -294,9 +509,9 @@ async function readExistingPluginManifest(pluginDir, getManifestPath) {
294
509
  return null;
295
510
  }
296
511
  try {
297
- const content = await readFile(manifestPath);
512
+ const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
298
513
  const manifest = pluginManifestSchema.parse(JSON.parse(content));
299
- const hashFilePath = manifestPath.replace("plugin.json", CONTENT_HASH_FILE);
514
+ const hashFilePath = manifestPath.replace(STANDARD_FILES.PLUGIN_JSON, CONTENT_HASH_FILE);
300
515
  let contentHash;
301
516
  if (await fileExists(hashFilePath)) {
302
517
  contentHash = (await readFile(hashFilePath)).trim();
@@ -305,7 +520,8 @@ async function readExistingPluginManifest(pluginDir, getManifestPath) {
305
520
  version: manifest.version ?? DEFAULT_VERSION,
306
521
  contentHash
307
522
  };
308
- } catch {
523
+ } catch (error) {
524
+ warn(`Failed to read plugin manifest at '${manifestPath}': ${getErrorMessage(error)}`);
309
525
  return null;
310
526
  }
311
527
  }
@@ -329,22 +545,23 @@ async function determinePluginVersion(newHash, pluginDir, getManifestPath) {
329
545
  };
330
546
  }
331
547
  async function writeContentHash(pluginDir, contentHash, getManifestPath) {
332
- const hashFilePath = getManifestPath(pluginDir).replace("plugin.json", CONTENT_HASH_FILE);
548
+ const hashFilePath = getManifestPath(pluginDir).replace(
549
+ STANDARD_FILES.PLUGIN_JSON,
550
+ CONTENT_HASH_FILE
551
+ );
333
552
  await writeFile(hashFilePath, contentHash);
334
553
  }
335
554
 
336
555
  // src/cli/lib/skills/skill-metadata.ts
337
556
  async function readForkedFromMetadata(skillDir) {
338
- const metadataPath = path3.join(skillDir, "metadata.yaml");
557
+ const metadataPath = path3.join(skillDir, STANDARD_FILES.METADATA_YAML);
339
558
  if (!await fileExists(metadataPath)) {
340
559
  return null;
341
560
  }
342
561
  const content = await readFile(metadataPath);
343
- const result = localSkillMetadataSchema.safeParse(parseYaml3(content));
562
+ const result = localSkillMetadataSchema.safeParse(parseYaml2(content));
344
563
  if (!result.success) {
345
- warn(
346
- `Invalid metadata.yaml at ${metadataPath}: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
347
- );
564
+ warn(`Invalid metadata.yaml at ${metadataPath}: ${formatZodErrors(result.error.issues)}`);
348
565
  return null;
349
566
  }
350
567
  return result.data.forked_from ?? null;
@@ -365,13 +582,13 @@ async function getLocalSkillsWithMetadata(projectDir) {
365
582
  return result;
366
583
  }
367
584
  async function computeSourceHash(sourcePath, skillPath) {
368
- const skillMdPath = path3.join(sourcePath, "src", skillPath, "SKILL.md");
585
+ const skillMdPath = path3.join(sourcePath, "src", skillPath, STANDARD_FILES.SKILL_MD);
369
586
  if (!await fileExists(skillMdPath)) {
370
587
  return null;
371
588
  }
372
- return hashFile(skillMdPath);
589
+ return computeFileHash(skillMdPath);
373
590
  }
374
- async function compareSkills(projectDir, sourcePath, sourceSkills) {
591
+ async function compareLocalSkillsWithSource(projectDir, sourcePath, sourceSkills) {
375
592
  const results = [];
376
593
  const localSkills = await getLocalSkillsWithMetadata(projectDir);
377
594
  for (const [skillId, { dirName, forkedFrom }] of localSkills) {
@@ -421,16 +638,16 @@ async function compareSkills(projectDir, sourcePath, sourceSkills) {
421
638
  return sortBy(results, (r) => r.id);
422
639
  }
423
640
  async function injectForkedFromMetadata(destPath, skillId, contentHash) {
424
- const metadataPath = path3.join(destPath, "metadata.yaml");
641
+ const metadataPath = path3.join(destPath, STANDARD_FILES.METADATA_YAML);
425
642
  const rawContent = await readFile(metadataPath);
426
643
  const lines = rawContent.split("\n");
427
644
  let yamlContent = rawContent;
428
645
  if (lines[0]?.startsWith("# yaml-language-server:")) {
429
646
  yamlContent = lines.slice(1).join("\n");
430
647
  }
431
- const parseResult = localSkillMetadataSchema.safeParse(parseYaml3(yamlContent));
648
+ const parseResult = localSkillMetadataSchema.safeParse(parseYaml2(yamlContent));
432
649
  if (!parseResult.success) {
433
- warn(`Malformed metadata.yaml at ${metadataPath} \u2014 existing fields may be lost`);
650
+ warn(`Malformed metadata.yaml at '${metadataPath}' \u2014 existing fields may be lost`);
434
651
  }
435
652
  const metadata = parseResult.success ? parseResult.data : { forked_from: void 0 };
436
653
  metadata.forked_from = {
@@ -438,23 +655,45 @@ async function injectForkedFromMetadata(destPath, skillId, contentHash) {
438
655
  content_hash: contentHash,
439
656
  date: getCurrentDate()
440
657
  };
441
- const newYamlContent = stringifyYaml3(metadata, { lineWidth: 0 });
442
- await writeFile(metadataPath, newYamlContent);
658
+ const schemaComment = `${yamlSchemaComment(SCHEMA_PATHS.metadata)}
659
+ `;
660
+ const newYamlContent = stringifyYaml2(metadata, { lineWidth: YAML_FORMATTING.LINE_WIDTH_NONE });
661
+ await writeFile(metadataPath, `${schemaComment}${newYamlContent}`);
443
662
  }
444
663
 
445
664
  // src/cli/lib/skills/skill-copier.ts
446
665
  init_esm_shims();
447
666
  import path4 from "path";
667
+ var NULL_BYTE_PATTERN2 = /\0/;
668
+ function validateSkillPath(resolvedPath, expectedParent, skillPath) {
669
+ if (NULL_BYTE_PATTERN2.test(skillPath)) {
670
+ throw new Error(`Invalid skill path: '${skillPath}' contains null bytes`);
671
+ }
672
+ const normalizedResolved = path4.resolve(resolvedPath);
673
+ const normalizedParent = path4.resolve(expectedParent);
674
+ if (!normalizedResolved.startsWith(normalizedParent + path4.sep) && normalizedResolved !== normalizedParent) {
675
+ throw new Error(
676
+ `Invalid skill path: '${skillPath}' escapes expected directory '${normalizedParent}'`
677
+ );
678
+ }
679
+ }
680
+ function resolveSkillPath(basePath, skillPath) {
681
+ const resolved = path4.join(basePath, skillPath);
682
+ validateSkillPath(resolved, basePath, skillPath);
683
+ return resolved;
684
+ }
448
685
  function getSkillDestPath(skill, stackDir) {
449
686
  const skillRelativePath = skill.path.replace(/^skills\//, "");
450
- return path4.join(stackDir, "skills", skillRelativePath);
687
+ const skillsDir = path4.join(stackDir, "skills");
688
+ return resolveSkillPath(skillsDir, skillRelativePath);
451
689
  }
452
690
  async function generateSkillHash(skillSourcePath) {
453
- const skillMdPath = path4.join(skillSourcePath, "SKILL.md");
454
- return hashFile(skillMdPath);
691
+ const skillMdPath = path4.join(skillSourcePath, STANDARD_FILES.SKILL_MD);
692
+ return computeFileHash(skillMdPath);
455
693
  }
456
694
  function getSkillSourcePathFromSource(skill, sourceResult) {
457
- return path4.join(sourceResult.sourcePath, "src", skill.path);
695
+ const srcDir = path4.join(sourceResult.sourcePath, "src");
696
+ return resolveSkillPath(srcDir, skill.path);
458
697
  }
459
698
  async function copySkillFromSource(skill, stackDir, sourceResult) {
460
699
  const sourcePath = getSkillSourcePathFromSource(skill, sourceResult);
@@ -470,35 +709,43 @@ async function copySkillFromSource(skill, stackDir, sourceResult) {
470
709
  destPath
471
710
  };
472
711
  }
473
- async function copySkillsToPluginFromSource(selectedSkillIds, pluginDir, matrix, sourceResult, sourceSelections) {
474
- const copiedSkills = [];
475
- for (const skillId of selectedSkillIds) {
476
- const skill = matrix.skills[skillId];
477
- if (!skill) {
478
- console.warn(`Warning: Skill not found in matrix: ${skillId}`);
479
- continue;
480
- }
481
- const selectedSource = sourceSelections?.[skillId];
482
- const userSelectedRemote = selectedSource && selectedSource !== "local";
483
- if (skill.local && skill.localPath && !userSelectedRemote) {
484
- const localSkillPath = path4.join(process.cwd(), skill.localPath);
485
- const contentHash = await generateSkillHash(localSkillPath);
486
- copiedSkills.push({
487
- skillId: skill.id,
488
- sourcePath: skill.localPath,
489
- destPath: skill.localPath,
490
- contentHash,
491
- local: true
492
- });
493
- continue;
494
- }
495
- const copied = await copySkillFromSource(skill, pluginDir, sourceResult);
496
- copiedSkills.push(copied);
497
- }
498
- return copiedSkills;
712
+ async function copySkillsToPluginFromSource(selectedSkillIds, pluginDir, matrix, sourceResult, sourceSelections, onProgress) {
713
+ const total = selectedSkillIds.length;
714
+ let completed = 0;
715
+ const results = await Promise.all(
716
+ selectedSkillIds.map(async (skillId) => {
717
+ const skill = matrix.skills[skillId];
718
+ if (!skill) {
719
+ warn(`Skill not found in matrix: '${skillId}'`);
720
+ completed++;
721
+ onProgress?.(completed, total);
722
+ return null;
723
+ }
724
+ const selectedSource = sourceSelections?.[skillId];
725
+ const userSelectedRemote = selectedSource && selectedSource !== "local";
726
+ let result;
727
+ if (skill.local && skill.localPath && !userSelectedRemote) {
728
+ const localSkillPath = path4.join(process.cwd(), skill.localPath);
729
+ const contentHash = await generateSkillHash(localSkillPath);
730
+ result = {
731
+ skillId: skill.id,
732
+ sourcePath: skill.localPath,
733
+ destPath: skill.localPath,
734
+ contentHash,
735
+ local: true
736
+ };
737
+ } else {
738
+ result = await copySkillFromSource(skill, pluginDir, sourceResult);
739
+ }
740
+ completed++;
741
+ onProgress?.(completed, total);
742
+ return result;
743
+ })
744
+ );
745
+ return results.filter((r) => r !== null);
499
746
  }
500
747
  function getFlattenedSkillDestPath(skill, localSkillsDir) {
501
- return path4.join(localSkillsDir, skill.id);
748
+ return resolveSkillPath(localSkillsDir, skill.id);
502
749
  }
503
750
  async function copySkillToLocalFlattened(skill, localSkillsDir, sourceResult) {
504
751
  const sourcePath = getSkillSourcePathFromSource(skill, sourceResult);
@@ -515,31 +762,30 @@ async function copySkillToLocalFlattened(skill, localSkillsDir, sourceResult) {
515
762
  };
516
763
  }
517
764
  async function copySkillsToLocalFlattened(selectedSkillIds, localSkillsDir, matrix, sourceResult, sourceSelections) {
518
- const copiedSkills = [];
519
- for (const skillId of selectedSkillIds) {
520
- const skill = matrix.skills[skillId];
521
- if (!skill) {
522
- console.warn(`Warning: Skill not found in matrix: ${skillId}`);
523
- continue;
524
- }
525
- const selectedSource = sourceSelections?.[skillId];
526
- const userSelectedRemote = selectedSource && selectedSource !== "local";
527
- if (skill.local && skill.localPath && !userSelectedRemote) {
528
- const localSkillPath = path4.join(process.cwd(), skill.localPath);
529
- const contentHash = await generateSkillHash(localSkillPath);
530
- copiedSkills.push({
531
- skillId: skill.id,
532
- sourcePath: skill.localPath,
533
- destPath: skill.localPath,
534
- contentHash,
535
- local: true
536
- });
537
- continue;
538
- }
539
- const copied = await copySkillToLocalFlattened(skill, localSkillsDir, sourceResult);
540
- copiedSkills.push(copied);
541
- }
542
- return copiedSkills;
765
+ const results = await Promise.all(
766
+ selectedSkillIds.map(async (skillId) => {
767
+ const skill = matrix.skills[skillId];
768
+ if (!skill) {
769
+ warn(`Skill not found in matrix: '${skillId}'`);
770
+ return null;
771
+ }
772
+ const selectedSource = sourceSelections?.[skillId];
773
+ const userSelectedRemote = selectedSource && selectedSource !== "local";
774
+ if (skill.local && skill.localPath && !userSelectedRemote) {
775
+ const localSkillPath = path4.join(process.cwd(), skill.localPath);
776
+ const contentHash = await generateSkillHash(localSkillPath);
777
+ return {
778
+ skillId: skill.id,
779
+ sourcePath: skill.localPath,
780
+ destPath: skill.localPath,
781
+ contentHash,
782
+ local: true
783
+ };
784
+ }
785
+ return copySkillToLocalFlattened(skill, localSkillsDir, sourceResult);
786
+ })
787
+ );
788
+ return results.filter((r) => r !== null);
543
789
  }
544
790
 
545
791
  // src/cli/lib/skills/skill-agent-mappings.ts
@@ -550,7 +796,7 @@ init_esm_shims();
550
796
 
551
797
  // src/cli/lib/loading/loader.ts
552
798
  init_esm_shims();
553
- import { parse as parseYaml4 } from "yaml";
799
+ import { parse as parseYaml3 } from "yaml";
554
800
  import path5 from "path";
555
801
  import { unique } from "remeda";
556
802
  var FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/;
@@ -558,12 +804,10 @@ function parseFrontmatter(content, filePath) {
558
804
  const match = content.match(FRONTMATTER_REGEX);
559
805
  if (!match) return null;
560
806
  const yamlContent = match[1];
561
- const parsed = skillFrontmatterLoaderSchema.safeParse(parseYaml4(yamlContent));
807
+ const parsed = skillFrontmatterLoaderSchema.safeParse(parseYaml3(yamlContent));
562
808
  if (!parsed.success) {
563
809
  const location = filePath ?? "unknown file";
564
- warn(
565
- `Invalid SKILL.md frontmatter in ${location}: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
566
- );
810
+ warn(`Invalid SKILL.md frontmatter in '${location}': ${formatZodErrors(parsed.error.issues)}`);
567
811
  return null;
568
812
  }
569
813
  return parsed.data;
@@ -576,7 +820,7 @@ async function loadAllAgents(projectRoot) {
576
820
  const fullPath = path5.join(agentSourcesDir, file);
577
821
  try {
578
822
  const content = await readFile(fullPath);
579
- const config = agentYamlConfigSchema.parse(parseYaml4(content));
823
+ const config = agentYamlConfigSchema.parse(parseYaml3(content));
580
824
  const agentPath = path5.dirname(file);
581
825
  agents[config.id] = {
582
826
  title: config.title,
@@ -588,9 +832,7 @@ async function loadAllAgents(projectRoot) {
588
832
  };
589
833
  verbose(`Loaded agent: ${config.id} from ${file}`);
590
834
  } catch (error) {
591
- warn(
592
- `Skipping invalid agent.yaml at ${fullPath}: ${error instanceof Error ? error.message : String(error)}`
593
- );
835
+ warn(`Skipping invalid agent.yaml at '${fullPath}': ${getErrorMessage(error)}`);
594
836
  }
595
837
  }
596
838
  return agents;
@@ -607,7 +849,7 @@ async function loadProjectAgents(projectRoot) {
607
849
  const fullPath = path5.join(projectAgentsDir, file);
608
850
  try {
609
851
  const content = await readFile(fullPath);
610
- const config = agentYamlConfigSchema.parse(parseYaml4(content));
852
+ const config = agentYamlConfigSchema.parse(parseYaml3(content));
611
853
  const agentPath = path5.dirname(file);
612
854
  agents[config.id] = {
613
855
  title: config.title,
@@ -621,9 +863,7 @@ async function loadProjectAgents(projectRoot) {
621
863
  };
622
864
  verbose(`Loaded project agent: ${config.id} from ${file}`);
623
865
  } catch (error) {
624
- warn(
625
- `Skipping invalid agent.yaml at ${fullPath}: ${error instanceof Error ? error.message : String(error)}`
626
- );
866
+ warn(`Skipping invalid agent.yaml at '${fullPath}': ${getErrorMessage(error)}`);
627
867
  }
628
868
  }
629
869
  return agents;
@@ -655,13 +895,13 @@ async function loadSkillsByIds(skillIds, projectRoot) {
655
895
  } else {
656
896
  const childSkills = allSkillIds.filter((id) => {
657
897
  const dirPath = idToDirectoryPath[id];
658
- return dirPath.startsWith(skillId + "/");
898
+ return dirPath.startsWith(`${skillId}/`);
659
899
  });
660
900
  if (childSkills.length > 0) {
661
901
  expandedSkillIds.push(...childSkills);
662
902
  verbose(`Expanded directory '${skillId}' to ${childSkills.length} skills`);
663
903
  } else {
664
- console.warn(` Warning: Unknown skill reference '${skillId}'`);
904
+ warn(`Unknown skill reference '${skillId}'`);
665
905
  }
666
906
  }
667
907
  }
@@ -669,16 +909,16 @@ async function loadSkillsByIds(skillIds, projectRoot) {
669
909
  for (const skillId of uniqueSkillIds) {
670
910
  const directoryPath = idToDirectoryPath[skillId];
671
911
  if (!directoryPath) {
672
- console.warn(` Warning: Could not find skill ${skillId}: No matching skill found`);
912
+ warn(`Could not find skill '${skillId}': no matching skill found`);
673
913
  continue;
674
914
  }
675
915
  const skillPath = path5.join(skillsDir, directoryPath);
676
- const skillMdPath = path5.join(skillPath, "SKILL.md");
916
+ const skillMdPath = path5.join(skillPath, STANDARD_FILES.SKILL_MD);
677
917
  try {
678
918
  const content = await readFile(skillMdPath);
679
919
  const frontmatter = parseFrontmatter(content, skillMdPath);
680
920
  if (!frontmatter) {
681
- warn(`Skipping ${skillId}: Missing or invalid frontmatter`);
921
+ warn(`Skipping '${skillId}': missing or invalid frontmatter`);
682
922
  continue;
683
923
  }
684
924
  const canonicalId = frontmatter.name;
@@ -693,7 +933,7 @@ async function loadSkillsByIds(skillIds, projectRoot) {
693
933
  }
694
934
  verbose(`Loaded skill: ${canonicalId} (from ${directoryPath})`);
695
935
  } catch (error) {
696
- console.warn(` Warning: Could not load skill ${skillId}: ${error}`);
936
+ warn(`Could not load skill '${skillId}': ${error}`);
697
937
  }
698
938
  }
699
939
  return skills;
@@ -710,7 +950,7 @@ async function loadPluginSkills(pluginDir) {
710
950
  const content = await readFile(fullPath);
711
951
  const frontmatter = parseFrontmatter(content, fullPath);
712
952
  if (!frontmatter) {
713
- warn(`Skipping ${file}: Missing or invalid frontmatter`);
953
+ warn(`Skipping '${file}': missing or invalid frontmatter`);
714
954
  continue;
715
955
  }
716
956
  const folderPath = file.replace("/SKILL.md", "");
@@ -735,7 +975,7 @@ init_esm_shims();
735
975
 
736
976
  // src/cli/lib/matrix/matrix-loader.ts
737
977
  init_esm_shims();
738
- import { parse as parseYaml5 } from "yaml";
978
+ import { parse as parseYaml4 } from "yaml";
739
979
  import path6 from "path";
740
980
  import { z } from "zod";
741
981
  var rawMetadataSchema = z.object({
@@ -756,11 +996,11 @@ var rawMetadataSchema = z.object({
756
996
  });
757
997
  async function loadSkillsMatrix(configPath) {
758
998
  const content = await readFile(configPath);
759
- const raw = parseYaml5(content);
999
+ const raw = parseYaml4(content);
760
1000
  const result = skillsMatrixConfigSchema.safeParse(raw);
761
1001
  if (!result.success) {
762
1002
  throw new Error(
763
- `Invalid skills matrix at ${configPath}: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
1003
+ `Invalid skills matrix at '${configPath}': ${formatZodErrors(result.error.issues)}`
764
1004
  );
765
1005
  }
766
1006
  verbose(`Loaded skills matrix: ${configPath}`);
@@ -768,21 +1008,21 @@ async function loadSkillsMatrix(configPath) {
768
1008
  }
769
1009
  async function extractAllSkills(skillsDir) {
770
1010
  const skills = [];
771
- const metadataFiles = await glob("**/metadata.yaml", skillsDir);
1011
+ const metadataFiles = await glob(`**/${STANDARD_FILES.METADATA_YAML}`, skillsDir);
772
1012
  for (const metadataFile of metadataFiles) {
773
1013
  const skillDir = path6.dirname(metadataFile);
774
- const skillMdPath = path6.join(skillsDir, skillDir, "SKILL.md");
1014
+ const skillMdPath = path6.join(skillsDir, skillDir, STANDARD_FILES.SKILL_MD);
775
1015
  const metadataPath = path6.join(skillsDir, metadataFile);
776
1016
  if (!await fileExists(skillMdPath)) {
777
- verbose(`Skipping ${metadataFile}: No SKILL.md found`);
1017
+ verbose(`Skipping ${metadataFile}: No ${STANDARD_FILES.SKILL_MD} found`);
778
1018
  continue;
779
1019
  }
780
1020
  const metadataContent = await readFile(metadataPath);
781
- const rawMetadata = parseYaml5(metadataContent);
1021
+ const rawMetadata = parseYaml4(metadataContent);
782
1022
  const metadataResult = rawMetadataSchema.safeParse(rawMetadata);
783
1023
  if (!metadataResult.success) {
784
1024
  warn(
785
- `Skipping ${metadataFile}: Invalid metadata.yaml \u2014 ${metadataResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
1025
+ `Skipping '${metadataFile}': invalid metadata.yaml \u2014 ${formatZodErrors(metadataResult.error.issues)}`
786
1026
  );
787
1027
  continue;
788
1028
  }
@@ -795,7 +1035,7 @@ async function extractAllSkills(skillsDir) {
795
1035
  }
796
1036
  if (!metadata.cli_name) {
797
1037
  throw new Error(
798
- `Skill at ${metadataFile} is missing required 'cli_name' field in metadata.yaml`
1038
+ `Skill at ${metadataFile} is missing required '${METADATA_KEYS.CLI_NAME}' field in metadata.yaml`
799
1039
  );
800
1040
  }
801
1041
  const skillId = frontmatter.name;
@@ -849,10 +1089,13 @@ function buildAliasTargetToSkillIdMap(displayNameToId, skills) {
849
1089
  }
850
1090
  const aliasTargets = new Set(Object.values(displayNameToId));
851
1091
  for (const skill of skills) {
852
- for (const aliasTarget of aliasTargets) {
853
- if (aliasTarget !== skill.id && (skill.id.endsWith(`/${aliasTarget}`) || skill.id === aliasTarget)) {
854
- map[aliasTarget] = skill.id;
1092
+ let slashIdx = skill.id.indexOf("/");
1093
+ while (slashIdx !== -1) {
1094
+ const suffix = skill.id.slice(slashIdx + 1);
1095
+ if (suffix && aliasTargets.has(suffix) && suffix !== skill.id) {
1096
+ map[suffix] = skill.id;
855
1097
  }
1098
+ slashIdx = skill.id.indexOf("/", slashIdx + 1);
856
1099
  }
857
1100
  }
858
1101
  return map;
@@ -911,110 +1154,99 @@ async function mergeMatrixWithSkills(matrix, skills) {
911
1154
  };
912
1155
  return merged;
913
1156
  }
914
- function buildResolvedSkill(skill, matrix, displayNameToId, displayNames, directoryPathToId, aliasTargetToSkillId) {
915
- const conflictsWith = [];
916
- const recommends = [];
917
- const requires = [];
918
- const alternatives = [];
919
- const discourages = [];
920
- const resolve = (id, relationContext) => resolveToCanonicalId(
921
- id,
922
- displayNameToId,
923
- directoryPathToId,
924
- aliasTargetToSkillId,
925
- relationContext ? `${skill.id} ${relationContext}` : void 0
926
- );
927
- for (const conflictRef of skill.conflictsWith) {
928
- const canonicalId = resolve(conflictRef, "conflictsWith");
929
- conflictsWith.push({
930
- skillId: canonicalId,
1157
+ function resolveConflicts(skillId, metadataConflicts, conflictRules, resolve) {
1158
+ const conflicts = [];
1159
+ for (const conflictRef of metadataConflicts) {
1160
+ conflicts.push({
1161
+ skillId: resolve(conflictRef, "conflictsWith"),
931
1162
  reason: "Defined in skill metadata"
932
1163
  });
933
1164
  }
934
- for (const conflictRule of matrix.relationships.conflicts) {
935
- const resolvedSkills = conflictRule.skills.map((id) => resolve(id, "conflicts"));
936
- if (resolvedSkills.includes(skill.id)) {
937
- for (const otherSkill of resolvedSkills) {
938
- if (otherSkill !== skill.id) {
939
- if (!conflictsWith.some((c) => c.skillId === otherSkill)) {
940
- conflictsWith.push({
941
- skillId: otherSkill,
942
- reason: conflictRule.reason
943
- });
944
- }
945
- }
1165
+ for (const rule of conflictRules) {
1166
+ const resolved = rule.skills.map((id) => resolve(id, "conflicts"));
1167
+ if (!resolved.includes(skillId)) continue;
1168
+ for (const other of resolved) {
1169
+ if (other !== skillId && !conflicts.some((c) => c.skillId === other)) {
1170
+ conflicts.push({ skillId: other, reason: rule.reason });
946
1171
  }
947
1172
  }
948
1173
  }
949
- for (const compatRef of skill.compatibleWith) {
950
- const canonicalId = resolve(compatRef, "compatibleWith");
1174
+ return conflicts;
1175
+ }
1176
+ function resolveRecommends(skillId, compatibleWith, recommendRules, resolve) {
1177
+ const recommends = [];
1178
+ for (const compatRef of compatibleWith) {
951
1179
  recommends.push({
952
- skillId: canonicalId,
1180
+ skillId: resolve(compatRef, "compatibleWith"),
953
1181
  reason: "Compatible with this skill"
954
1182
  });
955
1183
  }
956
- for (const recommendRule of matrix.relationships.recommends) {
957
- const whenCanonicalId = resolve(recommendRule.when, "recommends.when");
958
- if (whenCanonicalId === skill.id) {
959
- for (const suggested of recommendRule.suggest) {
960
- const canonicalId = resolve(suggested, "recommends.suggest");
961
- if (!recommends.some((r) => r.skillId === canonicalId)) {
962
- recommends.push({
963
- skillId: canonicalId,
964
- reason: recommendRule.reason
965
- });
966
- }
1184
+ for (const rule of recommendRules) {
1185
+ if (resolve(rule.when, "recommends.when") !== skillId) continue;
1186
+ for (const suggested of rule.suggest) {
1187
+ const canonicalId = resolve(suggested, "recommends.suggest");
1188
+ if (!recommends.some((r) => r.skillId === canonicalId)) {
1189
+ recommends.push({ skillId: canonicalId, reason: rule.reason });
967
1190
  }
968
1191
  }
969
1192
  }
970
- if (skill.requires.length > 0) {
1193
+ return recommends;
1194
+ }
1195
+ function resolveRequirements(skillId, metadataRequires, requireRules, resolve) {
1196
+ const requires = [];
1197
+ if (metadataRequires.length > 0) {
971
1198
  requires.push({
972
- skillIds: skill.requires.map((id) => resolve(id, "requires")),
1199
+ skillIds: metadataRequires.map((id) => resolve(id, "requires")),
973
1200
  needsAny: false,
974
1201
  reason: "Defined in skill metadata"
975
1202
  });
976
1203
  }
977
- for (const requireRule of matrix.relationships.requires) {
978
- const skillCanonicalId = resolve(requireRule.skill, "requires.skill");
979
- if (skillCanonicalId === skill.id) {
980
- requires.push({
981
- skillIds: requireRule.needs.map((id) => resolve(id, "requires.needs")),
982
- needsAny: requireRule.needs_any ?? false,
983
- reason: requireRule.reason
984
- });
985
- }
1204
+ for (const rule of requireRules) {
1205
+ if (resolve(rule.skill, "requires.skill") !== skillId) continue;
1206
+ requires.push({
1207
+ skillIds: rule.needs.map((id) => resolve(id, "requires.needs")),
1208
+ needsAny: rule.needs_any ?? false,
1209
+ reason: rule.reason
1210
+ });
986
1211
  }
987
- for (const altGroup of matrix.relationships.alternatives) {
988
- const resolvedAlts = altGroup.skills.map((id) => resolve(id, "alternatives"));
989
- if (resolvedAlts.includes(skill.id)) {
990
- for (const altSkill of resolvedAlts) {
991
- if (altSkill !== skill.id) {
992
- alternatives.push({
993
- skillId: altSkill,
994
- purpose: altGroup.purpose
995
- });
996
- }
1212
+ return requires;
1213
+ }
1214
+ function resolveAlternatives(skillId, alternativeGroups, resolve) {
1215
+ const alternatives = [];
1216
+ for (const group of alternativeGroups) {
1217
+ const resolved = group.skills.map((id) => resolve(id, "alternatives"));
1218
+ if (!resolved.includes(skillId)) continue;
1219
+ for (const alt of resolved) {
1220
+ if (alt !== skillId) {
1221
+ alternatives.push({ skillId: alt, purpose: group.purpose });
997
1222
  }
998
1223
  }
999
1224
  }
1000
- if (matrix.relationships.discourages) {
1001
- for (const discourageRule of matrix.relationships.discourages) {
1002
- const resolvedSkills = discourageRule.skills.map((id) => resolve(id, "discourages"));
1003
- if (resolvedSkills.includes(skill.id)) {
1004
- for (const otherSkill of resolvedSkills) {
1005
- if (otherSkill !== skill.id) {
1006
- if (!discourages.some((d) => d.skillId === otherSkill)) {
1007
- discourages.push({
1008
- skillId: otherSkill,
1009
- reason: discourageRule.reason
1010
- });
1011
- }
1012
- }
1013
- }
1225
+ return alternatives;
1226
+ }
1227
+ function resolveDiscourages(skillId, discourageRules, resolve) {
1228
+ if (!discourageRules) return [];
1229
+ const discourages = [];
1230
+ for (const rule of discourageRules) {
1231
+ const resolved = rule.skills.map((id) => resolve(id, "discourages"));
1232
+ if (!resolved.includes(skillId)) continue;
1233
+ for (const other of resolved) {
1234
+ if (other !== skillId && !discourages.some((d) => d.skillId === other)) {
1235
+ discourages.push({ skillId: other, reason: rule.reason });
1014
1236
  }
1015
1237
  }
1016
1238
  }
1017
- const compatibleWith = skill.compatibleWith.map((id) => resolve(id, "compatibleWith"));
1239
+ return discourages;
1240
+ }
1241
+ function buildResolvedSkill(skill, matrix, displayNameToId, displayNames, directoryPathToId, aliasTargetToSkillId) {
1242
+ const resolve = (id, context) => resolveToCanonicalId(
1243
+ id,
1244
+ displayNameToId,
1245
+ directoryPathToId,
1246
+ aliasTargetToSkillId,
1247
+ context ? `${skill.id} ${context}` : void 0
1248
+ );
1249
+ const { relationships } = matrix;
1018
1250
  return {
1019
1251
  id: skill.id,
1020
1252
  displayName: displayNames[skill.id],
@@ -1024,12 +1256,22 @@ function buildResolvedSkill(skill, matrix, displayNameToId, displayNames, direct
1024
1256
  categoryExclusive: skill.categoryExclusive,
1025
1257
  tags: skill.tags,
1026
1258
  author: skill.author,
1027
- conflictsWith,
1028
- recommends,
1029
- requires,
1030
- alternatives,
1031
- discourages,
1032
- compatibleWith,
1259
+ conflictsWith: resolveConflicts(
1260
+ skill.id,
1261
+ skill.conflictsWith,
1262
+ relationships.conflicts,
1263
+ resolve
1264
+ ),
1265
+ recommends: resolveRecommends(
1266
+ skill.id,
1267
+ skill.compatibleWith,
1268
+ relationships.recommends,
1269
+ resolve
1270
+ ),
1271
+ requires: resolveRequirements(skill.id, skill.requires, relationships.requires, resolve),
1272
+ alternatives: resolveAlternatives(skill.id, relationships.alternatives, resolve),
1273
+ discourages: resolveDiscourages(skill.id, relationships.discourages, resolve),
1274
+ compatibleWith: skill.compatibleWith.map((id) => resolve(id, "compatibleWith")),
1033
1275
  requiresSetup: skill.requiresSetup.map((id) => resolve(id, "requiresSetup")),
1034
1276
  providesSetupFor: skill.providesSetupFor.map((id) => resolve(id, "providesSetupFor")),
1035
1277
  path: skill.path
@@ -1048,6 +1290,11 @@ function getLabel(skill, fallback) {
1048
1290
  function resolveAlias(aliasOrId, matrix) {
1049
1291
  return matrix.displayNameToId[aliasOrId] || aliasOrId;
1050
1292
  }
1293
+ function initializeSelectionContext(currentSelections, matrix) {
1294
+ const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
1295
+ const selectedSet = new Set(resolvedSelections);
1296
+ return { resolvedSelections, selectedSet };
1297
+ }
1051
1298
  function isDisabled(skillId, currentSelections, matrix, options) {
1052
1299
  if (options?.expertMode) {
1053
1300
  return false;
@@ -1063,19 +1310,19 @@ function isDisabled(skillId, currentSelections, matrix, options) {
1063
1310
  return true;
1064
1311
  }
1065
1312
  const selectedSkill = matrix.skills[selectedFullId];
1066
- if (selectedSkill && selectedSkill.conflictsWith.some((c) => c.skillId === fullId)) {
1313
+ if (selectedSkill?.conflictsWith.some((c) => c.skillId === fullId)) {
1067
1314
  return true;
1068
1315
  }
1069
1316
  }
1070
- const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
1317
+ const { selectedSet } = initializeSelectionContext(currentSelections, matrix);
1071
1318
  for (const requirement of skill.requires) {
1072
1319
  if (requirement.needsAny) {
1073
- const hasAny = requirement.skillIds.some((reqId) => resolvedSelections.includes(reqId));
1320
+ const hasAny = requirement.skillIds.some((reqId) => selectedSet.has(reqId));
1074
1321
  if (!hasAny) {
1075
1322
  return true;
1076
1323
  }
1077
1324
  } else {
1078
- const hasAll = requirement.skillIds.every((reqId) => resolvedSelections.includes(reqId));
1325
+ const hasAll = requirement.skillIds.every((reqId) => selectedSet.has(reqId));
1079
1326
  if (!hasAll) {
1080
1327
  return true;
1081
1328
  }
@@ -1089,7 +1336,7 @@ function getDisableReason(skillId, currentSelections, matrix) {
1089
1336
  if (!skill) {
1090
1337
  return void 0;
1091
1338
  }
1092
- const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
1339
+ const { resolvedSelections, selectedSet } = initializeSelectionContext(currentSelections, matrix);
1093
1340
  for (const selectedId of resolvedSelections) {
1094
1341
  const conflict = skill.conflictsWith.find((c) => c.skillId === selectedId);
1095
1342
  if (conflict) {
@@ -1106,15 +1353,13 @@ function getDisableReason(skillId, currentSelections, matrix) {
1106
1353
  }
1107
1354
  for (const requirement of skill.requires) {
1108
1355
  if (requirement.needsAny) {
1109
- const hasAny = requirement.skillIds.some((reqId) => resolvedSelections.includes(reqId));
1356
+ const hasAny = requirement.skillIds.some((reqId) => selectedSet.has(reqId));
1110
1357
  if (!hasAny) {
1111
1358
  const requiredNames = requirement.skillIds.map((id) => getLabel(matrix.skills[id], id)).join(" or ");
1112
1359
  return `${requirement.reason} (requires ${requiredNames})`;
1113
1360
  }
1114
1361
  } else {
1115
- const missingIds = requirement.skillIds.filter(
1116
- (reqId) => !resolvedSelections.includes(reqId)
1117
- );
1362
+ const missingIds = requirement.skillIds.filter((reqId) => !selectedSet.has(reqId));
1118
1363
  if (missingIds.length > 0) {
1119
1364
  const missingNames = missingIds.map((id) => getLabel(matrix.skills[id], id)).join(", ");
1120
1365
  return `${requirement.reason} (requires ${missingNames})`;
@@ -1129,10 +1374,10 @@ function isDiscouraged(skillId, currentSelections, matrix) {
1129
1374
  if (!skill) {
1130
1375
  return false;
1131
1376
  }
1132
- const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
1377
+ const { resolvedSelections } = initializeSelectionContext(currentSelections, matrix);
1133
1378
  for (const selectedId of resolvedSelections) {
1134
1379
  const selectedSkill = matrix.skills[selectedId];
1135
- if (selectedSkill && selectedSkill.discourages.some((d) => d.skillId === fullId)) {
1380
+ if (selectedSkill?.discourages.some((d) => d.skillId === fullId)) {
1136
1381
  return true;
1137
1382
  }
1138
1383
  if (skill.discourages.some((d) => d.skillId === selectedId)) {
@@ -1147,7 +1392,7 @@ function getDiscourageReason(skillId, currentSelections, matrix) {
1147
1392
  if (!skill) {
1148
1393
  return void 0;
1149
1394
  }
1150
- const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
1395
+ const { resolvedSelections } = initializeSelectionContext(currentSelections, matrix);
1151
1396
  for (const selectedId of resolvedSelections) {
1152
1397
  const selectedSkill = matrix.skills[selectedId];
1153
1398
  if (selectedSkill) {
@@ -1169,10 +1414,10 @@ function isRecommended(skillId, currentSelections, matrix) {
1169
1414
  if (!skill) {
1170
1415
  return false;
1171
1416
  }
1172
- const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
1417
+ const { resolvedSelections } = initializeSelectionContext(currentSelections, matrix);
1173
1418
  for (const selectedId of resolvedSelections) {
1174
1419
  const selectedSkill = matrix.skills[selectedId];
1175
- if (selectedSkill && selectedSkill.recommends.some((r) => r.skillId === fullId)) {
1420
+ if (selectedSkill?.recommends.some((r) => r.skillId === fullId)) {
1176
1421
  return true;
1177
1422
  }
1178
1423
  }
@@ -1184,7 +1429,7 @@ function getRecommendReason(skillId, currentSelections, matrix) {
1184
1429
  if (!skill) {
1185
1430
  return void 0;
1186
1431
  }
1187
- const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
1432
+ const { resolvedSelections } = initializeSelectionContext(currentSelections, matrix);
1188
1433
  for (const selectedId of resolvedSelections) {
1189
1434
  const selectedSkill = matrix.skills[selectedId];
1190
1435
  if (selectedSkill) {
@@ -1196,10 +1441,8 @@ function getRecommendReason(skillId, currentSelections, matrix) {
1196
1441
  }
1197
1442
  return void 0;
1198
1443
  }
1199
- function validateSelection(selections, matrix) {
1444
+ function validateConflicts(resolvedSelections, matrix) {
1200
1445
  const errors = [];
1201
- const warnings = [];
1202
- const resolvedSelections = selections.map((s) => resolveAlias(s, matrix));
1203
1446
  for (let i = 0; i < resolvedSelections.length; i++) {
1204
1447
  const skillA = matrix.skills[resolvedSelections[i]];
1205
1448
  if (!skillA) continue;
@@ -1215,12 +1458,16 @@ function validateSelection(selections, matrix) {
1215
1458
  }
1216
1459
  }
1217
1460
  }
1461
+ return { errors, warnings: [] };
1462
+ }
1463
+ function validateRequirements(resolvedSelections, selectedSet, matrix) {
1464
+ const errors = [];
1218
1465
  for (const skillId of resolvedSelections) {
1219
1466
  const skill = matrix.skills[skillId];
1220
1467
  if (!skill) continue;
1221
1468
  for (const requirement of skill.requires) {
1222
1469
  if (requirement.needsAny) {
1223
- const hasAny = requirement.skillIds.some((reqId) => resolvedSelections.includes(reqId));
1470
+ const hasAny = requirement.skillIds.some((reqId) => selectedSet.has(reqId));
1224
1471
  if (!hasAny) {
1225
1472
  errors.push({
1226
1473
  type: "missing_requirement",
@@ -1229,9 +1476,7 @@ function validateSelection(selections, matrix) {
1229
1476
  });
1230
1477
  }
1231
1478
  } else {
1232
- const missingIds = requirement.skillIds.filter(
1233
- (reqId) => !resolvedSelections.includes(reqId)
1234
- );
1479
+ const missingIds = requirement.skillIds.filter((reqId) => !selectedSet.has(reqId));
1235
1480
  if (missingIds.length > 0) {
1236
1481
  errors.push({
1237
1482
  type: "missing_requirement",
@@ -1242,6 +1487,10 @@ function validateSelection(selections, matrix) {
1242
1487
  }
1243
1488
  }
1244
1489
  }
1490
+ return { errors, warnings: [] };
1491
+ }
1492
+ function validateExclusivity(resolvedSelections, matrix) {
1493
+ const errors = [];
1245
1494
  const validSkills = resolvedSelections.map((skillId) => ({ skillId, skill: matrix.skills[skillId] })).filter((entry) => entry.skill != null);
1246
1495
  const categorySelections = groupBy(validSkills, (entry) => entry.skill.category);
1247
1496
  for (const [categoryId, entries] of typedEntries(categorySelections)) {
@@ -1257,15 +1506,19 @@ function validateSelection(selections, matrix) {
1257
1506
  }
1258
1507
  }
1259
1508
  }
1509
+ return { errors, warnings: [] };
1510
+ }
1511
+ function validateRecommendations(resolvedSelections, selectedSet, matrix) {
1512
+ const warnings = [];
1260
1513
  for (const skillId of resolvedSelections) {
1261
1514
  const skill = matrix.skills[skillId];
1262
1515
  if (!skill) continue;
1263
1516
  for (const recommendation of skill.recommends) {
1264
- if (!resolvedSelections.includes(recommendation.skillId)) {
1517
+ if (!selectedSet.has(recommendation.skillId)) {
1265
1518
  const recommendedSkill = matrix.skills[recommendation.skillId];
1266
1519
  if (recommendedSkill) {
1267
1520
  const hasConflict = recommendedSkill.conflictsWith.some(
1268
- (c) => resolvedSelections.includes(c.skillId)
1521
+ (c) => selectedSet.has(c.skillId)
1269
1522
  );
1270
1523
  if (!hasConflict) {
1271
1524
  warnings.push({
@@ -1278,12 +1531,14 @@ function validateSelection(selections, matrix) {
1278
1531
  }
1279
1532
  }
1280
1533
  }
1534
+ return { errors: [], warnings };
1535
+ }
1536
+ function validateSetupUsage(resolvedSelections, selectedSet, matrix) {
1537
+ const warnings = [];
1281
1538
  for (const skillId of resolvedSelections) {
1282
1539
  const skill = matrix.skills[skillId];
1283
1540
  if (!skill || skill.providesSetupFor.length === 0) continue;
1284
- const hasUsageSkill = skill.providesSetupFor.some(
1285
- (usageId) => resolvedSelections.includes(usageId)
1286
- );
1541
+ const hasUsageSkill = skill.providesSetupFor.some((usageId) => selectedSet.has(usageId));
1287
1542
  if (!hasUsageSkill) {
1288
1543
  warnings.push({
1289
1544
  type: "unused_setup",
@@ -1292,6 +1547,23 @@ function validateSelection(selections, matrix) {
1292
1547
  });
1293
1548
  }
1294
1549
  }
1550
+ return { errors: [], warnings };
1551
+ }
1552
+ function mergeValidationResults(results) {
1553
+ return {
1554
+ errors: results.flatMap((r) => r.errors),
1555
+ warnings: results.flatMap((r) => r.warnings)
1556
+ };
1557
+ }
1558
+ function validateSelection(selections, matrix) {
1559
+ const { resolvedSelections, selectedSet } = initializeSelectionContext(selections, matrix);
1560
+ const { errors, warnings } = mergeValidationResults([
1561
+ validateConflicts(resolvedSelections, matrix),
1562
+ validateRequirements(resolvedSelections, selectedSet, matrix),
1563
+ validateExclusivity(resolvedSelections, matrix),
1564
+ validateRecommendations(resolvedSelections, selectedSet, matrix),
1565
+ validateSetupUsage(resolvedSelections, selectedSet, matrix)
1566
+ ]);
1295
1567
  return {
1296
1568
  valid: errors.length === 0,
1297
1569
  errors,
@@ -1300,7 +1572,7 @@ function validateSelection(selections, matrix) {
1300
1572
  }
1301
1573
  function getAvailableSkills(categoryId, currentSelections, matrix, options) {
1302
1574
  const skillOptions = [];
1303
- const resolvedSelections = currentSelections.map((s) => resolveAlias(s, matrix));
1575
+ const { selectedSet } = initializeSelectionContext(currentSelections, matrix);
1304
1576
  for (const skill of Object.values(matrix.skills)) {
1305
1577
  if (!skill) continue;
1306
1578
  if (skill.category !== categoryId) {
@@ -1319,7 +1591,7 @@ function getAvailableSkills(categoryId, currentSelections, matrix, options) {
1319
1591
  discouragedReason: discouraged ? getDiscourageReason(skill.id, currentSelections, matrix) : void 0,
1320
1592
  recommended,
1321
1593
  recommendedReason: recommended ? getRecommendReason(skill.id, currentSelections, matrix) : void 0,
1322
- selected: resolvedSelections.includes(skill.id),
1594
+ selected: selectedSet.has(skill.id),
1323
1595
  alternatives: skill.alternatives.map((a) => a.skillId)
1324
1596
  });
1325
1597
  }
@@ -1473,8 +1745,8 @@ init_esm_shims();
1473
1745
  // src/cli/lib/plugins/plugin-manifest.ts
1474
1746
  init_esm_shims();
1475
1747
  import path7 from "path";
1476
- var PLUGIN_DIR_NAME = ".claude-plugin";
1477
- var PLUGIN_MANIFEST_FILE2 = "plugin.json";
1748
+ var PLUGIN_DIR_NAME = PLUGIN_MANIFEST_DIR;
1749
+ var PLUGIN_MANIFEST_FILE2 = STANDARD_FILES.PLUGIN_JSON;
1478
1750
  var SKILL_PLUGIN_PREFIX = "";
1479
1751
  var AGENT_PLUGIN_PREFIX = "agent-";
1480
1752
  function buildAuthor(name, email) {
@@ -1567,9 +1839,11 @@ async function findPluginManifest(startDir) {
1567
1839
  // src/cli/lib/plugins/plugin-finder.ts
1568
1840
  init_esm_shims();
1569
1841
  import path9 from "path";
1842
+ import { last, zip } from "remeda";
1843
+ var MAX_SKILL_NAME_LENGTH = 100;
1570
1844
  function getCollectivePluginDir(projectDir) {
1571
1845
  const dir = projectDir ?? process.cwd();
1572
- return path9.join(dir, CLAUDE_DIR, PLUGINS_SUBDIR, "claude-collective");
1846
+ return path9.join(dir, CLAUDE_DIR, PLUGINS_SUBDIR, DEFAULT_PLUGIN_NAME);
1573
1847
  }
1574
1848
  function getProjectPluginsDir(projectDir) {
1575
1849
  const dir = projectDir ?? process.cwd();
@@ -1591,7 +1865,7 @@ async function readPluginManifest(pluginDir) {
1591
1865
  return null;
1592
1866
  }
1593
1867
  try {
1594
- const content = await readFile(manifestPath);
1868
+ const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
1595
1869
  const manifest = pluginManifestSchema.parse(JSON.parse(content));
1596
1870
  if (!manifest.name || typeof manifest.name !== "string") {
1597
1871
  verbose(` Invalid manifest at ${manifestPath}: missing name`);
@@ -1607,29 +1881,40 @@ async function getPluginSkillIds(pluginSkillsDir, matrix) {
1607
1881
  const skillFiles = await glob("**/SKILL.md", pluginSkillsDir);
1608
1882
  const skillIds = [];
1609
1883
  const aliasToId = /* @__PURE__ */ new Map();
1610
- for (const [id, skill] of Object.entries(matrix.skills)) {
1884
+ for (const [id, skill] of typedEntries(matrix.skills)) {
1611
1885
  if (!skill) continue;
1612
1886
  if (skill.displayName) {
1613
1887
  aliasToId.set(skill.displayName.toLowerCase(), id);
1614
1888
  }
1615
1889
  }
1616
1890
  const dirToId = /* @__PURE__ */ new Map();
1617
- for (const [id] of Object.entries(matrix.skills)) {
1891
+ for (const [id] of typedEntries(matrix.skills)) {
1618
1892
  const idParts = id.split("/");
1619
- const lastPart = idParts[idParts.length - 1];
1893
+ const lastPart = last(idParts);
1620
1894
  if (lastPart) {
1621
1895
  dirToId.set(lastPart.toLowerCase(), id);
1622
1896
  }
1623
1897
  }
1624
- for (const skillFile of skillFiles) {
1625
- const fullPath = path9.join(pluginSkillsDir, skillFile);
1626
- const content = await readFile(fullPath);
1898
+ const fileContents = await Promise.all(
1899
+ skillFiles.map((skillFile) => readFile(path9.join(pluginSkillsDir, skillFile)))
1900
+ );
1901
+ for (const [skillFile, content] of zip(skillFiles, fileContents)) {
1627
1902
  const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
1628
1903
  if (frontmatterMatch) {
1629
1904
  const frontmatter = frontmatterMatch[1];
1630
1905
  const nameMatch = frontmatter.match(/^name:\s*["']?(.+?)["']?\s*$/m);
1631
1906
  if (nameMatch) {
1632
1907
  const skillName = nameMatch[1].trim();
1908
+ if (skillName.length === 0) {
1909
+ warn(`Skipping plugin skill '${skillFile}': empty name in frontmatter`);
1910
+ continue;
1911
+ }
1912
+ if (skillName.length > MAX_SKILL_NAME_LENGTH) {
1913
+ warn(
1914
+ `Skipping plugin skill '${skillFile}': name exceeds ${MAX_SKILL_NAME_LENGTH} characters`
1915
+ );
1916
+ continue;
1917
+ }
1633
1918
  if (matrix.skills[skillName]) {
1634
1919
  skillIds.push(skillName);
1635
1920
  continue;
@@ -1663,8 +1948,8 @@ init_esm_shims();
1663
1948
  init_esm_shims();
1664
1949
  import path10 from "path";
1665
1950
  async function detectInstallation(projectDir = process.cwd()) {
1666
- const srcConfigPath = path10.join(projectDir, CLAUDE_SRC_DIR, "config.yaml");
1667
- const legacyConfigPath = path10.join(projectDir, CLAUDE_DIR, "config.yaml");
1951
+ const srcConfigPath = path10.join(projectDir, CLAUDE_SRC_DIR, STANDARD_FILES.CONFIG_YAML);
1952
+ const legacyConfigPath = path10.join(projectDir, CLAUDE_DIR, STANDARD_FILES.CONFIG_YAML);
1668
1953
  const localConfigPath = await fileExists(srcConfigPath) ? srcConfigPath : await fileExists(legacyConfigPath) ? legacyConfigPath : null;
1669
1954
  if (localConfigPath) {
1670
1955
  const loaded = await loadProjectConfig(projectDir);
@@ -1680,7 +1965,7 @@ async function detectInstallation(projectDir = process.cwd()) {
1680
1965
  }
1681
1966
  }
1682
1967
  const pluginDir = getCollectivePluginDir(projectDir);
1683
- const pluginConfigPath = path10.join(pluginDir, "config.yaml");
1968
+ const pluginConfigPath = path10.join(pluginDir, STANDARD_FILES.CONFIG_YAML);
1684
1969
  if (await directoryExists(pluginDir)) {
1685
1970
  return {
1686
1971
  mode: "plugin",
@@ -1696,41 +1981,59 @@ async function detectInstallation(projectDir = process.cwd()) {
1696
1981
  // src/cli/lib/installation/local-installer.ts
1697
1982
  init_esm_shims();
1698
1983
  import path15 from "path";
1699
- import { stringify as stringifyYaml4 } from "yaml";
1984
+ import { stringify as stringifyYaml3 } from "yaml";
1700
1985
 
1701
1986
  // src/cli/lib/stacks/index.ts
1702
1987
  init_esm_shims();
1703
1988
 
1704
1989
  // src/cli/lib/stacks/stacks-loader.ts
1705
1990
  init_esm_shims();
1706
- import { parse as parseYaml6 } from "yaml";
1991
+ import { parse as parseYaml5 } from "yaml";
1707
1992
  import path11 from "path";
1708
- import { mapValues } from "remeda";
1993
+ import { mapValues, pipe, flatMap, unique as unique2 } from "remeda";
1709
1994
  var STACKS_FILE = "config/stacks.yaml";
1710
1995
  var stacksCache = /* @__PURE__ */ new Map();
1711
- async function loadStacks(configDir) {
1712
- const cacheKey = configDir;
1996
+ function normalizeAgentConfig(agentConfig) {
1997
+ return mapValues(agentConfig, (value) => {
1998
+ const items = Array.isArray(value) ? value : [value];
1999
+ return items.map(
2000
+ (item) => typeof item === "string" ? { id: item, preloaded: false } : item
2001
+ );
2002
+ });
2003
+ }
2004
+ function normalizeStackRecord(rawStack) {
2005
+ return mapValues(rawStack, (agentConfig) => normalizeAgentConfig(agentConfig));
2006
+ }
2007
+ async function loadStacks(configDir, stacksFile) {
2008
+ const resolvedStacksFile = stacksFile ?? STACKS_FILE;
2009
+ const cacheKey = `${configDir}:${resolvedStacksFile}`;
1713
2010
  const cached = stacksCache.get(cacheKey);
1714
2011
  if (cached) return cached;
1715
- const stacksPath = path11.join(configDir, STACKS_FILE);
2012
+ const stacksPath = path11.join(configDir, resolvedStacksFile);
1716
2013
  if (!await fileExists(stacksPath)) {
1717
2014
  verbose(`No stacks file found at ${stacksPath}`);
1718
2015
  return [];
1719
2016
  }
1720
2017
  try {
1721
2018
  const content = await readFile(stacksPath);
1722
- const result = stacksConfigSchema.safeParse(parseYaml6(content));
2019
+ const result = stacksConfigSchema.safeParse(parseYaml5(content));
1723
2020
  if (!result.success) {
1724
2021
  throw new Error(
1725
- `Invalid stacks.yaml at ${stacksPath}: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
2022
+ `Invalid stacks.yaml at '${stacksPath}': ${formatZodErrors(result.error.issues)}`
1726
2023
  );
1727
2024
  }
1728
- const config = result.data;
1729
- stacksCache.set(cacheKey, config.stacks);
1730
- verbose(`Loaded ${config.stacks.length} stacks from ${stacksPath}`);
1731
- return config.stacks;
2025
+ const stacks = result.data.stacks.map((stack) => ({
2026
+ ...stack,
2027
+ agents: mapValues(
2028
+ stack.agents,
2029
+ (agentConfig) => normalizeAgentConfig(agentConfig)
2030
+ )
2031
+ }));
2032
+ stacksCache.set(cacheKey, stacks);
2033
+ verbose(`Loaded ${stacks.length} stacks from ${stacksPath}`);
2034
+ return stacks;
1732
2035
  } catch (error) {
1733
- const errorMessage = error instanceof Error ? error.message : String(error);
2036
+ const errorMessage = getErrorMessage(error);
1734
2037
  throw new Error(`Failed to load stacks from '${stacksPath}': ${errorMessage}`);
1735
2038
  }
1736
2039
  }
@@ -1744,25 +2047,36 @@ async function loadStackById(stackId, configDir) {
1744
2047
  verbose(`Found stack: ${stack.name} (${stackId})`);
1745
2048
  return stack;
1746
2049
  }
1747
- function resolveAgentConfigToSkills(agentConfig, displayNameToId) {
2050
+ function resolveAgentConfigToSkills(agentConfig) {
1748
2051
  const skillRefs = [];
1749
- for (const [subcategory, technologyDisplayName] of Object.entries(agentConfig)) {
1750
- const fullSkillId = displayNameToId[technologyDisplayName];
1751
- if (!fullSkillId) {
1752
- warn(
1753
- `No skill found for display name '${technologyDisplayName}' (subcategory: ${subcategory}) in stack config. Skipping.`
1754
- );
1755
- continue;
2052
+ for (const [subcategory, assignments] of typedEntries(
2053
+ agentConfig
2054
+ )) {
2055
+ if (!assignments) continue;
2056
+ for (const assignment of assignments) {
2057
+ if (!SKILL_ID_PATTERN.test(assignment.id)) {
2058
+ warn(
2059
+ `Invalid skill ID '${assignment.id}' for subcategory '${subcategory}' in stack config. Skipping.`
2060
+ );
2061
+ continue;
2062
+ }
2063
+ skillRefs.push({
2064
+ id: assignment.id,
2065
+ usage: `when working with ${subcategory}`,
2066
+ preloaded: assignment.preloaded ?? false
2067
+ });
1756
2068
  }
1757
- const isKeySkill = KEY_SUBCATEGORIES.has(subcategory);
1758
- skillRefs.push({
1759
- id: fullSkillId,
1760
- usage: `when working with ${subcategory}`,
1761
- preloaded: isKeySkill
1762
- });
1763
2069
  }
1764
2070
  return skillRefs;
1765
2071
  }
2072
+ function getStackSkillIds(stack) {
2073
+ return pipe(
2074
+ Object.values(stack),
2075
+ flatMap(resolveAgentConfigToSkills),
2076
+ (refs) => refs.map((r) => r.id),
2077
+ unique2()
2078
+ );
2079
+ }
1766
2080
 
1767
2081
  // src/cli/lib/stacks/stack-installer.ts
1768
2082
  init_esm_shims();
@@ -1777,7 +2091,7 @@ import path13 from "path";
1777
2091
  init_esm_shims();
1778
2092
  import { Liquid } from "liquidjs";
1779
2093
  import path12 from "path";
1780
- import { pipe, flatMap, filter, uniqueBy } from "remeda";
2094
+ import { pipe as pipe2, flatMap as flatMap2, filter, uniqueBy } from "remeda";
1781
2095
 
1782
2096
  // src/cli/lib/resolver.ts
1783
2097
  init_esm_shims();
@@ -1797,17 +2111,9 @@ function resolveSkillReferences(skillRefs, skills) {
1797
2111
  return skillRefs.map((ref) => resolveSkillReference(ref, skills)).filter((skill) => skill !== null);
1798
2112
  }
1799
2113
  function buildSkillRefsFromConfig(agentStack) {
1800
- const skillRefs = [];
1801
- for (const [subcategory, skillId] of typedEntries(agentStack)) {
1802
- skillRefs.push({
1803
- id: skillId,
1804
- usage: `when working with ${subcategory}`,
1805
- preloaded: KEY_SUBCATEGORIES.has(subcategory)
1806
- });
1807
- }
1808
- return skillRefs;
2114
+ return resolveAgentConfigToSkills(agentStack);
1809
2115
  }
1810
- function resolveAgentSkillsFromStack(agentName, stack, displayNameToId) {
2116
+ function resolveAgentSkillsFromStack(agentName, stack) {
1811
2117
  const agentConfig = stack.agents[agentName];
1812
2118
  if (!agentConfig) {
1813
2119
  verbose(`Agent '${agentName}' not found in stack '${stack.id}'`);
@@ -1817,33 +2123,16 @@ function resolveAgentSkillsFromStack(agentName, stack, displayNameToId) {
1817
2123
  verbose(`Agent '${agentName}' has no technology config in stack '${stack.id}'`);
1818
2124
  return [];
1819
2125
  }
1820
- const skillRefs = [];
1821
- for (const [subcategory, technologyDisplayName] of typedEntries(
1822
- agentConfig
1823
- )) {
1824
- const fullSkillId = displayNameToId[technologyDisplayName];
1825
- if (!fullSkillId) {
1826
- verbose(
1827
- `Warning: No skill found for display name '${technologyDisplayName}' (agent: ${agentName}, subcategory: ${subcategory}). Skipping.`
1828
- );
1829
- continue;
1830
- }
1831
- const isKeySkill = KEY_SUBCATEGORIES.has(subcategory);
1832
- skillRefs.push({
1833
- id: fullSkillId,
1834
- usage: `when working with ${subcategory}`,
1835
- preloaded: isKeySkill
1836
- });
1837
- }
2126
+ const skillRefs = resolveAgentConfigToSkills(agentConfig);
1838
2127
  verbose(`Resolved ${skillRefs.length} skills for agent '${agentName}' from stack '${stack.id}'`);
1839
2128
  return skillRefs;
1840
2129
  }
1841
- async function getAgentSkills(agentName, agentConfig, stack, displayNameToId) {
2130
+ async function resolveAgentSkillRefs(agentName, agentConfig, stack) {
1842
2131
  if (agentConfig.skills && agentConfig.skills.length > 0) {
1843
2132
  return agentConfig.skills;
1844
2133
  }
1845
- if (stack && displayNameToId) {
1846
- const stackSkills = resolveAgentSkillsFromStack(agentName, stack, displayNameToId);
2134
+ if (stack) {
2135
+ const stackSkills = resolveAgentSkillsFromStack(agentName, stack);
1847
2136
  if (stackSkills.length > 0) {
1848
2137
  verbose(`Resolved ${stackSkills.length} skills from stack for ${agentName}`);
1849
2138
  return stackSkills;
@@ -1851,7 +2140,7 @@ async function getAgentSkills(agentName, agentConfig, stack, displayNameToId) {
1851
2140
  }
1852
2141
  return [];
1853
2142
  }
1854
- async function resolveAgents(agents, skills, compileConfig, _projectRoot, stack, displayNameToId) {
2143
+ async function resolveAgents(agents, skills, compileConfig, _projectRoot, stack) {
1855
2144
  const resolved = {};
1856
2145
  const agentNames = typedKeys(compileConfig.agents);
1857
2146
  for (const agentName of agentNames) {
@@ -1864,7 +2153,7 @@ async function resolveAgents(agents, skills, compileConfig, _projectRoot, stack,
1864
2153
  );
1865
2154
  }
1866
2155
  const agentConfig = compileConfig.agents[agentName];
1867
- const skillRefs = await getAgentSkills(agentName, agentConfig, stack, displayNameToId);
2156
+ const skillRefs = await resolveAgentSkillRefs(agentName, agentConfig, stack);
1868
2157
  const resolvedSkills = resolveSkillReferences(skillRefs, skills);
1869
2158
  resolved[agentName] = {
1870
2159
  name: agentName,
@@ -1880,7 +2169,7 @@ async function resolveAgents(agents, skills, compileConfig, _projectRoot, stack,
1880
2169
  }
1881
2170
  return resolved;
1882
2171
  }
1883
- function stackToCompileConfig(stackId, stack) {
2172
+ function convertStackToCompileConfig(stackId, stack) {
1884
2173
  const agents = {};
1885
2174
  for (const agentId of stack.agents) {
1886
2175
  agents[agentId] = {};
@@ -1895,7 +2184,7 @@ function stackToCompileConfig(stackId, stack) {
1895
2184
 
1896
2185
  // src/cli/utils/frontmatter.ts
1897
2186
  init_esm_shims();
1898
- import { parse as parseYaml7 } from "yaml";
2187
+ import { parse as parseYaml6 } from "yaml";
1899
2188
  function extractFrontmatter(content) {
1900
2189
  const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---/;
1901
2190
  const match = content.match(frontmatterRegex);
@@ -1903,13 +2192,75 @@ function extractFrontmatter(content) {
1903
2192
  return null;
1904
2193
  }
1905
2194
  try {
1906
- return parseYaml7(match[1]);
2195
+ return parseYaml6(match[1]);
1907
2196
  } catch {
1908
2197
  return null;
1909
2198
  }
1910
2199
  }
1911
2200
 
1912
2201
  // src/cli/lib/compiler.ts
2202
+ var LIQUID_SYNTAX_PATTERN = /\{\{|\}\}|\{%|%\}/g;
2203
+ function sanitizeLiquidSyntax(value, fieldName) {
2204
+ if (!LIQUID_SYNTAX_PATTERN.test(value)) return value;
2205
+ LIQUID_SYNTAX_PATTERN.lastIndex = 0;
2206
+ const sanitized = value.replace(LIQUID_SYNTAX_PATTERN, "");
2207
+ warn(`Stripped Liquid template syntax from '${fieldName}' \u2014 possible template injection attempt`);
2208
+ return sanitized;
2209
+ }
2210
+ function sanitizeString(value, fieldName) {
2211
+ if (value === void 0) return void 0;
2212
+ return sanitizeLiquidSyntax(value, fieldName);
2213
+ }
2214
+ function sanitizeStringArray(values, fieldName) {
2215
+ if (!values) return values;
2216
+ return values.map((v) => sanitizeLiquidSyntax(v, fieldName));
2217
+ }
2218
+ function sanitizeSkills(skills) {
2219
+ return skills.map((s) => ({
2220
+ ...s,
2221
+ id: sanitizeLiquidSyntax(s.id, "skill.id"),
2222
+ description: sanitizeLiquidSyntax(s.description, "skill.description"),
2223
+ usage: sanitizeLiquidSyntax(s.usage, "skill.usage"),
2224
+ pluginRef: sanitizeString(s.pluginRef, "skill.pluginRef")
2225
+ }));
2226
+ }
2227
+ function sanitizeCompiledAgentData(data) {
2228
+ const sanitizedAgent = {
2229
+ ...data.agent,
2230
+ name: sanitizeLiquidSyntax(data.agent.name, "agent.name"),
2231
+ title: sanitizeLiquidSyntax(data.agent.title, "agent.title"),
2232
+ description: sanitizeLiquidSyntax(data.agent.description, "agent.description"),
2233
+ tools: sanitizeStringArray(data.agent.tools, "agent.tools") ?? data.agent.tools,
2234
+ disallowed_tools: sanitizeStringArray(data.agent.disallowed_tools, "agent.disallowed_tools"),
2235
+ model: sanitizeString(data.agent.model, "agent.model"),
2236
+ permission_mode: sanitizeString(
2237
+ data.agent.permission_mode,
2238
+ "agent.permission_mode"
2239
+ )
2240
+ };
2241
+ const sanitizedSkills = sanitizeSkills(data.skills);
2242
+ const sanitizedPreloaded = sanitizeSkills(data.preloadedSkills);
2243
+ const sanitizedDynamic = sanitizeSkills(data.dynamicSkills);
2244
+ const sanitizedPreloadedIds = data.preloadedSkillIds.map(
2245
+ (id) => sanitizeLiquidSyntax(String(id), "preloadedSkillId")
2246
+ );
2247
+ return {
2248
+ agent: sanitizedAgent,
2249
+ intro: sanitizeLiquidSyntax(data.intro, "intro"),
2250
+ workflow: sanitizeLiquidSyntax(data.workflow, "workflow"),
2251
+ examples: sanitizeLiquidSyntax(data.examples, "examples"),
2252
+ criticalRequirementsTop: sanitizeLiquidSyntax(
2253
+ data.criticalRequirementsTop,
2254
+ "criticalRequirementsTop"
2255
+ ),
2256
+ criticalReminders: sanitizeLiquidSyntax(data.criticalReminders, "criticalReminders"),
2257
+ outputFormat: sanitizeLiquidSyntax(data.outputFormat, "outputFormat"),
2258
+ skills: sanitizedSkills,
2259
+ preloadedSkills: sanitizedPreloaded,
2260
+ dynamicSkills: sanitizedDynamic,
2261
+ preloadedSkillIds: sanitizedPreloadedIds
2262
+ };
2263
+ }
1913
2264
  async function createLiquidEngine(projectDir) {
1914
2265
  const roots = [];
1915
2266
  if (projectDir) {
@@ -1934,42 +2285,48 @@ async function createLiquidEngine(projectDir) {
1934
2285
  }
1935
2286
 
1936
2287
  // src/cli/lib/stacks/stack-plugin-compiler.ts
1937
- import { unique as unique2 } from "remeda";
2288
+ import { unique as unique3 } from "remeda";
1938
2289
  function hashStackConfig(stack) {
1939
- const stackSkillIds = stack.stack ? [...new Set(Object.values(stack.stack).flatMap((a) => Object.values(a)))].sort() : [];
2290
+ const stackSkillIds = stack.stack ? getStackSkillIds(stack.stack).sort() : [];
1940
2291
  const parts = [
1941
2292
  `name:${stack.name}`,
1942
2293
  `description:${stack.description ?? ""}`,
1943
2294
  `skills:${stackSkillIds.join(",")}`,
1944
2295
  `agents:${(stack.agents || []).sort().join(",")}`
1945
2296
  ];
1946
- return hashString(parts.join("\n"));
2297
+ return computeStringHash(parts.join("\n"));
1947
2298
  }
1948
2299
  async function compileAgentForPlugin(name, agent, fallbackRoot, engine, installMode) {
1949
2300
  verbose(`Compiling agent: ${name}`);
1950
2301
  const agentSourceRoot = agent.sourceRoot || fallbackRoot;
1951
2302
  const agentBaseDir = agent.agentBaseDir || DIRS.agents;
1952
2303
  const agentDir = path13.join(agentSourceRoot, agentBaseDir, agent.path || name);
1953
- const intro = await readFile(path13.join(agentDir, "intro.md"));
1954
- const workflow = await readFile(path13.join(agentDir, "workflow.md"));
2304
+ const intro = await readFile(path13.join(agentDir, STANDARD_FILES.INTRO_MD));
2305
+ const workflow = await readFile(path13.join(agentDir, STANDARD_FILES.WORKFLOW_MD));
1955
2306
  const examples = await readFileOptional(
1956
- path13.join(agentDir, "examples.md"),
2307
+ path13.join(agentDir, STANDARD_FILES.EXAMPLES_MD),
1957
2308
  "## Examples\n\n_No examples defined._"
1958
2309
  );
1959
2310
  const criticalRequirementsTop = await readFileOptional(
1960
- path13.join(agentDir, "critical-requirements.md"),
2311
+ path13.join(agentDir, STANDARD_FILES.CRITICAL_REQUIREMENTS_MD),
1961
2312
  ""
1962
2313
  );
1963
2314
  const criticalReminders = await readFileOptional(
1964
- path13.join(agentDir, "critical-reminders.md"),
2315
+ path13.join(agentDir, STANDARD_FILES.CRITICAL_REMINDERS_MD),
1965
2316
  ""
1966
2317
  );
1967
2318
  const agentPath = agent.path || name;
1968
2319
  const category = agentPath.split("/")[0];
1969
2320
  const categoryDir = path13.join(agentSourceRoot, agentBaseDir, category);
1970
- let outputFormat = await readFileOptional(path13.join(agentDir, "output-format.md"), "");
2321
+ let outputFormat = await readFileOptional(
2322
+ path13.join(agentDir, STANDARD_FILES.OUTPUT_FORMAT_MD),
2323
+ ""
2324
+ );
1971
2325
  if (!outputFormat) {
1972
- outputFormat = await readFileOptional(path13.join(categoryDir, "output-format.md"), "");
2326
+ outputFormat = await readFileOptional(
2327
+ path13.join(categoryDir, STANDARD_FILES.OUTPUT_FORMAT_MD),
2328
+ ""
2329
+ );
1973
2330
  }
1974
2331
  const skills = installMode === "plugin" ? agent.skills.map((s) => ({ ...s, pluginRef: `${s.id}:${s.id}` })) : agent.skills;
1975
2332
  const preloadedSkills = skills.filter((s) => s.preloaded);
@@ -1991,7 +2348,7 @@ async function compileAgentForPlugin(name, agent, fallbackRoot, engine, installM
1991
2348
  dynamicSkills,
1992
2349
  preloadedSkillIds
1993
2350
  };
1994
- return engine.renderFile("agent", data);
2351
+ return engine.renderFile("agent", sanitizeCompiledAgentData(data));
1995
2352
  }
1996
2353
  function generateStackReadme(stackId, stack, agents, skillPlugins) {
1997
2354
  const lines = [];
@@ -2004,9 +2361,9 @@ function generateStackReadme(stackId, stack, agents, skillPlugins) {
2004
2361
  lines.push("Add this plugin to your Claude Code configuration:");
2005
2362
  lines.push("");
2006
2363
  lines.push("```json");
2007
- lines.push(`{`);
2364
+ lines.push("{");
2008
2365
  lines.push(` "plugins": ["${stackId}"]`);
2009
- lines.push(`}`);
2366
+ lines.push("}");
2010
2367
  lines.push("```");
2011
2368
  lines.push("");
2012
2369
  lines.push("## Agents");
@@ -2022,7 +2379,7 @@ function generateStackReadme(stackId, stack, agents, skillPlugins) {
2022
2379
  lines.push("");
2023
2380
  lines.push("This stack includes the following skills:");
2024
2381
  lines.push("");
2025
- const uniqueSkills = unique2(skillPlugins).sort();
2382
+ const uniqueSkills = unique3(skillPlugins).sort();
2026
2383
  for (const skill of uniqueSkills) {
2027
2384
  lines.push(`- \`${skill}\``);
2028
2385
  }
@@ -2049,19 +2406,8 @@ async function compileStackPlugin(options) {
2049
2406
  );
2050
2407
  let newStack = options.stack || await loadStackById(stackId, projectRoot);
2051
2408
  if (!newStack) {
2052
- newStack = await loadStackById(stackId, PROJECT_ROOT);
2053
- }
2054
- const sourceMatrixPath = path13.join(projectRoot, SKILLS_MATRIX_PATH);
2055
- const cliMatrixPath = path13.join(PROJECT_ROOT, SKILLS_MATRIX_PATH);
2056
- let matrix;
2057
- try {
2058
- matrix = await loadSkillsMatrix(
2059
- await fileExists(sourceMatrixPath) ? sourceMatrixPath : cliMatrixPath
2060
- );
2061
- } catch {
2062
- matrix = await loadSkillsMatrix(cliMatrixPath);
2409
+ newStack = await loadStackById(stackId, PROJECT_ROOT);
2063
2410
  }
2064
- const skillAliases = matrix.skill_aliases || {};
2065
2411
  let stack;
2066
2412
  if (newStack) {
2067
2413
  verbose(` Found stack: ${newStack.name}`);
@@ -2069,7 +2415,7 @@ async function compileStackPlugin(options) {
2069
2415
  for (const agentName of typedKeys(newStack.agents)) {
2070
2416
  const agentConfig = newStack.agents[agentName];
2071
2417
  if (!agentConfig) continue;
2072
- const skillRefs = resolveAgentConfigToSkills(agentConfig, skillAliases);
2418
+ const skillRefs = resolveAgentConfigToSkills(agentConfig);
2073
2419
  for (const ref of skillRefs) {
2074
2420
  agentSkillIds.add(ref.id);
2075
2421
  }
@@ -2079,25 +2425,18 @@ async function compileStackPlugin(options) {
2079
2425
  description: newStack.description,
2080
2426
  agents: typedKeys(newStack.agents),
2081
2427
  skills: [...agentSkillIds],
2082
- stack: buildStackProperty(newStack, skillAliases)
2428
+ stack: buildStackProperty(newStack)
2083
2429
  };
2084
2430
  } else {
2085
2431
  throw new Error(`Stack '${stackId}' not found in config/stacks.yaml`);
2086
2432
  }
2087
- const stackSkillIds = stack.stack ? [...new Set(Object.values(stack.stack).flatMap((a) => Object.values(a)))] : [];
2433
+ const stackSkillIds = stack.stack ? getStackSkillIds(stack.stack) : [];
2088
2434
  const skills = await loadSkillsByIds(
2089
2435
  stackSkillIds.map((id) => ({ id })),
2090
2436
  projectRoot
2091
2437
  );
2092
- const compileConfig = stackToCompileConfig(stackId, stack);
2093
- const resolvedAgents = await resolveAgents(
2094
- agents,
2095
- skills,
2096
- compileConfig,
2097
- projectRoot,
2098
- newStack,
2099
- skillAliases
2100
- );
2438
+ const compileConfig = convertStackToCompileConfig(stackId, stack);
2439
+ const resolvedAgents = await resolveAgents(agents, skills, compileConfig, projectRoot, newStack);
2101
2440
  const pluginDir = path13.join(outputDir, stackId);
2102
2441
  const agentsDir = path13.join(pluginDir, "agents");
2103
2442
  await ensureDir(pluginDir);
@@ -2132,11 +2471,11 @@ async function compileStackPlugin(options) {
2132
2471
  verbose(` Compiled agent: ${name}`);
2133
2472
  }
2134
2473
  const stackDir = path13.join(projectRoot, DIRS.stacks, stackId);
2135
- const claudeMdPath = path13.join(stackDir, "CLAUDE.md");
2474
+ const claudeMdPath = path13.join(stackDir, STANDARD_FILES.CLAUDE_MD);
2136
2475
  if (await fileExists(claudeMdPath)) {
2137
2476
  const claudeContent = await readFile(claudeMdPath);
2138
- await writeFile(path13.join(pluginDir, "CLAUDE.md"), claudeContent);
2139
- verbose(` Copied CLAUDE.md`);
2477
+ await writeFile(path13.join(pluginDir, STANDARD_FILES.CLAUDE_MD), claudeContent);
2478
+ verbose(` Copied ${STANDARD_FILES.CLAUDE_MD}`);
2140
2479
  }
2141
2480
  const newHash = hashStackConfig(stack);
2142
2481
  const { version, contentHash } = await determinePluginVersion(
@@ -2144,7 +2483,7 @@ async function compileStackPlugin(options) {
2144
2483
  pluginDir,
2145
2484
  getPluginManifestPath
2146
2485
  );
2147
- const uniqueSkillPlugins = unique2(allSkillPlugins);
2486
+ const uniqueSkillPlugins = unique3(allSkillPlugins);
2148
2487
  const manifest = generateStackPluginManifest({
2149
2488
  stackName: stackId,
2150
2489
  description: stack.description,
@@ -2160,7 +2499,7 @@ async function compileStackPlugin(options) {
2160
2499
  verbose(` Wrote plugin.json (v${version})`);
2161
2500
  const readme = generateStackReadme(stackId, stack, compiledAgentNames, uniqueSkillPlugins);
2162
2501
  await writeFile(path13.join(pluginDir, "README.md"), readme);
2163
- verbose(` Generated README.md`);
2502
+ verbose(" Generated README.md");
2164
2503
  return {
2165
2504
  pluginPath: pluginDir,
2166
2505
  manifest,
@@ -2171,27 +2510,111 @@ async function compileStackPlugin(options) {
2171
2510
  };
2172
2511
  }
2173
2512
  function printStackCompilationSummary(result) {
2174
- console.log(`
2513
+ log(`
2175
2514
  Stack plugin compiled: ${result.stackName}`);
2176
- console.log(` Path: ${result.pluginPath}`);
2177
- console.log(` Agents: ${result.agents.length}`);
2515
+ log(` Path: ${result.pluginPath}`);
2516
+ log(` Agents: ${result.agents.length}`);
2178
2517
  for (const agent of result.agents) {
2179
- console.log(` - ${agent}`);
2518
+ log(` - ${agent}`);
2180
2519
  }
2181
2520
  if (result.skillPlugins.length > 0) {
2182
- console.log(` Skills included: ${result.skillPlugins.length}`);
2521
+ log(` Skills included: ${result.skillPlugins.length}`);
2183
2522
  for (const skill of result.skillPlugins) {
2184
- console.log(` - ${skill}`);
2523
+ log(` - ${skill}`);
2185
2524
  }
2186
2525
  }
2187
2526
  if (result.hasHooks) {
2188
- console.log(` Hooks: enabled`);
2527
+ log(" Hooks: enabled");
2189
2528
  }
2190
2529
  }
2191
2530
 
2192
2531
  // src/cli/utils/exec.ts
2193
2532
  init_esm_shims();
2194
2533
  import { spawn } from "child_process";
2534
+ var MAX_PLUGIN_PATH_LENGTH = 1024;
2535
+ var MAX_GITHUB_REPO_LENGTH = 256;
2536
+ var MAX_MARKETPLACE_NAME_LENGTH = 128;
2537
+ var MAX_PLUGIN_NAME_LENGTH = 256;
2538
+ var GITHUB_REPO_PATTERN = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/;
2539
+ var SAFE_NAME_PATTERN = /^[a-zA-Z0-9._@/-]+$/;
2540
+ var SAFE_PLUGIN_PATH_PATTERN = /^[a-zA-Z0-9._@/:~-]+$/;
2541
+ var CONTROL_CHAR_PATTERN = /[\x00-\x08\x0E-\x1F\x7F]/u;
2542
+ function validatePluginPath(pluginPath) {
2543
+ if (!pluginPath || pluginPath.trim().length === 0) {
2544
+ throw new Error("Plugin path must not be empty.");
2545
+ }
2546
+ if (pluginPath.length > MAX_PLUGIN_PATH_LENGTH) {
2547
+ throw new Error(
2548
+ `Plugin path is too long (${pluginPath.length} characters, max ${MAX_PLUGIN_PATH_LENGTH}).`
2549
+ );
2550
+ }
2551
+ if (CONTROL_CHAR_PATTERN.test(pluginPath)) {
2552
+ throw new Error("Plugin path contains invalid control characters.");
2553
+ }
2554
+ if (!SAFE_PLUGIN_PATH_PATTERN.test(pluginPath)) {
2555
+ throw new Error(
2556
+ `Plugin path contains invalid characters: "${pluginPath}"
2557
+ Plugin paths may only contain alphanumeric characters, dashes, underscores, dots, slashes, @, and colons.`
2558
+ );
2559
+ }
2560
+ }
2561
+ function validateGithubRepo(githubRepo) {
2562
+ if (!githubRepo || githubRepo.trim().length === 0) {
2563
+ throw new Error("GitHub repository must not be empty.");
2564
+ }
2565
+ if (githubRepo.length > MAX_GITHUB_REPO_LENGTH) {
2566
+ throw new Error(
2567
+ `GitHub repository is too long (${githubRepo.length} characters, max ${MAX_GITHUB_REPO_LENGTH}).`
2568
+ );
2569
+ }
2570
+ if (CONTROL_CHAR_PATTERN.test(githubRepo)) {
2571
+ throw new Error("GitHub repository contains invalid control characters.");
2572
+ }
2573
+ if (!GITHUB_REPO_PATTERN.test(githubRepo)) {
2574
+ throw new Error(
2575
+ `Invalid GitHub repository format: "${githubRepo}"
2576
+ Expected format: owner/repo (e.g., 'my-org/my-skills').`
2577
+ );
2578
+ }
2579
+ }
2580
+ function validateMarketplaceName(name) {
2581
+ if (!name || name.trim().length === 0) {
2582
+ throw new Error("Marketplace name must not be empty.");
2583
+ }
2584
+ if (name.length > MAX_MARKETPLACE_NAME_LENGTH) {
2585
+ throw new Error(
2586
+ `Marketplace name is too long (${name.length} characters, max ${MAX_MARKETPLACE_NAME_LENGTH}).`
2587
+ );
2588
+ }
2589
+ if (CONTROL_CHAR_PATTERN.test(name)) {
2590
+ throw new Error("Marketplace name contains invalid control characters.");
2591
+ }
2592
+ if (!SAFE_NAME_PATTERN.test(name)) {
2593
+ throw new Error(
2594
+ `Marketplace name contains invalid characters: "${name}"
2595
+ Names may only contain alphanumeric characters, dashes, underscores, dots, @, and slashes.`
2596
+ );
2597
+ }
2598
+ }
2599
+ function validatePluginName(pluginName) {
2600
+ if (!pluginName || pluginName.trim().length === 0) {
2601
+ throw new Error("Plugin name must not be empty.");
2602
+ }
2603
+ if (pluginName.length > MAX_PLUGIN_NAME_LENGTH) {
2604
+ throw new Error(
2605
+ `Plugin name is too long (${pluginName.length} characters, max ${MAX_PLUGIN_NAME_LENGTH}).`
2606
+ );
2607
+ }
2608
+ if (CONTROL_CHAR_PATTERN.test(pluginName)) {
2609
+ throw new Error("Plugin name contains invalid control characters.");
2610
+ }
2611
+ if (!SAFE_NAME_PATTERN.test(pluginName)) {
2612
+ throw new Error(
2613
+ `Plugin name contains invalid characters: "${pluginName}"
2614
+ Names may only contain alphanumeric characters, dashes, underscores, dots, @, and slashes.`
2615
+ );
2616
+ }
2617
+ }
2195
2618
  async function execCommand(command, args, options) {
2196
2619
  return new Promise((resolve, reject) => {
2197
2620
  const proc = spawn(command, args, {
@@ -2220,6 +2643,7 @@ async function execCommand(command, args, options) {
2220
2643
  });
2221
2644
  }
2222
2645
  async function claudePluginInstall(pluginPath, scope, projectDir) {
2646
+ validatePluginPath(pluginPath);
2223
2647
  const args = ["plugin", "install", pluginPath, "--scope", scope];
2224
2648
  const result = await execCommand("claude", args, { cwd: projectDir });
2225
2649
  if (result.exitCode !== 0) {
@@ -2262,14 +2686,14 @@ async function claudePluginMarketplaceExists(name) {
2262
2686
  return marketplaces.some((m) => m.name === name);
2263
2687
  }
2264
2688
  async function claudePluginMarketplaceAdd(githubRepo, name) {
2689
+ validateGithubRepo(githubRepo);
2690
+ validateMarketplaceName(name);
2265
2691
  const args = ["plugin", "marketplace", "add", githubRepo, "--name", name];
2266
2692
  let result;
2267
2693
  try {
2268
2694
  result = await execCommand("claude", args, {});
2269
2695
  } catch (err) {
2270
- throw new Error(
2271
- `Failed to add marketplace: ${err instanceof Error ? err.message : "Unknown error"}`
2272
- );
2696
+ throw new Error(`Failed to add marketplace: ${getErrorMessage(err)}`);
2273
2697
  }
2274
2698
  if (result.exitCode !== 0) {
2275
2699
  const errorMessage = result.stderr || result.stdout || "Unknown error";
@@ -2280,6 +2704,7 @@ async function claudePluginMarketplaceAdd(githubRepo, name) {
2280
2704
  }
2281
2705
  }
2282
2706
  async function claudePluginUninstall(pluginName, scope, projectDir) {
2707
+ validatePluginName(pluginName);
2283
2708
  const args = ["plugin", "uninstall", pluginName, "--scope", scope];
2284
2709
  const result = await execCommand("claude", args, { cwd: projectDir });
2285
2710
  if (result.exitCode !== 0) {
@@ -2351,9 +2776,33 @@ async function installStackAsPlugin(options) {
2351
2776
  }
2352
2777
 
2353
2778
  // src/cli/lib/installation/local-installer.ts
2354
- var PLUGIN_NAME = "claude-collective";
2355
- var YAML_INDENT = 2;
2356
- var YAML_LINE_WIDTH = 120;
2779
+ function resolveInstallPaths(projectDir) {
2780
+ return {
2781
+ skillsDir: path15.join(projectDir, LOCAL_SKILLS_PATH),
2782
+ agentsDir: path15.join(projectDir, CLAUDE_DIR, "agents"),
2783
+ configPath: path15.join(projectDir, CLAUDE_SRC_DIR, STANDARD_FILES.CONFIG_YAML)
2784
+ };
2785
+ }
2786
+ async function prepareDirectories(paths) {
2787
+ await ensureDir(paths.skillsDir);
2788
+ await ensureDir(paths.agentsDir);
2789
+ await ensureDir(path15.dirname(paths.configPath));
2790
+ }
2791
+ async function archiveAndCopySkills(wizardResult, sourceResult, projectDir, skillsDir) {
2792
+ for (const skillId of wizardResult.selectedSkills) {
2793
+ const selectedSource = wizardResult.sourceSelections?.[skillId];
2794
+ if (selectedSource && selectedSource !== "public") {
2795
+ verbose(`Using alternate source '${selectedSource}' for ${skillId}`);
2796
+ await archiveLocalSkill(projectDir, skillId);
2797
+ }
2798
+ }
2799
+ return copySkillsToLocalFlattened(
2800
+ wizardResult.selectedSkills,
2801
+ skillsDir,
2802
+ sourceResult.matrix,
2803
+ sourceResult
2804
+ );
2805
+ }
2357
2806
  function buildLocalSkillsMap(copiedSkills, matrix) {
2358
2807
  const localSkillsForResolution = {};
2359
2808
  for (const copiedSkill of copiedSkills) {
@@ -2370,6 +2819,11 @@ function buildLocalSkillsMap(copiedSkills, matrix) {
2370
2819
  }
2371
2820
  return localSkillsForResolution;
2372
2821
  }
2822
+ async function loadMergedAgents(sourcePath) {
2823
+ const cliAgents = await loadAllAgents(PROJECT_ROOT);
2824
+ const sourceAgents = await loadAllAgents(sourcePath);
2825
+ return { ...cliAgents, ...sourceAgents };
2826
+ }
2373
2827
  async function buildLocalConfig(wizardResult, sourceResult) {
2374
2828
  let loadedStack = null;
2375
2829
  if (wizardResult.selectedStackId) {
@@ -2382,7 +2836,7 @@ async function buildLocalConfig(wizardResult, sourceResult) {
2382
2836
  if (wizardResult.selectedStackId) {
2383
2837
  if (loadedStack) {
2384
2838
  localConfig = generateProjectConfigFromSkills(
2385
- PLUGIN_NAME,
2839
+ DEFAULT_PLUGIN_NAME,
2386
2840
  wizardResult.selectedSkills,
2387
2841
  sourceResult.matrix
2388
2842
  );
@@ -2401,7 +2855,7 @@ async function buildLocalConfig(wizardResult, sourceResult) {
2401
2855
  }
2402
2856
  } else {
2403
2857
  localConfig = generateProjectConfigFromSkills(
2404
- PLUGIN_NAME,
2858
+ DEFAULT_PLUGIN_NAME,
2405
2859
  wizardResult.selectedSkills,
2406
2860
  sourceResult.matrix
2407
2861
  );
@@ -2419,6 +2873,21 @@ function setConfigMetadata(config, wizardResult, sourceResult, sourceFlag) {
2419
2873
  config.marketplace = sourceResult.marketplace;
2420
2874
  }
2421
2875
  }
2876
+ async function buildAndMergeConfig(wizardResult, sourceResult, projectDir, sourceFlag) {
2877
+ const { config } = await buildLocalConfig(wizardResult, sourceResult);
2878
+ setConfigMetadata(config, wizardResult, sourceResult, sourceFlag);
2879
+ return mergeWithExistingConfig(config, { projectDir });
2880
+ }
2881
+ async function writeConfigFile(config, configPath) {
2882
+ const schemaComment = `${yamlSchemaComment(SCHEMA_PATHS.projectConfig)}
2883
+ `;
2884
+ const serializable = config.stack ? { ...config, stack: compactStackForYaml(config.stack) } : config;
2885
+ const configYaml = stringifyYaml3(serializable, {
2886
+ indent: YAML_FORMATTING.INDENT,
2887
+ lineWidth: YAML_FORMATTING.LINE_WIDTH
2888
+ });
2889
+ await writeFile(configPath, `${schemaComment}${configYaml}`);
2890
+ }
2422
2891
  function buildCompileAgents(config, agents) {
2423
2892
  const compileAgents = {};
2424
2893
  for (const agentId of config.agents) {
@@ -2453,42 +2922,22 @@ async function compileAndWriteAgents(compileConfig, agents, localSkills, sourceR
2453
2922
  }
2454
2923
  async function installLocal(options) {
2455
2924
  const { wizardResult, sourceResult, projectDir, sourceFlag } = options;
2456
- const matrix = sourceResult.matrix;
2457
- const localSkillsDir = path15.join(projectDir, LOCAL_SKILLS_PATH);
2458
- const localAgentsDir = path15.join(projectDir, CLAUDE_DIR, "agents");
2459
- const localConfigPath = path15.join(projectDir, CLAUDE_SRC_DIR, "config.yaml");
2460
- await ensureDir(localSkillsDir);
2461
- await ensureDir(localAgentsDir);
2462
- await ensureDir(path15.dirname(localConfigPath));
2463
- for (const skillId of wizardResult.selectedSkills) {
2464
- const selectedSource = wizardResult.sourceSelections?.[skillId];
2465
- if (selectedSource && selectedSource !== "public") {
2466
- verbose(`Using alternate source '${selectedSource}' for ${skillId}`);
2467
- await archiveLocalSkill(projectDir, skillId);
2468
- }
2469
- }
2470
- const copiedSkills = await copySkillsToLocalFlattened(
2471
- wizardResult.selectedSkills,
2472
- localSkillsDir,
2473
- matrix,
2474
- sourceResult
2925
+ const paths = resolveInstallPaths(projectDir);
2926
+ await prepareDirectories(paths);
2927
+ const copiedSkills = await archiveAndCopySkills(
2928
+ wizardResult,
2929
+ sourceResult,
2930
+ projectDir,
2931
+ paths.skillsDir
2475
2932
  );
2476
- const localSkillsForResolution = buildLocalSkillsMap(copiedSkills, matrix);
2477
- const cliAgents = await loadAllAgents(PROJECT_ROOT);
2478
- const localAgents = await loadAllAgents(sourceResult.sourcePath);
2479
- const agents = { ...cliAgents, ...localAgents };
2480
- const { config: builtConfig } = await buildLocalConfig(wizardResult, sourceResult);
2481
- setConfigMetadata(builtConfig, wizardResult, sourceResult, sourceFlag);
2482
- const mergeResult = await mergeWithExistingConfig(builtConfig, { projectDir });
2933
+ const localSkillsForResolution = buildLocalSkillsMap(copiedSkills, sourceResult.matrix);
2934
+ const agents = await loadMergedAgents(sourceResult.sourcePath);
2935
+ const mergeResult = await buildAndMergeConfig(wizardResult, sourceResult, projectDir, sourceFlag);
2483
2936
  const finalConfig = mergeResult.config;
2484
- const configYaml = stringifyYaml4(finalConfig, {
2485
- indent: YAML_INDENT,
2486
- lineWidth: YAML_LINE_WIDTH
2487
- });
2488
- await writeFile(localConfigPath, configYaml);
2937
+ await writeConfigFile(finalConfig, paths.configPath);
2489
2938
  const compileAgentsConfig = buildCompileAgents(finalConfig, agents);
2490
2939
  const compileConfig = {
2491
- name: PLUGIN_NAME,
2940
+ name: DEFAULT_PLUGIN_NAME,
2492
2941
  description: finalConfig.description || `Local setup with ${wizardResult.selectedSkills.length} skills`,
2493
2942
  agents: compileAgentsConfig
2494
2943
  };
@@ -2498,23 +2947,22 @@ async function installLocal(options) {
2498
2947
  localSkillsForResolution,
2499
2948
  sourceResult,
2500
2949
  projectDir,
2501
- localAgentsDir,
2950
+ paths.agentsDir,
2502
2951
  wizardResult.installMode
2503
2952
  );
2504
2953
  return {
2505
2954
  copiedSkills,
2506
2955
  config: finalConfig,
2507
- configPath: localConfigPath,
2956
+ configPath: paths.configPath,
2508
2957
  compiledAgents: compiledAgentNames,
2509
2958
  wasMerged: mergeResult.merged,
2510
2959
  mergedConfigPath: mergeResult.existingConfigPath,
2511
- skillsDir: localSkillsDir,
2512
- agentsDir: localAgentsDir
2960
+ skillsDir: paths.skillsDir,
2961
+ agentsDir: paths.agentsDir
2513
2962
  };
2514
2963
  }
2515
2964
 
2516
2965
  // src/cli/lib/plugins/plugin-info.ts
2517
- var DEFAULT_NAME = "claude-collective";
2518
2966
  async function getInstallationInfo() {
2519
2967
  const installation = await detectInstallation();
2520
2968
  if (!installation) {
@@ -2522,7 +2970,7 @@ async function getInstallationInfo() {
2522
2970
  }
2523
2971
  let skillCount = 0;
2524
2972
  let agentCount = 0;
2525
- let name = DEFAULT_NAME;
2973
+ let name = DEFAULT_PLUGIN_NAME;
2526
2974
  let version = DEFAULT_DISPLAY_VERSION;
2527
2975
  if (await directoryExists(installation.skillsDir)) {
2528
2976
  try {
@@ -2545,14 +2993,14 @@ async function getInstallationInfo() {
2545
2993
  if (installation.mode === "local") {
2546
2994
  const loaded = await loadProjectConfig(installation.projectDir);
2547
2995
  if (loaded?.config) {
2548
- name = loaded.config.name || DEFAULT_NAME;
2996
+ name = loaded.config.name || DEFAULT_PLUGIN_NAME;
2549
2997
  version = "local";
2550
2998
  }
2551
2999
  } else {
2552
3000
  const pluginDir = getCollectivePluginDir(installation.projectDir);
2553
3001
  const manifest = await readPluginManifest(pluginDir);
2554
3002
  if (manifest) {
2555
- name = manifest.name || DEFAULT_NAME;
3003
+ name = manifest.name || DEFAULT_PLUGIN_NAME;
2556
3004
  version = manifest.version || DEFAULT_DISPLAY_VERSION;
2557
3005
  }
2558
3006
  }
@@ -2587,7 +3035,7 @@ function parseVersion(version) {
2587
3035
  }
2588
3036
  async function bumpPluginVersion(pluginDir, type) {
2589
3037
  const manifestPath = path16.join(pluginDir, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
2590
- const content = await readFile(manifestPath);
3038
+ const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
2591
3039
  const manifest = pluginManifestSchema.parse(JSON.parse(content));
2592
3040
  const [major, minor, patch] = parseVersion(manifest.version || DEFAULT_VERSION);
2593
3041
  let newVersion;
@@ -2601,6 +3049,10 @@ async function bumpPluginVersion(pluginDir, type) {
2601
3049
  case "patch":
2602
3050
  newVersion = `${major}.${minor}.${patch + 1}`;
2603
3051
  break;
3052
+ default: {
3053
+ const exhaustiveCheck = type;
3054
+ throw new Error(`Unknown version bump type: ${exhaustiveCheck}`);
3055
+ }
2604
3056
  }
2605
3057
  manifest.version = newVersion;
2606
3058
  await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
@@ -2608,7 +3060,7 @@ async function bumpPluginVersion(pluginDir, type) {
2608
3060
  }
2609
3061
  async function getPluginVersion(pluginDir) {
2610
3062
  const manifestPath = path16.join(pluginDir, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
2611
- const content = await readFile(manifestPath);
3063
+ const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
2612
3064
  const manifest = pluginManifestSchema.parse(JSON.parse(content));
2613
3065
  return manifest.version || DEFAULT_VERSION;
2614
3066
  }
@@ -2619,8 +3071,8 @@ import { z as z2 } from "zod";
2619
3071
  import path17 from "path";
2620
3072
  import fg from "fast-glob";
2621
3073
  import { countBy as countBy2 } from "remeda";
2622
- var PLUGIN_DIR = ".claude-plugin";
2623
- var PLUGIN_MANIFEST = "plugin.json";
3074
+ var PLUGIN_DIR = PLUGIN_MANIFEST_DIR;
3075
+ var PLUGIN_MANIFEST = STANDARD_FILES.PLUGIN_JSON;
2624
3076
  var KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2625
3077
  var SEMVER_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
2626
3078
  var pluginManifestValidationSchema = z2.object({
@@ -2634,13 +3086,13 @@ var pluginManifestValidationSchema = z2.object({
2634
3086
  skills: z2.union([z2.string(), z2.array(z2.string())]).optional(),
2635
3087
  hooks: z2.union([z2.string(), hooksRecordSchema]).optional()
2636
3088
  }).strict();
2637
- function formatZodErrors(error) {
3089
+ function formatZodErrors2(error) {
2638
3090
  return error.issues.map((issue) => {
2639
- const path26 = issue.path.join(".");
3091
+ const path25 = issue.path.join(".");
2640
3092
  if (issue.code === "unrecognized_keys") {
2641
3093
  return `Unrecognized key: "${issue.keys.join('", "')}"`;
2642
3094
  }
2643
- return path26 ? `${path26}: ${issue.message}` : issue.message;
3095
+ return path25 ? `${path25}: ${issue.message}` : issue.message;
2644
3096
  });
2645
3097
  }
2646
3098
  function isKebabCase(str) {
@@ -2689,19 +3141,18 @@ async function validatePluginManifest(manifestPath) {
2689
3141
  }
2690
3142
  let manifest;
2691
3143
  try {
2692
- const content = await readFile(manifestPath);
3144
+ const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
2693
3145
  manifest = JSON.parse(content);
2694
3146
  } catch (err) {
2695
- const message = err instanceof Error ? err.message : String(err);
2696
3147
  return {
2697
3148
  valid: false,
2698
- errors: [`Invalid JSON in ${PLUGIN_MANIFEST}: ${message}`],
3149
+ errors: [`Invalid JSON in ${PLUGIN_MANIFEST}: ${getErrorMessage(err)}`],
2699
3150
  warnings: []
2700
3151
  };
2701
3152
  }
2702
3153
  const result = pluginManifestValidationSchema.safeParse(manifest);
2703
3154
  if (!result.success) {
2704
- errors.push(...formatZodErrors(result.error));
3155
+ errors.push(...formatZodErrors2(result.error));
2705
3156
  }
2706
3157
  if (manifest.name && typeof manifest.name === "string") {
2707
3158
  if (!isKebabCase(manifest.name)) {
@@ -2758,7 +3209,7 @@ async function validateSkillFrontmatter(skillPath) {
2758
3209
  }
2759
3210
  const result = skillFrontmatterValidationSchema.safeParse(frontmatter);
2760
3211
  if (!result.success) {
2761
- errors.push(...formatZodErrors(result.error));
3212
+ errors.push(...formatZodErrors2(result.error));
2762
3213
  }
2763
3214
  return {
2764
3215
  valid: errors.length === 0,
@@ -2787,7 +3238,7 @@ async function validateAgentFrontmatter(agentPath) {
2787
3238
  }
2788
3239
  const result = agentFrontmatterValidationSchema.safeParse(frontmatter);
2789
3240
  if (!result.success) {
2790
- errors.push(...formatZodErrors(result.error));
3241
+ errors.push(...formatZodErrors2(result.error));
2791
3242
  }
2792
3243
  const fm = frontmatter;
2793
3244
  if (fm.name && typeof fm.name === "string") {
@@ -2801,74 +3252,72 @@ async function validateAgentFrontmatter(agentPath) {
2801
3252
  warnings
2802
3253
  };
2803
3254
  }
2804
- async function validatePlugin(pluginPath) {
2805
- const errors = [];
2806
- const warnings = [];
2807
- const structureResult = await validatePluginStructure(pluginPath);
2808
- errors.push(...structureResult.errors);
2809
- warnings.push(...structureResult.warnings);
2810
- if (!structureResult.valid) {
2811
- return { valid: false, errors, warnings };
3255
+ function mergeResults(results) {
3256
+ const errors = results.flatMap((r) => r.errors);
3257
+ const warnings = results.flatMap((r) => r.warnings);
3258
+ return { valid: errors.length === 0, errors, warnings };
3259
+ }
3260
+ var EMPTY_RESULT = { valid: true, errors: [], warnings: [] };
3261
+ function prefixResult(result, prefix) {
3262
+ return {
3263
+ valid: result.valid,
3264
+ errors: result.valid ? [] : result.errors.map((e) => `${prefix}: ${e}`),
3265
+ warnings: result.warnings.map((w) => `${prefix}: ${w}`)
3266
+ };
3267
+ }
3268
+ async function validatePluginSkillFiles(pluginPath, skillsRelPath) {
3269
+ const skillsDir = path17.join(pluginPath, skillsRelPath);
3270
+ if (!await directoryExists(skillsDir)) return EMPTY_RESULT;
3271
+ const files = await fg("**/SKILL.md", { cwd: skillsDir, absolute: true });
3272
+ if (files.length === 0) {
3273
+ return {
3274
+ valid: true,
3275
+ errors: [],
3276
+ warnings: [`Skills directory exists but contains no SKILL.md files: ${skillsRelPath}`]
3277
+ };
2812
3278
  }
2813
- const manifestPath = path17.join(pluginPath, PLUGIN_DIR, PLUGIN_MANIFEST);
2814
- const manifestResult = await validatePluginManifest(manifestPath);
2815
- errors.push(...manifestResult.errors);
2816
- warnings.push(...manifestResult.warnings);
2817
- let manifest = null;
3279
+ const results = await Promise.all(
3280
+ files.map(
3281
+ async (f) => prefixResult(await validateSkillFrontmatter(f), path17.relative(pluginPath, f))
3282
+ )
3283
+ );
3284
+ return mergeResults(results);
3285
+ }
3286
+ async function validatePluginAgentFiles(pluginPath, agentsRelPath) {
3287
+ const agentsDir = path17.join(pluginPath, agentsRelPath);
3288
+ if (!await directoryExists(agentsDir)) return EMPTY_RESULT;
3289
+ const files = await fg("*.md", { cwd: agentsDir, absolute: true });
3290
+ if (files.length === 0) {
3291
+ return {
3292
+ valid: true,
3293
+ errors: [],
3294
+ warnings: [`Agents directory exists but contains no .md files: ${agentsRelPath}`]
3295
+ };
3296
+ }
3297
+ const results = await Promise.all(
3298
+ files.map(
3299
+ async (f) => prefixResult(await validateAgentFrontmatter(f), path17.relative(pluginPath, f))
3300
+ )
3301
+ );
3302
+ return mergeResults(results);
3303
+ }
3304
+ async function loadManifestForValidation(manifestPath) {
2818
3305
  try {
2819
- const content = await readFile(manifestPath);
2820
- manifest = JSON.parse(content);
3306
+ const content = await readFileSafe(manifestPath, MAX_PLUGIN_FILE_SIZE);
3307
+ return JSON.parse(content);
2821
3308
  } catch {
3309
+ return null;
2822
3310
  }
2823
- if (manifest) {
2824
- if (manifest.skills && typeof manifest.skills === "string") {
2825
- const skillsDir = path17.join(pluginPath, manifest.skills);
2826
- if (await directoryExists(skillsDir)) {
2827
- const skillFiles = await fg("**/SKILL.md", {
2828
- cwd: skillsDir,
2829
- absolute: true
2830
- });
2831
- if (skillFiles.length === 0) {
2832
- warnings.push(
2833
- `Skills directory exists but contains no SKILL.md files: ${manifest.skills}`
2834
- );
2835
- }
2836
- for (const skillFile of skillFiles) {
2837
- const relativePath = path17.relative(pluginPath, skillFile);
2838
- const skillResult = await validateSkillFrontmatter(skillFile);
2839
- if (!skillResult.valid) {
2840
- errors.push(...skillResult.errors.map((e) => `${relativePath}: ${e}`));
2841
- }
2842
- warnings.push(...skillResult.warnings.map((w) => `${relativePath}: ${w}`));
2843
- }
2844
- }
2845
- }
2846
- if (manifest.agents && typeof manifest.agents === "string") {
2847
- const agentsDir = path17.join(pluginPath, manifest.agents);
2848
- if (await directoryExists(agentsDir)) {
2849
- const agentFiles = await fg("*.md", {
2850
- cwd: agentsDir,
2851
- absolute: true
2852
- });
2853
- if (agentFiles.length === 0) {
2854
- warnings.push(`Agents directory exists but contains no .md files: ${manifest.agents}`);
2855
- }
2856
- for (const agentFile of agentFiles) {
2857
- const relativePath = path17.relative(pluginPath, agentFile);
2858
- const agentResult = await validateAgentFrontmatter(agentFile);
2859
- if (!agentResult.valid) {
2860
- errors.push(...agentResult.errors.map((e) => `${relativePath}: ${e}`));
2861
- }
2862
- warnings.push(...agentResult.warnings.map((w) => `${relativePath}: ${w}`));
2863
- }
2864
- }
2865
- }
2866
- }
2867
- return {
2868
- valid: errors.length === 0,
2869
- errors,
2870
- warnings
2871
- };
3311
+ }
3312
+ async function validatePlugin(pluginPath) {
3313
+ const structureResult = await validatePluginStructure(pluginPath);
3314
+ if (!structureResult.valid) return structureResult;
3315
+ const manifestPath = path17.join(pluginPath, PLUGIN_DIR, PLUGIN_MANIFEST);
3316
+ const manifestResult = await validatePluginManifest(manifestPath);
3317
+ const manifest = await loadManifestForValidation(manifestPath);
3318
+ const skillsResult = manifest?.skills && typeof manifest.skills === "string" ? await validatePluginSkillFiles(pluginPath, manifest.skills) : EMPTY_RESULT;
3319
+ const agentsResult = manifest?.agents && typeof manifest.agents === "string" ? await validatePluginAgentFiles(pluginPath, manifest.agents) : EMPTY_RESULT;
3320
+ return mergeResults([structureResult, manifestResult, skillsResult, agentsResult]);
2872
3321
  }
2873
3322
  async function validateAllPlugins(pluginsDir) {
2874
3323
  const results = [];
@@ -2936,21 +3385,21 @@ function printPluginValidationResult(name, result, verbose2 = false) {
2936
3385
  if (result.valid && result.warnings.length === 0 && !verbose2) {
2937
3386
  return;
2938
3387
  }
2939
- console.log(`
3388
+ log(`
2940
3389
  ${status} ${name}`);
2941
3390
  if (result.errors.length > 0) {
2942
- console.log(" Errors:");
2943
- result.errors.forEach((e) => console.log(` - ${e}`));
3391
+ log(" Errors:");
3392
+ result.errors.forEach((e) => log(` - ${e}`));
2944
3393
  }
2945
3394
  if (result.warnings.length > 0) {
2946
- console.log(" Warnings:");
2947
- result.warnings.forEach((w) => console.log(` - ${w}`));
3395
+ log(" Warnings:");
3396
+ result.warnings.forEach((w) => log(` - ${w}`));
2948
3397
  }
2949
3398
  }
2950
3399
 
2951
3400
  // src/cli/lib/loading/multi-source-loader.ts
2952
3401
  var PUBLIC_SOURCE_NAME = "public";
2953
- async function loadSkillsFromAllSources(primaryMatrix, sourceConfig, projectDir) {
3402
+ async function loadSkillsFromAllSources(primaryMatrix, _sourceConfig, projectDir) {
2954
3403
  tagPrimarySourceSkills(primaryMatrix);
2955
3404
  tagLocalSkills(primaryMatrix);
2956
3405
  await tagPluginSkills(primaryMatrix, projectDir);
@@ -2958,9 +3407,7 @@ async function loadSkillsFromAllSources(primaryMatrix, sourceConfig, projectDir)
2958
3407
  setActiveSources(primaryMatrix);
2959
3408
  }
2960
3409
  function tagPrimarySourceSkills(matrix) {
2961
- for (const [, skill] of typedEntries(
2962
- matrix.skills
2963
- )) {
3410
+ for (const [, skill] of typedEntries(matrix.skills)) {
2964
3411
  if (!skill) continue;
2965
3412
  const source = {
2966
3413
  name: PUBLIC_SOURCE_NAME,
@@ -2974,9 +3421,7 @@ function tagPrimarySourceSkills(matrix) {
2974
3421
  }
2975
3422
  function tagLocalSkills(matrix) {
2976
3423
  let count = 0;
2977
- for (const [, skill] of typedEntries(
2978
- matrix.skills
2979
- )) {
3424
+ for (const [, skill] of typedEntries(matrix.skills)) {
2980
3425
  if (!skill) continue;
2981
3426
  if (!skill.local) continue;
2982
3427
  const source = {
@@ -3063,14 +3508,12 @@ async function tagExtraSources(matrix, projectDir) {
3063
3508
  `Extra source '${extraSource.name}': ${skills.length} skills found, ${matchCount} matching`
3064
3509
  );
3065
3510
  } catch (error) {
3066
- warn(`Failed to load extra source '${extraSource.name}' (${extraSource.url}): ${error}`);
3511
+ warn(`Failed to load extra source '${extraSource.name}' ('${extraSource.url}'): ${error}`);
3067
3512
  }
3068
3513
  }
3069
3514
  }
3070
3515
  function setActiveSources(matrix) {
3071
- for (const [, skill] of typedEntries(
3072
- matrix.skills
3073
- )) {
3516
+ for (const [, skill] of typedEntries(matrix.skills)) {
3074
3517
  if (!skill) continue;
3075
3518
  if (!skill.availableSources || skill.availableSources.length === 0) continue;
3076
3519
  const installedSource = skill.availableSources.find((s) => s.installed);
@@ -3102,7 +3545,7 @@ async function searchExtraSources(alias, configuredSources) {
3102
3545
  }
3103
3546
  }
3104
3547
  } catch (error) {
3105
- warn(`Failed to search extra source '${source.name}' (${source.url}): ${error}`);
3548
+ warn(`Failed to search extra source '${source.name}' ('${source.url}'): ${error}`);
3106
3549
  }
3107
3550
  }
3108
3551
  return candidates;
@@ -3141,30 +3584,7 @@ async function loadFromLocal(source, sourceConfig) {
3141
3584
  skillsPath = PROJECT_ROOT;
3142
3585
  }
3143
3586
  verbose(`Loading skills from local path: ${skillsPath}`);
3144
- const sourceMatrixPath = path19.join(skillsPath, SKILLS_MATRIX_PATH);
3145
- const cliMatrixPath = path19.join(PROJECT_ROOT, SKILLS_MATRIX_PATH);
3146
- let matrixPath;
3147
- if (await fileExists(sourceMatrixPath)) {
3148
- matrixPath = sourceMatrixPath;
3149
- verbose(`Matrix from source: ${matrixPath}`);
3150
- } else {
3151
- matrixPath = cliMatrixPath;
3152
- verbose(`Matrix from CLI (source has no matrix): ${matrixPath}`);
3153
- }
3154
- const skillsDir = path19.join(skillsPath, SKILLS_DIR_PATH);
3155
- verbose(`Skills from source: ${skillsDir}`);
3156
- const matrix = await loadSkillsMatrix(matrixPath);
3157
- const skills = await extractAllSkills(skillsDir);
3158
- const mergedMatrix = await mergeMatrixWithSkills(matrix, skills);
3159
- const sourceStacks = await loadStacks(skillsPath);
3160
- const stacks = sourceStacks.length > 0 ? sourceStacks : await loadStacks(PROJECT_ROOT);
3161
- if (stacks.length > 0) {
3162
- mergedMatrix.suggestedStacks = stacks.map(
3163
- (stack) => stackToResolvedStack(stack, mergedMatrix.displayNameToId)
3164
- );
3165
- const stackSource = sourceStacks.length > 0 ? "source" : "CLI";
3166
- verbose(`Loaded ${stacks.length} stacks from ${stackSource}`);
3167
- }
3587
+ const mergedMatrix = await loadAndMergeFromBasePath(skillsPath);
3168
3588
  return {
3169
3589
  matrix: mergedMatrix,
3170
3590
  sourceConfig,
@@ -3177,7 +3597,21 @@ async function loadFromRemote(source, sourceConfig, forceRefresh) {
3177
3597
  verbose(`Fetching skills from remote source: ${source}`);
3178
3598
  const fetchResult = await fetchFromSource(source, { forceRefresh });
3179
3599
  verbose(`Fetched to: ${fetchResult.path}`);
3180
- const sourceMatrixPath = path19.join(fetchResult.path, SKILLS_MATRIX_PATH);
3600
+ const mergedMatrix = await loadAndMergeFromBasePath(fetchResult.path);
3601
+ return {
3602
+ matrix: mergedMatrix,
3603
+ sourceConfig,
3604
+ sourcePath: fetchResult.path,
3605
+ isLocal: false,
3606
+ marketplace: sourceConfig.marketplace
3607
+ };
3608
+ }
3609
+ async function loadAndMergeFromBasePath(basePath) {
3610
+ const sourceProjectConfig = await loadProjectSourceConfig(basePath);
3611
+ const matrixRelPath = sourceProjectConfig?.matrix_file ?? SKILLS_MATRIX_PATH;
3612
+ const skillsDirRelPath = sourceProjectConfig?.skills_dir ?? SKILLS_DIR_PATH;
3613
+ const stacksRelFile = sourceProjectConfig?.stacks_file;
3614
+ const sourceMatrixPath = path19.join(basePath, matrixRelPath);
3181
3615
  const cliMatrixPath = path19.join(PROJECT_ROOT, SKILLS_MATRIX_PATH);
3182
3616
  let matrixPath;
3183
3617
  if (await fileExists(sourceMatrixPath)) {
@@ -3187,35 +3621,27 @@ async function loadFromRemote(source, sourceConfig, forceRefresh) {
3187
3621
  matrixPath = cliMatrixPath;
3188
3622
  verbose(`Matrix from CLI (source has no matrix): ${matrixPath}`);
3189
3623
  }
3190
- const skillsDir = path19.join(fetchResult.path, SKILLS_DIR_PATH);
3624
+ const skillsDir = path19.join(basePath, skillsDirRelPath);
3191
3625
  verbose(`Skills from source: ${skillsDir}`);
3192
3626
  const matrix = await loadSkillsMatrix(matrixPath);
3193
3627
  const skills = await extractAllSkills(skillsDir);
3194
3628
  const mergedMatrix = await mergeMatrixWithSkills(matrix, skills);
3195
- const sourceStacks = await loadStacks(fetchResult.path);
3629
+ const sourceStacks = await loadStacks(basePath, stacksRelFile);
3196
3630
  const stacks = sourceStacks.length > 0 ? sourceStacks : await loadStacks(PROJECT_ROOT);
3197
3631
  if (stacks.length > 0) {
3198
- mergedMatrix.suggestedStacks = stacks.map(
3199
- (stack) => stackToResolvedStack(stack, mergedMatrix.displayNameToId)
3200
- );
3632
+ mergedMatrix.suggestedStacks = stacks.map((stack) => convertStackToResolvedStack(stack));
3201
3633
  const stackSource = sourceStacks.length > 0 ? "source" : "CLI";
3202
3634
  verbose(`Loaded ${stacks.length} stacks from ${stackSource}`);
3203
3635
  }
3204
- return {
3205
- matrix: mergedMatrix,
3206
- sourceConfig,
3207
- sourcePath: fetchResult.path,
3208
- isLocal: false,
3209
- marketplace: sourceConfig.marketplace
3210
- };
3636
+ return mergedMatrix;
3211
3637
  }
3212
- function stackToResolvedStack(stack, displayNameToId) {
3638
+ function convertStackToResolvedStack(stack) {
3213
3639
  const allSkillIds = [];
3214
3640
  const seenSkillIds = /* @__PURE__ */ new Set();
3215
3641
  for (const agentId of typedKeys(stack.agents)) {
3216
3642
  const agentConfig = stack.agents[agentId];
3217
3643
  if (!agentConfig) continue;
3218
- const skillRefs = resolveAgentConfigToSkills(agentConfig, displayNameToId);
3644
+ const skillRefs = resolveAgentConfigToSkills(agentConfig);
3219
3645
  for (const ref of skillRefs) {
3220
3646
  if (!seenSkillIds.has(ref.id)) {
3221
3647
  seenSkillIds.add(ref.id);
@@ -3237,6 +3663,26 @@ function stackToResolvedStack(stack, displayNameToId) {
3237
3663
  philosophy: stack.philosophy || ""
3238
3664
  };
3239
3665
  }
3666
+ function extractSourceName(source) {
3667
+ const withoutProtocol = source.replace(/^(?:github|gh|gitlab|bitbucket|sourcehut):/, "");
3668
+ const withoutUrl = withoutProtocol.replace(/^https?:\/\/[^/]+\//, "");
3669
+ const firstSegment = withoutUrl.split("/")[0];
3670
+ return firstSegment || source;
3671
+ }
3672
+ function getMarketplaceLabel(sourceResult) {
3673
+ if (sourceResult.isLocal) return void 0;
3674
+ const { marketplace } = sourceResult;
3675
+ if (!marketplace) {
3676
+ const name = extractSourceName(sourceResult.sourceConfig.source);
3677
+ return `${name} (public)`;
3678
+ }
3679
+ const PUBLIC_MARKETPLACE_COUNT = 1;
3680
+ const isDefaultSource = sourceResult.sourceConfig.source === DEFAULT_SOURCE;
3681
+ if (!isDefaultSource) {
3682
+ return `${marketplace} + ${PUBLIC_MARKETPLACE_COUNT} public`;
3683
+ }
3684
+ return marketplace;
3685
+ }
3240
3686
  function mergeLocalSkillsIntoMatrix(matrix, localResult) {
3241
3687
  for (const metadata of localResult.skills) {
3242
3688
  const existingSkill = matrix.skills[metadata.id];
@@ -3250,7 +3696,7 @@ function mergeLocalSkillsIntoMatrix(matrix, localResult) {
3250
3696
  category,
3251
3697
  categoryExclusive: metadata.categoryExclusive,
3252
3698
  tags: metadata.tags ?? [],
3253
- author: "@local",
3699
+ author: LOCAL_DEFAULTS.AUTHOR,
3254
3700
  conflictsWith: existingSkill?.conflictsWith ?? [],
3255
3701
  recommends: existingSkill?.recommends ?? [],
3256
3702
  requires: existingSkill?.requires ?? [],
@@ -3271,7 +3717,6 @@ function mergeLocalSkillsIntoMatrix(matrix, localResult) {
3271
3717
 
3272
3718
  // src/cli/lib/loading/defaults-loader.ts
3273
3719
  init_esm_shims();
3274
- import { parse as parseYaml8 } from "yaml";
3275
3720
  var cachedDefaults = null;
3276
3721
  function getCachedDefaults() {
3277
3722
  return cachedDefaults;
@@ -3389,7 +3834,7 @@ function getEffectiveSkillToAgents() {
3389
3834
  }
3390
3835
  return SKILL_TO_AGENTS;
3391
3836
  }
3392
- function getAgentsForSkill(skillPath, category, projectConfig) {
3837
+ function getAgentsForSkill(skillPath, category, _projectConfig) {
3393
3838
  const normalizedPath = skillPath.replace(/^skills\//, "").replace(/\/$/, "");
3394
3839
  const skillToAgents = getEffectiveSkillToAgents();
3395
3840
  if (skillToAgents[category]) {
@@ -3414,14 +3859,12 @@ function getAgentsForSkill(skillPath, category, projectConfig) {
3414
3859
  // src/cli/lib/skills/skill-plugin-compiler.ts
3415
3860
  init_esm_shims();
3416
3861
  import path20 from "path";
3417
- import { parse as parseYaml9 } from "yaml";
3418
- var SKILL_FILES = ["SKILL.md", "reference.md"];
3419
- var SKILL_DIRS = ["examples", "scripts"];
3862
+ import { parse as parseYaml7 } from "yaml";
3420
3863
  function sanitizeSkillName(name) {
3421
3864
  return name.replace(/\+/g, "-");
3422
3865
  }
3423
3866
  async function readSkillMetadata(skillPath) {
3424
- const metadataPath = path20.join(skillPath, "metadata.yaml");
3867
+ const metadataPath = path20.join(skillPath, STANDARD_FILES.METADATA_YAML);
3425
3868
  if (!await fileExists(metadataPath)) {
3426
3869
  return null;
3427
3870
  }
@@ -3429,16 +3872,14 @@ async function readSkillMetadata(skillPath) {
3429
3872
  const content = await readFile(metadataPath);
3430
3873
  const lines = content.split("\n");
3431
3874
  const yamlContent = lines[0]?.startsWith("# yaml-language-server:") ? lines.slice(1).join("\n") : content;
3432
- const result = skillMetadataLoaderSchema.safeParse(parseYaml9(yamlContent));
3875
+ const result = skillMetadataLoaderSchema.safeParse(parseYaml7(yamlContent));
3433
3876
  if (!result.success) {
3434
- warn(
3435
- `Invalid metadata.yaml at ${skillPath}: ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
3436
- );
3877
+ warn(`Invalid metadata.yaml at '${skillPath}': ${formatZodErrors(result.error.issues)}`);
3437
3878
  return null;
3438
3879
  }
3439
3880
  return result.data;
3440
3881
  } catch (error) {
3441
- warn(`Failed to read metadata.yaml at ${skillPath}: ${error}`);
3882
+ warn(`Failed to read metadata.yaml at '${skillPath}': ${error}`);
3442
3883
  return null;
3443
3884
  }
3444
3885
  }
@@ -3459,17 +3900,17 @@ function generateReadme(skillName, frontmatter, metadata) {
3459
3900
  lines.push("Add this plugin to your Claude Code configuration:");
3460
3901
  lines.push("");
3461
3902
  lines.push("```json");
3462
- lines.push(`{`);
3903
+ lines.push("{");
3463
3904
  lines.push(` "plugins": ["${skillName}"]`);
3464
- lines.push(`}`);
3905
+ lines.push("}");
3465
3906
  lines.push("```");
3466
3907
  lines.push("");
3467
3908
  lines.push("## Usage");
3468
3909
  lines.push("");
3469
- lines.push(`This skill is automatically available when installed.`);
3910
+ lines.push("This skill is automatically available when installed.");
3470
3911
  if (metadata?.requires && metadata.requires.length > 0) {
3471
3912
  lines.push("");
3472
- lines.push("**Requires:** " + metadata.requires.join(", "));
3913
+ lines.push(`**Requires:** ${metadata.requires.join(", ")}`);
3473
3914
  }
3474
3915
  lines.push("");
3475
3916
  lines.push("---");
@@ -3481,17 +3922,17 @@ function generateReadme(skillName, frontmatter, metadata) {
3481
3922
  async function compileSkillPlugin(options) {
3482
3923
  const { skillPath, outputDir, skillName: overrideName } = options;
3483
3924
  const dirBasename = path20.basename(skillPath);
3484
- const skillMdPath = path20.join(skillPath, "SKILL.md");
3925
+ const skillMdPath = path20.join(skillPath, STANDARD_FILES.SKILL_MD);
3485
3926
  if (!await fileExists(skillMdPath)) {
3486
3927
  throw new Error(
3487
- `Skill '${dirBasename}' is missing required SKILL.md file. Expected at: ${skillMdPath}`
3928
+ `Skill '${dirBasename}' is missing required ${STANDARD_FILES.SKILL_MD} file. Expected at: ${skillMdPath}`
3488
3929
  );
3489
3930
  }
3490
3931
  const skillMdContent = await readFile(skillMdPath);
3491
3932
  const frontmatter = parseFrontmatter(skillMdContent, skillMdPath);
3492
3933
  if (!frontmatter) {
3493
3934
  throw new Error(
3494
- `Skill '${dirBasename}' has invalid or missing YAML frontmatter in SKILL.md. Required fields: 'name' and 'description'. File: ${skillMdPath}`
3935
+ `Skill '${dirBasename}' has invalid or missing YAML frontmatter in ${STANDARD_FILES.SKILL_MD}. Required fields: 'name' and 'description'. File: ${skillMdPath}`
3495
3936
  );
3496
3937
  }
3497
3938
  const skillName = overrideName ?? sanitizeSkillName(frontmatter.name);
@@ -3501,7 +3942,7 @@ async function compileSkillPlugin(options) {
3501
3942
  const skillsDir = path20.join(pluginDir, "skills", skillName);
3502
3943
  await ensureDir(pluginDir);
3503
3944
  await ensureDir(skillsDir);
3504
- const newHash = await hashSkillFolder(skillPath);
3945
+ const newHash = await computeSkillFolderHash(skillPath);
3505
3946
  const { version, contentHash } = await determinePluginVersion(
3506
3947
  newHash,
3507
3948
  pluginDir,
@@ -3517,10 +3958,10 @@ async function compileSkillPlugin(options) {
3517
3958
  await writePluginManifest(pluginDir, manifest);
3518
3959
  await writeContentHash(pluginDir, contentHash, getPluginManifestPath);
3519
3960
  verbose(` Wrote plugin.json for ${skillName} (v${version})`);
3520
- await writeFile(path20.join(skillsDir, "SKILL.md"), skillMdContent);
3521
- verbose(` Copied SKILL.md`);
3522
- for (const fileName of SKILL_FILES) {
3523
- if (fileName === "SKILL.md") continue;
3961
+ await writeFile(path20.join(skillsDir, STANDARD_FILES.SKILL_MD), skillMdContent);
3962
+ verbose(` Copied ${STANDARD_FILES.SKILL_MD}`);
3963
+ for (const fileName of SKILL_CONTENT_FILES) {
3964
+ if (fileName === STANDARD_FILES.SKILL_MD) continue;
3524
3965
  const sourcePath = path20.join(skillPath, fileName);
3525
3966
  if (await fileExists(sourcePath)) {
3526
3967
  const content = await readFile(sourcePath);
@@ -3528,7 +3969,7 @@ async function compileSkillPlugin(options) {
3528
3969
  verbose(` Copied ${fileName}`);
3529
3970
  }
3530
3971
  }
3531
- for (const dirName of SKILL_DIRS) {
3972
+ for (const dirName of SKILL_CONTENT_DIRS) {
3532
3973
  const sourceDir = path20.join(skillPath, dirName);
3533
3974
  if (await fileExists(sourceDir)) {
3534
3975
  await copy(sourceDir, path20.join(skillsDir, dirName));
@@ -3537,7 +3978,7 @@ async function compileSkillPlugin(options) {
3537
3978
  }
3538
3979
  const readme = generateReadme(skillName, frontmatter, metadata);
3539
3980
  await writeFile(path20.join(pluginDir, "README.md"), readme);
3540
- verbose(` Generated README.md`);
3981
+ verbose(" Generated README.md");
3541
3982
  return {
3542
3983
  pluginPath: pluginDir,
3543
3984
  manifest,
@@ -3546,7 +3987,7 @@ async function compileSkillPlugin(options) {
3546
3987
  }
3547
3988
  async function compileAllSkillPlugins(skillsDir, outputDir) {
3548
3989
  const results = [];
3549
- const skillMdFiles = await glob("**/SKILL.md", skillsDir);
3990
+ const skillMdFiles = await glob(`**/${STANDARD_FILES.SKILL_MD}`, skillsDir);
3550
3991
  for (const skillMdFile of skillMdFiles) {
3551
3992
  const skillPath = path20.join(skillsDir, path20.dirname(skillMdFile));
3552
3993
  try {
@@ -3555,29 +3996,27 @@ async function compileAllSkillPlugins(skillsDir, outputDir) {
3555
3996
  outputDir
3556
3997
  });
3557
3998
  results.push(result);
3558
- console.log(` [OK] ${result.skillName}`);
3999
+ log(` [OK] ${result.skillName}`);
3559
4000
  } catch (error) {
3560
- const errorMessage = error instanceof Error ? error.message : String(error);
4001
+ const errorMessage = getErrorMessage(error);
3561
4002
  const dirBasename = path20.basename(skillPath);
3562
- console.warn(` [WARN] Failed to compile skill from ${dirBasename}: ${errorMessage}`);
4003
+ warn(`Failed to compile skill from '${dirBasename}': ${errorMessage}`);
3563
4004
  }
3564
4005
  }
3565
4006
  return results;
3566
4007
  }
3567
4008
  function printCompilationSummary(results) {
3568
- console.log(`
4009
+ log(`
3569
4010
  Compiled ${results.length} skill plugins:`);
3570
4011
  for (const result of results) {
3571
- console.log(` - ${result.skillName} (v${result.manifest.version})`);
4012
+ log(` - ${result.skillName} (v${result.manifest.version})`);
3572
4013
  }
3573
4014
  }
3574
4015
 
3575
4016
  // src/cli/lib/skills/local-skill-loader.ts
3576
4017
  init_esm_shims();
3577
- import { parse as parseYaml10 } from "yaml";
4018
+ import { parse as parseYaml8 } from "yaml";
3578
4019
  import path21 from "path";
3579
- var LOCAL_CATEGORY = "local";
3580
- var LOCAL_AUTHOR = "@local";
3581
4020
  async function discoverLocalSkills(projectDir) {
3582
4021
  const localSkillsPath = path21.join(projectDir, LOCAL_SKILLS_PATH);
3583
4022
  if (!await directoryExists(localSkillsPath)) {
@@ -3600,8 +4039,8 @@ async function discoverLocalSkills(projectDir) {
3600
4039
  }
3601
4040
  async function extractLocalSkill(localSkillsPath, skillDirName) {
3602
4041
  const skillDir = path21.join(localSkillsPath, skillDirName);
3603
- const metadataPath = path21.join(skillDir, "metadata.yaml");
3604
- const skillMdPath = path21.join(skillDir, "SKILL.md");
4042
+ const metadataPath = path21.join(skillDir, STANDARD_FILES.METADATA_YAML);
4043
+ const skillMdPath = path21.join(skillDir, STANDARD_FILES.SKILL_MD);
3605
4044
  if (!await fileExists(metadataPath)) {
3606
4045
  verbose(`Skipping local skill '${skillDirName}': No metadata.yaml found`);
3607
4046
  return null;
@@ -3611,30 +4050,32 @@ async function extractLocalSkill(localSkillsPath, skillDirName) {
3611
4050
  return null;
3612
4051
  }
3613
4052
  const metadataContent = await readFile(metadataPath);
3614
- const parsed = localRawMetadataSchema.safeParse(parseYaml10(metadataContent));
4053
+ const parsed = localRawMetadataSchema.safeParse(parseYaml8(metadataContent));
3615
4054
  if (!parsed.success) {
3616
4055
  warn(
3617
- `Skipping local skill '${skillDirName}': Invalid metadata.yaml \u2014 ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
4056
+ `Skipping local skill '${skillDirName}': invalid metadata.yaml \u2014 ${formatZodErrors(parsed.error.issues)}`
3618
4057
  );
3619
4058
  return null;
3620
4059
  }
3621
4060
  const metadata = parsed.data;
3622
4061
  if (!metadata.cli_name) {
3623
- warn(`Skipping local skill '${skillDirName}': Missing required 'cli_name' in metadata.yaml`);
4062
+ warn(
4063
+ `Skipping local skill '${skillDirName}': missing required '${METADATA_KEYS.CLI_NAME}' in metadata.yaml`
4064
+ );
3624
4065
  return null;
3625
4066
  }
3626
4067
  const skillMdContent = await readFile(skillMdPath);
3627
4068
  const frontmatter = parseFrontmatter(skillMdContent, skillMdPath);
3628
4069
  if (!frontmatter) {
3629
- warn(`Skipping local skill '${skillDirName}': Invalid SKILL.md frontmatter`);
4070
+ warn(`Skipping local skill '${skillDirName}': invalid SKILL.md frontmatter`);
3630
4071
  return null;
3631
4072
  }
3632
4073
  const relativePath = `${LOCAL_SKILLS_PATH}/${skillDirName}/`;
3633
4074
  const skillId = frontmatter.name;
3634
- const category = metadata.category || LOCAL_CATEGORY;
4075
+ const category = metadata.category || LOCAL_DEFAULTS.CATEGORY;
3635
4076
  if (!metadata.category) {
3636
4077
  warn(
3637
- `Local skill '${skillDirName}' has no category in metadata.yaml \u2014 defaulting to '${LOCAL_CATEGORY}' (will not appear in wizard domain views)`
4078
+ `Local skill '${skillDirName}' has no category in metadata.yaml \u2014 defaulting to '${LOCAL_DEFAULTS.CATEGORY}' (will not appear in wizard domain views)`
3638
4079
  );
3639
4080
  }
3640
4081
  const extracted = {
@@ -3644,7 +4085,7 @@ async function extractLocalSkill(localSkillsPath, skillDirName) {
3644
4085
  usageGuidance: metadata.usage_guidance,
3645
4086
  category,
3646
4087
  categoryExclusive: metadata.category_exclusive ?? false,
3647
- author: LOCAL_AUTHOR,
4088
+ author: LOCAL_DEFAULTS.AUTHOR,
3648
4089
  tags: metadata.tags ?? [],
3649
4090
  compatibleWith: metadata.compatible_with ?? [],
3650
4091
  conflictsWith: metadata.conflicts_with ?? [],
@@ -3662,38 +4103,65 @@ async function extractLocalSkill(localSkillsPath, skillDirName) {
3662
4103
  // src/cli/lib/skills/source-switcher.ts
3663
4104
  init_esm_shims();
3664
4105
  import path22 from "path";
4106
+ function validateSkillId(skillId) {
4107
+ if (!SKILL_ID_PATTERN.test(skillId)) {
4108
+ return false;
4109
+ }
4110
+ return !(skillId.includes("\0") || skillId.includes("..") || skillId.includes("/") || skillId.includes("\\"));
4111
+ }
4112
+ function validatePathBoundary(resolvedPath, expectedParent) {
4113
+ const normalizedPath = path22.resolve(resolvedPath);
4114
+ const normalizedParent = path22.resolve(expectedParent);
4115
+ return normalizedPath.startsWith(normalizedParent + path22.sep);
4116
+ }
3665
4117
  async function archiveLocalSkill(projectDir, skillId) {
3666
- const skillPath = path22.join(projectDir, LOCAL_SKILLS_PATH, skillId);
3667
- const archivedDir = path22.join(projectDir, LOCAL_SKILLS_PATH, ARCHIVED_SKILLS_DIR_NAME);
3668
- const archivedSkillPath = path22.join(archivedDir, skillId);
3669
- if (!await directoryExists(skillPath)) {
3670
- warn(`Skill directory not found for archiving: ${skillPath}`);
4118
+ if (!validateSkillId(skillId)) {
4119
+ warn(`Invalid skill ID for archiving: '${skillId}'`);
4120
+ return;
4121
+ }
4122
+ const skillsDir = path22.resolve(path22.join(projectDir, LOCAL_SKILLS_PATH));
4123
+ const skillPath = path22.resolve(path22.join(skillsDir, skillId));
4124
+ const archivedDir = path22.resolve(path22.join(skillsDir, ARCHIVED_SKILLS_DIR_NAME));
4125
+ const archivedSkillPath = path22.resolve(path22.join(archivedDir, skillId));
4126
+ if (!validatePathBoundary(skillPath, skillsDir) || !validatePathBoundary(archivedSkillPath, archivedDir)) {
4127
+ warn(`Skill ID '${skillId}' resolves outside the skills directory.`);
4128
+ return;
4129
+ }
4130
+ try {
4131
+ await ensureDir(archivedDir);
4132
+ await copy(skillPath, archivedSkillPath);
4133
+ await remove(skillPath);
4134
+ } catch (error) {
4135
+ warn(`Failed to archive skill '${skillId}': ${getErrorMessage(error)}`);
3671
4136
  return;
3672
4137
  }
3673
- await ensureDir(archivedDir);
3674
- await copy(skillPath, archivedSkillPath);
3675
- await remove(skillPath);
3676
4138
  verbose(`Archived local skill '${skillId}' to ${ARCHIVED_SKILLS_DIR_NAME}/`);
3677
4139
  }
3678
4140
  async function restoreArchivedSkill(projectDir, skillId) {
3679
- const skillPath = path22.join(projectDir, LOCAL_SKILLS_PATH, skillId);
3680
- const archivedSkillPath = path22.join(
3681
- projectDir,
3682
- LOCAL_SKILLS_PATH,
3683
- ARCHIVED_SKILLS_DIR_NAME,
3684
- skillId
3685
- );
3686
- if (!await directoryExists(archivedSkillPath)) {
4141
+ if (!validateSkillId(skillId)) {
4142
+ warn(`Invalid skill ID for restoring: '${skillId}'`);
4143
+ return false;
4144
+ }
4145
+ const skillsDir = path22.resolve(path22.join(projectDir, LOCAL_SKILLS_PATH));
4146
+ const skillPath = path22.resolve(path22.join(skillsDir, skillId));
4147
+ const archivedDir = path22.resolve(path22.join(skillsDir, ARCHIVED_SKILLS_DIR_NAME));
4148
+ const archivedSkillPath = path22.resolve(path22.join(archivedDir, skillId));
4149
+ if (!validatePathBoundary(skillPath, skillsDir) || !validatePathBoundary(archivedSkillPath, archivedDir)) {
4150
+ warn(`Skill ID '${skillId}' resolves outside the skills directory.`);
4151
+ return false;
4152
+ }
4153
+ try {
4154
+ await copy(archivedSkillPath, skillPath);
4155
+ await remove(archivedSkillPath);
4156
+ } catch {
3687
4157
  return false;
3688
4158
  }
3689
- await copy(archivedSkillPath, skillPath);
3690
- await remove(archivedSkillPath);
3691
4159
  verbose(`Restored archived skill '${skillId}' from ${ARCHIVED_SKILLS_DIR_NAME}/`);
3692
4160
  return true;
3693
4161
  }
3694
4162
 
3695
4163
  // src/cli/lib/configuration/config-generator.ts
3696
- function extractSubcategory(categoryPath) {
4164
+ function extractSubcategoryFromPath(categoryPath) {
3697
4165
  if (categoryPath === "local") return void 0;
3698
4166
  const parts = categoryPath.split("/");
3699
4167
  return parts.length >= 2 ? parts[1] : parts[0];
@@ -3709,14 +4177,14 @@ function generateProjectConfigFromSkills(name, selectedSkillIds, matrix, options
3709
4177
  const skillPath = skill.path;
3710
4178
  const category = skill.category;
3711
4179
  const agents = getAgentsForSkill(skillPath, category);
3712
- const subcategory = extractSubcategory(category);
4180
+ const subcategory = extractSubcategoryFromPath(category);
3713
4181
  for (const agentId of agents) {
3714
4182
  neededAgents.add(agentId);
3715
4183
  if (subcategory) {
3716
4184
  if (!stackProperty[agentId]) {
3717
4185
  stackProperty[agentId] = {};
3718
4186
  }
3719
- stackProperty[agentId][subcategory] = skillId;
4187
+ stackProperty[agentId][subcategory] = [{ id: skillId, preloaded: false }];
3720
4188
  }
3721
4189
  }
3722
4190
  }
@@ -3736,23 +4204,18 @@ function generateProjectConfigFromSkills(name, selectedSkillIds, matrix, options
3736
4204
  }
3737
4205
  return config;
3738
4206
  }
3739
- function buildStackProperty(stack, displayNameToId) {
4207
+ function buildStackProperty(stack) {
3740
4208
  const result = {};
3741
4209
  for (const [agentId, agentConfig] of typedEntries(stack.agents)) {
3742
4210
  if (!agentConfig || Object.keys(agentConfig).length === 0) {
3743
4211
  continue;
3744
4212
  }
3745
4213
  const resolvedMappings = {};
3746
- for (const [subcategoryId, displayName] of typedEntries(
4214
+ for (const [subcategoryId, assignments] of typedEntries(
3747
4215
  agentConfig
3748
4216
  )) {
3749
- if (!displayName) continue;
3750
- const skillId = displayNameToId[displayName];
3751
- if (skillId) {
3752
- resolvedMappings[subcategoryId] = skillId;
3753
- } else {
3754
- resolvedMappings[subcategoryId] = displayName;
3755
- }
4217
+ if (!assignments || assignments.length === 0) continue;
4218
+ resolvedMappings[subcategoryId] = assignments;
3756
4219
  }
3757
4220
  if (Object.keys(resolvedMappings).length > 0) {
3758
4221
  result[agentId] = resolvedMappings;
@@ -3760,6 +4223,33 @@ function buildStackProperty(stack, displayNameToId) {
3760
4223
  }
3761
4224
  return result;
3762
4225
  }
4226
+ function compactStackForYaml(stack) {
4227
+ const result = {};
4228
+ for (const [agentId, agentConfig] of Object.entries(stack)) {
4229
+ const compacted = {};
4230
+ for (const [subcategory, assignments] of typedEntries(
4231
+ agentConfig
4232
+ )) {
4233
+ if (!assignments || assignments.length === 0) continue;
4234
+ if (assignments.length === 1) {
4235
+ const assignment = assignments[0];
4236
+ if (!assignment.preloaded) {
4237
+ compacted[subcategory] = assignment.id;
4238
+ } else {
4239
+ compacted[subcategory] = { id: assignment.id, preloaded: true };
4240
+ }
4241
+ } else {
4242
+ compacted[subcategory] = assignments.map(
4243
+ (a) => !a.preloaded ? a.id : { id: a.id, preloaded: true }
4244
+ );
4245
+ }
4246
+ }
4247
+ if (Object.keys(compacted).length > 0) {
4248
+ result[agentId] = compacted;
4249
+ }
4250
+ }
4251
+ return result;
4252
+ }
3763
4253
 
3764
4254
  // src/cli/lib/configuration/config-merger.ts
3765
4255
  init_esm_shims();
@@ -3768,7 +4258,6 @@ import { difference } from "remeda";
3768
4258
  // src/cli/lib/configuration/project-config.ts
3769
4259
  init_esm_shims();
3770
4260
  import path23 from "path";
3771
- import { parse as parseYaml11 } from "yaml";
3772
4261
  var CONFIG_PATH = `${CLAUDE_SRC_DIR}/config.yaml`;
3773
4262
  var LEGACY_CONFIG_PATH = `${CLAUDE_DIR}/config.yaml`;
3774
4263
  async function loadProjectConfig(projectDir) {
@@ -3784,38 +4273,29 @@ async function loadProjectConfig(projectDir) {
3784
4273
  return null;
3785
4274
  }
3786
4275
  }
3787
- try {
3788
- const content = await readFile(configPath);
3789
- const parsed = parseYaml11(content);
3790
- if (!parsed || typeof parsed !== "object") {
3791
- warn(`Invalid project config structure at ${configPath}`);
3792
- return null;
3793
- }
3794
- const result = projectConfigLoaderSchema.safeParse(parsed);
3795
- if (!result.success) {
3796
- warn(`Invalid project config at ${configPath}: ${result.error.message}`);
3797
- return null;
3798
- }
3799
- const config = result.data;
3800
- if (!config.name) {
3801
- warn(
3802
- `Project config at ${configPath} is missing required 'name' field \u2014 defaulting to directory name`
3803
- );
3804
- config.name = path23.basename(projectDir);
3805
- }
3806
- if (!config.skills) {
3807
- warn(`Project config at ${configPath} is missing 'skills' array \u2014 defaulting to empty`);
3808
- config.skills = [];
3809
- }
3810
- verbose(`Loaded project config from ${configPath}`);
3811
- return {
3812
- config,
3813
- configPath
3814
- };
3815
- } catch (error) {
3816
- warn(`Failed to parse project config at ${configPath}: ${error}`);
3817
- return null;
4276
+ const data = await safeLoadYamlFile(configPath, projectConfigLoaderSchema);
4277
+ if (!data) return null;
4278
+ const config = data;
4279
+ if (config.stack) {
4280
+ config.stack = normalizeStackRecord(
4281
+ config.stack
4282
+ );
4283
+ }
4284
+ if (!config.name) {
4285
+ warn(
4286
+ `Project config at '${configPath}' is missing required 'name' field \u2014 defaulting to directory name`
4287
+ );
4288
+ config.name = path23.basename(projectDir);
3818
4289
  }
4290
+ if (!config.skills) {
4291
+ warn(`Project config at '${configPath}' is missing 'skills' array \u2014 defaulting to empty`);
4292
+ config.skills = [];
4293
+ }
4294
+ verbose(`Loaded project config from ${configPath}`);
4295
+ return {
4296
+ config,
4297
+ configPath
4298
+ };
3819
4299
  }
3820
4300
  function validateProjectConfig(config) {
3821
4301
  const errors = [];
@@ -3899,42 +4379,22 @@ async function mergeWithExistingConfig(newConfig, context) {
3899
4379
 
3900
4380
  // src/cli/lib/configuration/config-saver.ts
3901
4381
  init_esm_shims();
3902
- import path24 from "path";
3903
- import { parse as parseYaml12, stringify as stringifyYaml5 } from "yaml";
3904
- var YAML_INDENT2 = 2;
3905
4382
  async function saveSourceToProjectConfig(projectDir, source) {
3906
- const configPath = path24.join(projectDir, CLAUDE_SRC_DIR, "config.yaml");
3907
- let config = {};
3908
- if (await fileExists(configPath)) {
3909
- const content = await readFile(configPath);
3910
- try {
3911
- const parsed = parseYaml12(content);
3912
- const result = projectSourceConfigSchema.safeParse(parsed);
3913
- config = result.success ? result.data : {};
3914
- if (!result.success) {
3915
- warn(
3916
- `Invalid config at ${configPath}: ${result.error.issues.map((i) => i.message).join(", ")}. Starting with empty config.`
3917
- );
3918
- }
3919
- } catch (error) {
3920
- warn(
3921
- `Failed to parse existing config at ${configPath}: ${error instanceof Error ? error.message : String(error)}. Starting with empty config.`
3922
- );
3923
- }
3924
- }
3925
- config.source = source;
3926
- await ensureDir(path24.join(projectDir, CLAUDE_SRC_DIR));
3927
- const configYaml = stringifyYaml5(config, { indent: YAML_INDENT2 });
3928
- await writeFile(configPath, configYaml);
4383
+ const existing = await loadProjectSourceConfig(projectDir) ?? {};
4384
+ await saveProjectConfig(projectDir, { ...existing, source });
3929
4385
  }
3930
4386
 
3931
4387
  // src/cli/lib/loading/source-fetcher.ts
4388
+ var SAFE_NAME_PATTERN2 = /^[a-zA-Z0-9@._/ -]+$/;
4389
+ var MAX_NAME_LENGTH = 200;
3932
4390
  function sanitizeSourceForCache(source) {
3933
- return source.replace(/:/g, "-").replace(/[\/]/g, "-").replace(/--+/g, "-").replace(/^-|-$/g, "");
4391
+ const hash = createHash2("sha256").update(source).digest("hex").slice(0, CACHE_HASH_LENGTH);
4392
+ const readable = source.replace(/[^a-zA-Z0-9]/g, "-").replace(/--+/g, "-").replace(/^-|-$/g, "").slice(0, CACHE_READABLE_PREFIX_LENGTH);
4393
+ return readable ? `${readable}-${hash}` : hash;
3934
4394
  }
3935
4395
  function getCacheDir(source) {
3936
- const sanitized = sanitizeSourceForCache(source);
3937
- return path25.join(CACHE_DIR, "sources", sanitized);
4396
+ const sanitized = sanitizeSourceForCache(source) || "unknown";
4397
+ return path24.join(CACHE_DIR, "sources", sanitized);
3938
4398
  }
3939
4399
  async function fetchFromSource(source, options = {}) {
3940
4400
  const { forceRefresh = false, subdir } = options;
@@ -3944,10 +4404,10 @@ async function fetchFromSource(source, options = {}) {
3944
4404
  return fetchFromRemoteSource(source, { forceRefresh, subdir });
3945
4405
  }
3946
4406
  async function fetchFromLocalSource(source, subdir) {
3947
- const fullPath = subdir ? path25.join(source, subdir) : source;
3948
- const absolutePath = path25.isAbsolute(fullPath) ? fullPath : path25.resolve(process.cwd(), fullPath);
4407
+ const fullPath = subdir ? path24.join(source, subdir) : source;
4408
+ const absolutePath = path24.isAbsolute(fullPath) ? fullPath : path24.resolve(process.cwd(), fullPath);
3949
4409
  if (!await directoryExists(absolutePath)) {
3950
- throw new Error(`Local source not found: ${absolutePath}`);
4410
+ throw new Error(`Local source not found: '${absolutePath}'`);
3951
4411
  }
3952
4412
  verbose(`Using local source: ${absolutePath}`);
3953
4413
  return {
@@ -3970,7 +4430,7 @@ async function fetchFromRemoteSource(source, options) {
3970
4430
  source: fullSource
3971
4431
  };
3972
4432
  }
3973
- await ensureDir(path25.dirname(cacheDir));
4433
+ await ensureDir(path24.dirname(cacheDir));
3974
4434
  try {
3975
4435
  const result = await downloadTemplate(fullSource, {
3976
4436
  dir: cacheDir,
@@ -3985,11 +4445,11 @@ async function fetchFromRemoteSource(source, options) {
3985
4445
  source: fullSource
3986
4446
  };
3987
4447
  } catch (error) {
3988
- throw wrapGigetError(error, source);
4448
+ throw createDetailedFetchError(error, source);
3989
4449
  }
3990
4450
  }
3991
- function wrapGigetError(error, source) {
3992
- const message = error instanceof Error ? error.message : String(error);
4451
+ function createDetailedFetchError(error, source) {
4452
+ const message = getErrorMessage(error);
3993
4453
  if (message.includes("404") || message.includes("Not Found")) {
3994
4454
  return new Error(
3995
4455
  `Repository not found: ${source}
@@ -4040,25 +4500,71 @@ async function fetchMarketplace(source, options = {}) {
4040
4500
  subdir: ""
4041
4501
  // Root of repo
4042
4502
  });
4043
- const marketplacePath = path25.join(result.path, ".claude-plugin", "marketplace.json");
4044
- if (!await directoryExists(path25.dirname(marketplacePath))) {
4503
+ const marketplacePath = path24.join(result.path, ".claude-plugin", "marketplace.json");
4504
+ if (!await directoryExists(path24.dirname(marketplacePath))) {
4045
4505
  throw new Error(
4046
- `Marketplace not found at: ${marketplacePath}
4506
+ `Marketplace not found for source: ${source}
4507
+
4508
+ The .claude-plugin/marketplace.json file is missing from this repository.
4047
4509
 
4048
- Expected .claude-plugin/marketplace.json in the source repository.`
4510
+ Possible causes:
4511
+ - The source URL may be incorrect
4512
+ - The repository may not have a marketplace configured
4513
+
4514
+ To create a marketplace, add a .claude-plugin/marketplace.json file to your source repository.`
4049
4515
  );
4050
4516
  }
4051
- const content = await readFile(marketplacePath);
4517
+ const content = await readFileSafe(marketplacePath, MAX_MARKETPLACE_FILE_SIZE);
4052
4518
  const parsed = JSON.parse(content);
4519
+ if (!validateNestingDepth(parsed, MAX_JSON_NESTING_DEPTH)) {
4520
+ throw new Error(
4521
+ `Invalid marketplace.json at: ${marketplacePath}
4522
+
4523
+ JSON structure exceeds maximum nesting depth of ${MAX_JSON_NESTING_DEPTH}.`
4524
+ );
4525
+ }
4053
4526
  const validation = marketplaceSchema.safeParse(parsed);
4054
4527
  if (!validation.success) {
4055
4528
  throw new Error(
4056
4529
  `Invalid marketplace.json at: ${marketplacePath}
4057
4530
 
4058
- Validation errors: ${validation.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
4531
+ Validation errors: ${formatZodErrors(validation.error.issues)}`
4059
4532
  );
4060
4533
  }
4061
4534
  const marketplace = validation.data;
4535
+ const EXPECTED_MARKETPLACE_KEYS = [
4536
+ "$schema",
4537
+ "name",
4538
+ "version",
4539
+ "description",
4540
+ "owner",
4541
+ "metadata",
4542
+ "plugins"
4543
+ ];
4544
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
4545
+ warnUnknownFields(
4546
+ parsed,
4547
+ EXPECTED_MARKETPLACE_KEYS,
4548
+ "marketplace.json"
4549
+ );
4550
+ }
4551
+ if (marketplace.plugins.length > MAX_MARKETPLACE_PLUGINS) {
4552
+ throw new Error(
4553
+ `Invalid marketplace.json at: ${marketplacePath}
4554
+
4555
+ Too many plugins: ${marketplace.plugins.length} (limit: ${MAX_MARKETPLACE_PLUGINS}).`
4556
+ );
4557
+ }
4558
+ for (const plugin of marketplace.plugins) {
4559
+ if (plugin.name.length > MAX_NAME_LENGTH) {
4560
+ warn(
4561
+ `Marketplace plugin name too long (${plugin.name.length} chars): '${plugin.name.slice(0, 50)}...'`
4562
+ );
4563
+ }
4564
+ if (!SAFE_NAME_PATTERN2.test(plugin.name)) {
4565
+ warn(`Marketplace plugin name contains unsafe characters: '${plugin.name.slice(0, 50)}'`);
4566
+ }
4567
+ }
4062
4568
  verbose(`Loaded marketplace: ${marketplace.name} v${marketplace.version}`);
4063
4569
  return {
4064
4570
  marketplace,
@@ -4140,6 +4646,8 @@ export {
4140
4646
  generateAgentPluginManifest,
4141
4647
  writePluginManifest,
4142
4648
  findPluginManifest,
4649
+ typedEntries,
4650
+ typedKeys,
4143
4651
  getCollectivePluginDir,
4144
4652
  getProjectPluginsDir,
4145
4653
  getPluginSkillsDir,
@@ -4156,13 +4664,15 @@ export {
4156
4664
  formatOrigin,
4157
4665
  resolveAuthor,
4158
4666
  resolveAllSources,
4667
+ IMPORT_DEFAULTS,
4668
+ LOCAL_DEFAULTS,
4159
4669
  getCurrentDate,
4160
- hashString,
4161
- hashFile,
4670
+ computeStringHash,
4671
+ computeFileHash,
4162
4672
  determinePluginVersion,
4163
4673
  writeContentHash,
4164
4674
  readForkedFromMetadata,
4165
- compareSkills,
4675
+ compareLocalSkillsWithSource,
4166
4676
  injectForkedFromMetadata,
4167
4677
  copySkillsToPluginFromSource,
4168
4678
  copySkillsToLocalFlattened,
@@ -4175,7 +4685,9 @@ export {
4175
4685
  getAvailableSkills,
4176
4686
  fetchFromSource,
4177
4687
  searchExtraSources,
4688
+ normalizeStackRecord,
4178
4689
  loadStacks,
4690
+ getStackSkillIds,
4179
4691
  buildSkillRefsFromConfig,
4180
4692
  resolveAgents,
4181
4693
  extractFrontmatter,
@@ -4190,6 +4702,7 @@ export {
4190
4702
  claudePluginUninstall,
4191
4703
  installStackAsPlugin,
4192
4704
  loadSkillsMatrixFromSource,
4705
+ getMarketplaceLabel,
4193
4706
  compileSkillPlugin,
4194
4707
  compileAllSkillPlugins,
4195
4708
  printCompilationSummary,
@@ -4212,4 +4725,4 @@ export {
4212
4725
  validateAllPlugins,
4213
4726
  printPluginValidationResult
4214
4727
  };
4215
- //# sourceMappingURL=chunk-CQ7657GA.js.map
4728
+ //# sourceMappingURL=chunk-SSHG7MEE.js.map