@sporesec/arcana 2.4.0 → 3.0.1

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 (247) hide show
  1. package/dist/cli.d.ts +0 -1
  2. package/dist/cli.js +124 -9
  3. package/dist/command-registry.d.ts +10 -0
  4. package/dist/command-registry.js +65 -0
  5. package/dist/commands/audit.d.ts +2 -3
  6. package/dist/commands/audit.js +47 -14
  7. package/dist/commands/benchmark.d.ts +4 -0
  8. package/dist/commands/benchmark.js +178 -0
  9. package/dist/commands/clean.d.ts +0 -1
  10. package/dist/commands/clean.js +19 -8
  11. package/dist/commands/compact.d.ts +2 -1
  12. package/dist/commands/compact.js +74 -14
  13. package/dist/commands/completions.d.ts +3 -0
  14. package/dist/commands/completions.js +104 -0
  15. package/dist/commands/config.d.ts +0 -1
  16. package/dist/commands/config.js +15 -6
  17. package/dist/commands/create.d.ts +0 -1
  18. package/dist/commands/create.js +1 -1
  19. package/dist/commands/diff.d.ts +4 -0
  20. package/dist/commands/diff.js +166 -0
  21. package/dist/commands/doctor.d.ts +0 -1
  22. package/dist/commands/doctor.js +64 -23
  23. package/dist/commands/export-cmd.d.ts +4 -0
  24. package/dist/commands/export-cmd.js +66 -0
  25. package/dist/commands/import-cmd.d.ts +4 -0
  26. package/dist/commands/import-cmd.js +131 -0
  27. package/dist/commands/info.d.ts +0 -1
  28. package/dist/commands/info.js +29 -4
  29. package/dist/commands/init.d.ts +0 -1
  30. package/dist/commands/init.js +26 -33
  31. package/dist/commands/install.d.ts +1 -1
  32. package/dist/commands/install.js +118 -205
  33. package/dist/commands/list.d.ts +0 -1
  34. package/dist/commands/list.js +12 -4
  35. package/dist/commands/lock.d.ts +4 -0
  36. package/dist/commands/lock.js +171 -0
  37. package/dist/commands/optimize.d.ts +0 -1
  38. package/dist/commands/optimize.js +111 -20
  39. package/dist/commands/outdated.d.ts +4 -0
  40. package/dist/commands/outdated.js +159 -0
  41. package/dist/commands/profile.d.ts +3 -0
  42. package/dist/commands/profile.js +274 -0
  43. package/dist/commands/providers.d.ts +0 -1
  44. package/dist/commands/providers.js +1 -4
  45. package/dist/commands/recommend.d.ts +5 -0
  46. package/dist/commands/recommend.js +96 -0
  47. package/dist/commands/scan.d.ts +0 -1
  48. package/dist/commands/scan.js +13 -7
  49. package/dist/commands/search.d.ts +2 -1
  50. package/dist/commands/search.js +32 -9
  51. package/dist/commands/stats.d.ts +0 -1
  52. package/dist/commands/stats.js +24 -20
  53. package/dist/commands/team.d.ts +3 -0
  54. package/dist/commands/team.js +291 -0
  55. package/dist/commands/uninstall.d.ts +0 -1
  56. package/dist/commands/uninstall.js +18 -4
  57. package/dist/commands/update.d.ts +0 -1
  58. package/dist/commands/update.js +155 -155
  59. package/dist/commands/validate.d.ts +3 -1
  60. package/dist/commands/validate.js +90 -15
  61. package/dist/commands/verify.d.ts +4 -0
  62. package/dist/commands/verify.js +116 -0
  63. package/dist/constants.d.ts +10 -0
  64. package/dist/constants.js +13 -0
  65. package/dist/index.d.ts +0 -1
  66. package/dist/index.js +0 -1
  67. package/dist/interactive/browse.d.ts +4 -0
  68. package/dist/interactive/browse.js +103 -0
  69. package/dist/interactive/categories.d.ts +4 -0
  70. package/dist/interactive/categories.js +87 -0
  71. package/dist/interactive/health.d.ts +1 -0
  72. package/dist/interactive/health.js +57 -0
  73. package/dist/interactive/helpers.d.ts +11 -0
  74. package/dist/interactive/helpers.js +66 -0
  75. package/dist/interactive/index.d.ts +1 -0
  76. package/dist/interactive/index.js +1 -0
  77. package/dist/interactive/manage.d.ts +2 -0
  78. package/dist/interactive/manage.js +187 -0
  79. package/dist/interactive/menu.d.ts +1 -0
  80. package/dist/interactive/menu.js +107 -0
  81. package/dist/interactive/search.d.ts +2 -0
  82. package/dist/interactive/search.js +66 -0
  83. package/dist/interactive/setup.d.ts +2 -0
  84. package/dist/interactive/setup.js +48 -0
  85. package/dist/interactive/skill-detail.d.ts +5 -0
  86. package/dist/interactive/skill-detail.js +126 -0
  87. package/dist/interactive.d.ts +0 -1
  88. package/dist/interactive.js +89 -66
  89. package/dist/providers/arcana.d.ts +0 -1
  90. package/dist/providers/arcana.js +0 -1
  91. package/dist/providers/base.d.ts +0 -1
  92. package/dist/providers/base.js +0 -1
  93. package/dist/providers/github.d.ts +0 -1
  94. package/dist/providers/github.js +8 -3
  95. package/dist/registry.d.ts +0 -1
  96. package/dist/registry.js +1 -4
  97. package/dist/types.d.ts +10 -1
  98. package/dist/types.js +0 -1
  99. package/dist/utils/atomic.d.ts +0 -1
  100. package/dist/utils/atomic.js +3 -2
  101. package/dist/utils/cache.d.ts +0 -1
  102. package/dist/utils/cache.js +3 -2
  103. package/dist/utils/config.d.ts +2 -1
  104. package/dist/utils/config.js +30 -5
  105. package/dist/utils/conflict-check.d.ts +8 -0
  106. package/dist/utils/conflict-check.js +72 -0
  107. package/dist/utils/errors.d.ts +0 -1
  108. package/dist/utils/errors.js +0 -1
  109. package/dist/utils/frontmatter.d.ts +0 -1
  110. package/dist/utils/frontmatter.js +44 -14
  111. package/dist/utils/fs.d.ts +0 -1
  112. package/dist/utils/fs.js +30 -11
  113. package/dist/utils/help.d.ts +0 -1
  114. package/dist/utils/help.js +15 -28
  115. package/dist/utils/history.d.ts +0 -1
  116. package/dist/utils/history.js +0 -1
  117. package/dist/utils/http.d.ts +0 -1
  118. package/dist/utils/http.js +14 -5
  119. package/dist/utils/install-core.d.ts +48 -0
  120. package/dist/utils/install-core.js +108 -0
  121. package/dist/utils/integrity.d.ts +17 -0
  122. package/dist/utils/integrity.js +84 -0
  123. package/dist/utils/parallel.d.ts +0 -1
  124. package/dist/utils/parallel.js +0 -1
  125. package/dist/utils/project-context.d.ts +19 -0
  126. package/dist/utils/project-context.js +283 -0
  127. package/dist/utils/quality.d.ts +27 -0
  128. package/dist/utils/quality.js +174 -0
  129. package/dist/utils/scanner.d.ts +0 -1
  130. package/dist/utils/scanner.js +138 -10
  131. package/dist/utils/scoring.d.ts +10 -0
  132. package/dist/utils/scoring.js +84 -0
  133. package/dist/utils/ui.d.ts +0 -1
  134. package/dist/utils/ui.js +11 -4
  135. package/dist/utils/validate.d.ts +0 -1
  136. package/dist/utils/validate.js +4 -1
  137. package/package.json +74 -62
  138. package/dist/cli.d.ts.map +0 -1
  139. package/dist/cli.js.map +0 -1
  140. package/dist/commands/audit.d.ts.map +0 -1
  141. package/dist/commands/audit.js.map +0 -1
  142. package/dist/commands/audit.test.d.ts +0 -2
  143. package/dist/commands/audit.test.d.ts.map +0 -1
  144. package/dist/commands/audit.test.js +0 -217
  145. package/dist/commands/audit.test.js.map +0 -1
  146. package/dist/commands/clean.d.ts.map +0 -1
  147. package/dist/commands/clean.js.map +0 -1
  148. package/dist/commands/compact.d.ts.map +0 -1
  149. package/dist/commands/compact.js.map +0 -1
  150. package/dist/commands/config.d.ts.map +0 -1
  151. package/dist/commands/config.js.map +0 -1
  152. package/dist/commands/create.d.ts.map +0 -1
  153. package/dist/commands/create.js.map +0 -1
  154. package/dist/commands/doctor.d.ts.map +0 -1
  155. package/dist/commands/doctor.js.map +0 -1
  156. package/dist/commands/info.d.ts.map +0 -1
  157. package/dist/commands/info.js.map +0 -1
  158. package/dist/commands/init.d.ts.map +0 -1
  159. package/dist/commands/init.js.map +0 -1
  160. package/dist/commands/install.d.ts.map +0 -1
  161. package/dist/commands/install.js.map +0 -1
  162. package/dist/commands/list.d.ts.map +0 -1
  163. package/dist/commands/list.js.map +0 -1
  164. package/dist/commands/optimize.d.ts.map +0 -1
  165. package/dist/commands/optimize.js.map +0 -1
  166. package/dist/commands/providers.d.ts.map +0 -1
  167. package/dist/commands/providers.js.map +0 -1
  168. package/dist/commands/scan.d.ts.map +0 -1
  169. package/dist/commands/scan.js.map +0 -1
  170. package/dist/commands/search.d.ts.map +0 -1
  171. package/dist/commands/search.js.map +0 -1
  172. package/dist/commands/stats.d.ts.map +0 -1
  173. package/dist/commands/stats.js.map +0 -1
  174. package/dist/commands/uninstall.d.ts.map +0 -1
  175. package/dist/commands/uninstall.js.map +0 -1
  176. package/dist/commands/update.d.ts.map +0 -1
  177. package/dist/commands/update.js.map +0 -1
  178. package/dist/commands/validate.d.ts.map +0 -1
  179. package/dist/commands/validate.js.map +0 -1
  180. package/dist/index.d.ts.map +0 -1
  181. package/dist/index.js.map +0 -1
  182. package/dist/interactive.d.ts.map +0 -1
  183. package/dist/interactive.js.map +0 -1
  184. package/dist/providers/arcana.d.ts.map +0 -1
  185. package/dist/providers/arcana.js.map +0 -1
  186. package/dist/providers/base.d.ts.map +0 -1
  187. package/dist/providers/base.js.map +0 -1
  188. package/dist/providers/github.d.ts.map +0 -1
  189. package/dist/providers/github.js.map +0 -1
  190. package/dist/registry.d.ts.map +0 -1
  191. package/dist/registry.js.map +0 -1
  192. package/dist/types.d.ts.map +0 -1
  193. package/dist/types.js.map +0 -1
  194. package/dist/utils/atomic.d.ts.map +0 -1
  195. package/dist/utils/atomic.js.map +0 -1
  196. package/dist/utils/atomic.test.d.ts +0 -2
  197. package/dist/utils/atomic.test.d.ts.map +0 -1
  198. package/dist/utils/atomic.test.js +0 -31
  199. package/dist/utils/atomic.test.js.map +0 -1
  200. package/dist/utils/cache.d.ts.map +0 -1
  201. package/dist/utils/cache.js.map +0 -1
  202. package/dist/utils/config.d.ts.map +0 -1
  203. package/dist/utils/config.js.map +0 -1
  204. package/dist/utils/config.test.d.ts +0 -2
  205. package/dist/utils/config.test.d.ts.map +0 -1
  206. package/dist/utils/config.test.js +0 -38
  207. package/dist/utils/config.test.js.map +0 -1
  208. package/dist/utils/errors.d.ts.map +0 -1
  209. package/dist/utils/errors.js.map +0 -1
  210. package/dist/utils/frontmatter.d.ts.map +0 -1
  211. package/dist/utils/frontmatter.js.map +0 -1
  212. package/dist/utils/frontmatter.test.d.ts +0 -2
  213. package/dist/utils/frontmatter.test.d.ts.map +0 -1
  214. package/dist/utils/frontmatter.test.js +0 -152
  215. package/dist/utils/frontmatter.test.js.map +0 -1
  216. package/dist/utils/fs.d.ts.map +0 -1
  217. package/dist/utils/fs.js.map +0 -1
  218. package/dist/utils/fs.test.d.ts +0 -2
  219. package/dist/utils/fs.test.d.ts.map +0 -1
  220. package/dist/utils/fs.test.js +0 -145
  221. package/dist/utils/fs.test.js.map +0 -1
  222. package/dist/utils/help.d.ts.map +0 -1
  223. package/dist/utils/help.js.map +0 -1
  224. package/dist/utils/help.test.d.ts +0 -2
  225. package/dist/utils/help.test.d.ts.map +0 -1
  226. package/dist/utils/help.test.js +0 -66
  227. package/dist/utils/help.test.js.map +0 -1
  228. package/dist/utils/history.d.ts.map +0 -1
  229. package/dist/utils/history.js.map +0 -1
  230. package/dist/utils/http.d.ts.map +0 -1
  231. package/dist/utils/http.js.map +0 -1
  232. package/dist/utils/http.test.d.ts +0 -2
  233. package/dist/utils/http.test.d.ts.map +0 -1
  234. package/dist/utils/http.test.js +0 -55
  235. package/dist/utils/http.test.js.map +0 -1
  236. package/dist/utils/parallel.d.ts.map +0 -1
  237. package/dist/utils/parallel.js.map +0 -1
  238. package/dist/utils/scanner.d.ts.map +0 -1
  239. package/dist/utils/scanner.js.map +0 -1
  240. package/dist/utils/ui.d.ts.map +0 -1
  241. package/dist/utils/ui.js.map +0 -1
  242. package/dist/utils/ui.test.d.ts +0 -2
  243. package/dist/utils/ui.test.d.ts.map +0 -1
  244. package/dist/utils/ui.test.js +0 -31
  245. package/dist/utils/ui.test.js.map +0 -1
  246. package/dist/utils/validate.d.ts.map +0 -1
  247. package/dist/utils/validate.js.map +0 -1
@@ -8,4 +8,3 @@ export class CliError extends Error {
8
8
  this.name = "CliError";
9
9
  }
10
10
  }
11
- //# sourceMappingURL=errors.js.map
@@ -9,4 +9,3 @@ export declare function extractFrontmatter(content: string): {
9
9
  export declare function parseFrontmatter(raw: string): SkillFrontmatter | null;
10
10
  export declare function fixSkillFrontmatter(content: string): string;
11
11
  export declare function validateSkillDir(skillDir: string, skillName: string): ValidationResult;
12
- //# sourceMappingURL=frontmatter.d.ts.map
@@ -40,7 +40,13 @@ export function parseFrontmatter(raw) {
40
40
  if (descMatch?.[1] !== undefined) {
41
41
  let value = descMatch[1].trim();
42
42
  // Handle YAML multiline: |, >, or bare indented continuation
43
- if (value === "|" || value === ">" || value === "|-" || value === "|+" || value === ">-" || value === ">+" || value === "") {
43
+ if (value === "|" ||
44
+ value === ">" ||
45
+ value === "|-" ||
46
+ value === "|+" ||
47
+ value === ">-" ||
48
+ value === ">+" ||
49
+ value === "") {
44
50
  const multilineLines = [];
45
51
  for (let j = i + 1; j < lines.length; j++) {
46
52
  const next = lines[j];
@@ -93,12 +99,7 @@ export function fixSkillFrontmatter(content) {
93
99
  if (!parsed)
94
100
  return content;
95
101
  // Rebuild clean frontmatter with only name and description
96
- const cleanFm = [
97
- FM_DELIMITER,
98
- `name: ${parsed.name}`,
99
- `description: ${parsed.description}`,
100
- FM_DELIMITER,
101
- ].join("\n");
102
+ const cleanFm = [FM_DELIMITER, `name: ${parsed.name}`, `description: ${parsed.description}`, FM_DELIMITER].join("\n");
102
103
  return cleanFm + "\n" + extracted.body.replace(/^\n+/, "\n");
103
104
  }
104
105
  export function validateSkillDir(skillDir, skillName) {
@@ -137,20 +138,40 @@ export function validateSkillDir(skillDir, skillName) {
137
138
  return result;
138
139
  }
139
140
  if (!parsed.description) {
140
- result.warnings.push("Missing description in frontmatter");
141
+ result.valid = false;
142
+ result.errors.push("Missing description in frontmatter");
141
143
  }
142
144
  else if (parsed.description.length < MIN_DESC_LENGTH) {
143
- result.warnings.push(`Description too short (${parsed.description.length} chars, recommend ${MIN_DESC_LENGTH}+)`);
145
+ result.valid = false;
146
+ result.errors.push(`Description too short (${parsed.description.length} chars, minimum ${MIN_DESC_LENGTH})`);
144
147
  }
145
148
  else if (parsed.description.length > MAX_DESC_LENGTH) {
146
- result.warnings.push(`Description too long (${parsed.description.length} chars, max ${MAX_DESC_LENGTH})`);
149
+ result.valid = false;
150
+ result.errors.push(`Description too long (${parsed.description.length} chars, max ${MAX_DESC_LENGTH})`);
147
151
  }
148
- // Check for non-standard fields
152
+ // Check for non-standard fields (metadata is invalid per spec)
149
153
  const standardFields = ["name", "description"];
154
+ const VALID_FIELDS = [
155
+ "name",
156
+ "description",
157
+ "argument-hint",
158
+ "compatibility",
159
+ "disable-model-invocation",
160
+ "license",
161
+ "user-invokable",
162
+ ];
150
163
  for (const line of extracted.raw.split("\n")) {
151
164
  const keyMatch = line.match(/^(\w[\w-]*):/);
152
165
  if (keyMatch?.[1] && !standardFields.includes(keyMatch[1])) {
153
- result.infos.push(`Non-standard field: ${keyMatch[1]}`);
166
+ if (keyMatch[1] === "metadata") {
167
+ result.warnings.push("Invalid field: metadata (not allowed in frontmatter)");
168
+ }
169
+ else if (!VALID_FIELDS.includes(keyMatch[1])) {
170
+ result.infos.push(`Non-standard field: ${keyMatch[1]}`);
171
+ }
172
+ else {
173
+ result.infos.push(`Optional field: ${keyMatch[1]}`);
174
+ }
154
175
  }
155
176
  }
156
177
  if (parsed.name !== skillName) {
@@ -163,10 +184,19 @@ export function validateSkillDir(skillDir, skillName) {
163
184
  result.warnings.push("SKILL.md body is very short (less than 50 chars)");
164
185
  }
165
186
  if (extracted.body.trim().length >= 50 && !extracted.body.includes("##")) {
166
- result.infos.push("Body has no ## headings (recommended for structure)");
187
+ result.warnings.push("Body has no ## headings (required for structure)");
188
+ }
189
+ // Check for code blocks (quality signal)
190
+ if (extracted.body.trim().length >= 50 && !extracted.body.includes("```")) {
191
+ result.warnings.push("No code blocks found (skills must include code examples)");
192
+ }
193
+ // Check for BAD/GOOD pattern examples
194
+ const hasPattern = /(?:BAD|GOOD|WRONG|RIGHT|AVOID|PREFER|DO NOT|INSTEAD)/i.test(extracted.body) ||
195
+ /<!--\s*(?:bad|good)\s*-->/i.test(extracted.body);
196
+ if (extracted.body.trim().length >= 100 && !hasPattern) {
197
+ result.infos.push("No BAD/GOOD contrast patterns found (recommended for teaching)");
167
198
  }
168
199
  if (result.errors.length > 0)
169
200
  result.valid = false;
170
201
  return result;
171
202
  }
172
- //# sourceMappingURL=frontmatter.js.map
@@ -32,4 +32,3 @@ export declare function listFilesByAge(dir: string, ext: string, olderThanDays:
32
32
  */
33
33
  export declare function isOrphanedProject(projectDirName: string): boolean;
34
34
  export declare function listSymlinks(): SymlinkInfo[];
35
- //# sourceMappingURL=fs.d.ts.map
package/dist/utils/fs.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, renameSync, lstatSync, readlinkSync } from "node:fs";
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, renameSync, lstatSync, readlinkSync, } from "node:fs";
2
2
  import { join, dirname, resolve, sep } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { loadConfig } from "../utils/config.js";
@@ -26,10 +26,14 @@ export function getDirSize(dir) {
26
26
  else
27
27
  size += stat.size;
28
28
  }
29
- catch { /* skip unreadable entries */ }
29
+ catch {
30
+ /* skip unreadable entries */
31
+ }
30
32
  }
31
33
  }
32
- catch { /* skip unreadable dirs */ }
34
+ catch {
35
+ /* skip unreadable dirs */
36
+ }
33
37
  }
34
38
  return size;
35
39
  }
@@ -45,14 +49,18 @@ export function installSkill(skillName, files) {
45
49
  try {
46
50
  for (const file of files) {
47
51
  // Reject paths containing .. before resolving
48
- if (file.path.includes("..") || file.path.includes("~") || file.path.startsWith("\\\\") || file.path.startsWith("//")) {
52
+ if (file.path.includes("..") ||
53
+ file.path.includes("~") ||
54
+ file.path.startsWith("\\\\") ||
55
+ file.path.startsWith("//")) {
49
56
  throw new Error(`Path traversal blocked: ${file.path}`);
50
57
  }
51
58
  const filePath = resolve(tempDir, file.path);
52
59
  // Normalize to lowercase on Windows for case-insensitive comparison
53
60
  const normalizedFile = process.platform === "win32" ? filePath.toLowerCase() : filePath;
54
61
  const normalizedTemp = process.platform === "win32" ? (tempDir + sep).toLowerCase() : tempDir + sep;
55
- if (!normalizedFile.startsWith(normalizedTemp) && normalizedFile !== (process.platform === "win32" ? tempDir.toLowerCase() : tempDir)) {
62
+ if (!normalizedFile.startsWith(normalizedTemp) &&
63
+ normalizedFile !== (process.platform === "win32" ? tempDir.toLowerCase() : tempDir)) {
56
64
  throw new Error(`Path traversal blocked: ${file.path}`);
57
65
  }
58
66
  const dir = dirname(filePath);
@@ -72,7 +80,9 @@ export function installSkill(skillName, files) {
72
80
  try {
73
81
  rmSync(tempDir, { recursive: true, force: true });
74
82
  }
75
- catch { /* best-effort */ }
83
+ catch {
84
+ /* best-effort */
85
+ }
76
86
  throw err;
77
87
  }
78
88
  return skillDir;
@@ -124,13 +134,21 @@ export function listFilesByAge(dir, ext, olderThanDays) {
124
134
  continue;
125
135
  const age = now - stat.mtimeMs;
126
136
  if (age > cutoff) {
127
- results.push({ path: full, sizeMB: stat.size / (1024 * 1024), daysOld: Math.floor(age / (24 * 60 * 60 * 1000)) });
137
+ results.push({
138
+ path: full,
139
+ sizeMB: stat.size / (1024 * 1024),
140
+ daysOld: Math.floor(age / (24 * 60 * 60 * 1000)),
141
+ });
128
142
  }
129
143
  }
130
- catch { /* skip */ }
144
+ catch {
145
+ /* skip */
146
+ }
131
147
  }
132
148
  }
133
- catch { /* skip */ }
149
+ catch {
150
+ /* skip */
151
+ }
134
152
  }
135
153
  return results;
136
154
  }
@@ -189,8 +207,9 @@ export function listSymlinks() {
189
207
  results.push({ name: entry, fullPath, target, broken: !existsSync(target) });
190
208
  }
191
209
  }
192
- catch { /* skip unreadable */ }
210
+ catch {
211
+ /* skip unreadable */
212
+ }
193
213
  }
194
214
  return results;
195
215
  }
196
- //# sourceMappingURL=fs.js.map
@@ -3,4 +3,3 @@ export declare function buildCustomHelp(version: string): string;
3
3
  export declare function isFirstRun(): boolean;
4
4
  export declare function markInitialized(): void;
5
5
  export declare function showWelcome(version: string): void;
6
- //# sourceMappingURL=help.d.ts.map
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
4
4
  import * as p from "@clack/prompts";
5
5
  import chalk from "chalk";
6
6
  import { ui } from "./ui.js";
7
+ import { getGroupedCommands } from "../command-registry.js";
7
8
  const noColor = !!(process.env.NO_COLOR || process.env.TERM === "dumb");
8
9
  function amberShade(hex, text) {
9
10
  if (noColor)
@@ -32,30 +33,12 @@ export function renderBanner() {
32
33
  }
33
34
  return BANNER_LINES.map((line, i) => ` ${amberShade(AMBER_HEXES[i], line)}`).join("\n");
34
35
  }
35
- const COMMAND_GROUPS = {
36
- "GETTING STARTED": [
37
- { cmd: "init", desc: "Initialize arcana in current project" },
38
- { cmd: "doctor", desc: "Check environment and diagnose issues" },
39
- ],
40
- SKILLS: [
41
- { cmd: "list", desc: "List available skills" },
42
- { cmd: "search <query>", desc: "Search across providers" },
43
- { cmd: "info <skill>", desc: "Show skill details" },
44
- { cmd: "install [skills...]", desc: "Install one or more skills" },
45
- { cmd: "update [skills...]", desc: "Update installed skills" },
46
- { cmd: "uninstall [skills...]", desc: "Remove one or more skills" },
47
- ],
48
- DEVELOPMENT: [
49
- { cmd: "create <name>", desc: "Create a new skill from template" },
50
- { cmd: "validate [skill]", desc: "Validate skill structure" },
51
- { cmd: "audit [skill]", desc: "Audit skill quality" },
52
- ],
53
- CONFIGURATION: [
54
- { cmd: "config [key] [val]", desc: "View or modify configuration" },
55
- { cmd: "providers", desc: "Manage skill providers" },
56
- { cmd: "clean", desc: "Remove orphaned data" },
57
- { cmd: "stats", desc: "Show session analytics" },
58
- ],
36
+ // Help groups: subset of registry for --help display (keeps output scannable)
37
+ const HELP_GROUPS = {
38
+ "GETTING STARTED": ["init", "doctor"],
39
+ SKILLS: ["list", "search", "info", "install", "update", "uninstall", "recommend"],
40
+ DEVELOPMENT: ["create", "validate", "audit"],
41
+ CONFIGURATION: ["config", "providers", "clean", "stats"],
59
42
  };
60
43
  const EXAMPLES = [
61
44
  "$ arcana install code-reviewer typescript golang",
@@ -74,11 +57,16 @@ export function buildCustomHelp(version) {
74
57
  lines.push("");
75
58
  lines.push(` ${ui.dim("USAGE")}`);
76
59
  lines.push(" arcana <command> [options]");
77
- for (const [group, commands] of Object.entries(COMMAND_GROUPS)) {
60
+ const allCommands = getGroupedCommands();
61
+ const allFlat = Object.values(allCommands).flat();
62
+ for (const [group, names] of Object.entries(HELP_GROUPS)) {
78
63
  lines.push("");
79
64
  lines.push(` ${ui.dim(group)}`);
80
- for (const { cmd, desc } of commands) {
81
- lines.push(` ${ui.cyan(padRight(cmd, 22))}${ui.dim(desc)}`);
65
+ for (const name of names) {
66
+ const entry = allFlat.find((c) => c.name === name);
67
+ if (!entry)
68
+ continue;
69
+ lines.push(` ${ui.cyan(padRight(entry.usage, 22))}${ui.dim(entry.description)}`);
82
70
  }
83
71
  }
84
72
  lines.push("");
@@ -114,4 +102,3 @@ export function showWelcome(version) {
114
102
  p.log.info("They install on-demand and only load when relevant, not all at once.");
115
103
  console.log();
116
104
  }
117
- //# sourceMappingURL=help.js.map
@@ -7,4 +7,3 @@ export declare function readHistory(): HistoryEntry[];
7
7
  export declare function appendHistory(action: string, target?: string): void;
8
8
  export declare function clearHistory(): void;
9
9
  export declare function getRecentSkills(limit?: number): string[];
10
- //# sourceMappingURL=history.d.ts.map
@@ -55,4 +55,3 @@ export function getRecentSkills(limit = 5) {
55
55
  }
56
56
  return skills;
57
57
  }
58
- //# sourceMappingURL=history.js.map
@@ -14,4 +14,3 @@ export declare class RateLimitError extends HttpError {
14
14
  constructor(url: string, resetAt: Date | null);
15
15
  }
16
16
  export declare function httpGet(url: string, timeout?: number): Promise<HttpResponse>;
17
- //# sourceMappingURL=http.d.ts.map
@@ -102,11 +102,15 @@ function doGet(url, timeout, redirectCount = 0) {
102
102
  if (token) {
103
103
  try {
104
104
  const hostname = new URL(url).hostname;
105
- if (hostname === "github.com" || hostname.endsWith(".github.com") || hostname.endsWith(".githubusercontent.com")) {
105
+ if (hostname === "github.com" ||
106
+ hostname.endsWith(".github.com") ||
107
+ hostname.endsWith(".githubusercontent.com")) {
106
108
  headers["Authorization"] = `token ${token}`;
107
109
  }
108
110
  }
109
- catch { /* invalid URL, skip auth */ }
111
+ catch {
112
+ /* invalid URL, skip auth */
113
+ }
110
114
  }
111
115
  const req = https.get(url, { headers, timeout, agent }, (res) => {
112
116
  // Follow redirects (HTTPS only)
@@ -123,8 +127,14 @@ function doGet(url, timeout, redirectCount = 0) {
123
127
  // After the existing https check, add:
124
128
  try {
125
129
  const redirectUrl = new URL(location);
126
- const allowedHosts = ["github.com", "raw.githubusercontent.com", "api.github.com", "objects.githubusercontent.com", "registry.npmjs.org"];
127
- if (!allowedHosts.some(h => redirectUrl.hostname === h || redirectUrl.hostname.endsWith("." + h))) {
130
+ const allowedHosts = [
131
+ "github.com",
132
+ "raw.githubusercontent.com",
133
+ "api.github.com",
134
+ "objects.githubusercontent.com",
135
+ "registry.npmjs.org",
136
+ ];
137
+ if (!allowedHosts.some((h) => redirectUrl.hostname === h || redirectUrl.hostname.endsWith("." + h))) {
128
138
  reject(new Error(`Redirect to untrusted host blocked: ${redirectUrl.hostname}`));
129
139
  return;
130
140
  }
@@ -162,4 +172,3 @@ function doGet(url, timeout, redirectCount = 0) {
162
172
  });
163
173
  });
164
174
  }
165
- //# sourceMappingURL=http.js.map
@@ -0,0 +1,48 @@
1
+ import type { Provider } from "../providers/base.js";
2
+ import type { SkillFile, SkillInfo } from "../types.js";
3
+ export interface InstallOneResult {
4
+ success: boolean;
5
+ skillName: string;
6
+ files?: SkillFile[];
7
+ sizeKB?: number;
8
+ error?: string;
9
+ scanBlocked?: boolean;
10
+ conflictBlocked?: boolean;
11
+ conflictWarnings?: string[];
12
+ alreadyInstalled?: boolean;
13
+ }
14
+ export interface InstallBatchResult {
15
+ installed: string[];
16
+ skipped: string[];
17
+ failed: string[];
18
+ failedErrors: Record<string, string>;
19
+ }
20
+ /** Scan fetched files for security threats. Returns true if install should proceed. */
21
+ export declare function preInstallScan(_skillName: string, files: SkillFile[], force?: boolean): {
22
+ proceed: boolean;
23
+ critical: string[];
24
+ high: string[];
25
+ };
26
+ /** Check for conflicts with existing project context. Returns warnings/blocks. */
27
+ export declare function preInstallConflictCheck(skillName: string, remote: SkillInfo | null, files: SkillFile[], force?: boolean): {
28
+ proceed: boolean;
29
+ blocks: string[];
30
+ warnings: string[];
31
+ };
32
+ /**
33
+ * Core install logic for a single skill. Handles:
34
+ * fetch -> security scan -> conflict check -> write files -> write meta -> update lock
35
+ */
36
+ export declare function installOneCore(skillName: string, provider: Provider, opts: {
37
+ force?: boolean;
38
+ noCheck?: boolean;
39
+ }): Promise<InstallOneResult>;
40
+ /** Compute size warning message if skill exceeds threshold. */
41
+ export declare function sizeWarning(sizeKB: number): string | null;
42
+ /** Check if a skill can be installed (not already present or force mode). */
43
+ export declare function canInstall(skillName: string, force?: boolean): {
44
+ proceed: boolean;
45
+ reason?: string;
46
+ };
47
+ /** Read existing meta to detect provider change on reinstall. */
48
+ export declare function detectProviderChange(skillName: string, newProvider: string): string | null;
@@ -0,0 +1,108 @@
1
+ import * as p from "@clack/prompts";
2
+ import { installSkill, isSkillInstalled, writeSkillMeta, readSkillMeta } from "./fs.js";
3
+ import { scanSkillContent } from "./scanner.js";
4
+ import { updateLockEntry } from "./integrity.js";
5
+ import { checkConflicts } from "./conflict-check.js";
6
+ import { detectProjectContext } from "./project-context.js";
7
+ import { LARGE_SKILL_KB_THRESHOLD, TOKENS_PER_KB } from "../constants.js";
8
+ /** Scan fetched files for security threats. Returns true if install should proceed. */
9
+ export function preInstallScan(_skillName, files, force) {
10
+ const skillMd = files.find((f) => f.path.endsWith("SKILL.md"));
11
+ if (!skillMd)
12
+ return { proceed: true, critical: [], high: [] };
13
+ const issues = scanSkillContent(skillMd.content);
14
+ if (issues.length === 0)
15
+ return { proceed: true, critical: [], high: [] };
16
+ const critical = issues
17
+ .filter((i) => i.level === "critical")
18
+ .map((i) => `${i.category}: ${i.detail} (line ${i.line})`);
19
+ const high = issues.filter((i) => i.level === "high").map((i) => `${i.category}: ${i.detail} (line ${i.line})`);
20
+ if (critical.length > 0 && !force) {
21
+ return { proceed: false, critical, high };
22
+ }
23
+ // When force is true with critical findings, proceed but return the findings
24
+ // so the caller can prompt for confirmation
25
+ return { proceed: true, critical, high };
26
+ }
27
+ /** Check for conflicts with existing project context. Returns warnings/blocks. */
28
+ export function preInstallConflictCheck(skillName, remote, files, force) {
29
+ const context = detectProjectContext(process.cwd());
30
+ const skillMd = files.find((f) => f.path.endsWith("SKILL.md"));
31
+ const warnings = checkConflicts(skillName, remote, skillMd?.content ?? null, context);
32
+ const blocks = warnings.filter((w) => w.severity === "block").map((w) => w.message);
33
+ const warns = warnings.filter((w) => w.severity === "warn").map((w) => w.message);
34
+ if (blocks.length > 0 && !force) {
35
+ return { proceed: false, blocks, warnings: warns };
36
+ }
37
+ return { proceed: true, blocks, warnings: warns };
38
+ }
39
+ /**
40
+ * Core install logic for a single skill. Handles:
41
+ * fetch -> security scan -> conflict check -> write files -> write meta -> update lock
42
+ */
43
+ export async function installOneCore(skillName, provider, opts) {
44
+ const files = await provider.fetch(skillName);
45
+ // Security scan
46
+ const scan = preInstallScan(skillName, files, opts.force);
47
+ if (!scan.proceed) {
48
+ return { success: false, skillName, scanBlocked: true, error: "Blocked by security scan" };
49
+ }
50
+ // When --force bypasses critical findings, require interactive confirmation
51
+ if (opts.force && scan.critical.length > 0 && process.stdout.isTTY) {
52
+ const confirmed = await p.confirm({
53
+ message: `${skillName} has ${scan.critical.length} CRITICAL finding(s). Install anyway?`,
54
+ initialValue: false,
55
+ });
56
+ if (!confirmed || p.isCancel(confirmed)) {
57
+ return { success: false, skillName, scanBlocked: true, error: "User declined forced install" };
58
+ }
59
+ }
60
+ // Conflict detection
61
+ let conflictWarnings = [];
62
+ if (!opts.noCheck) {
63
+ const remote = await provider.info(skillName);
64
+ const conflict = preInstallConflictCheck(skillName, remote, files, opts.force);
65
+ conflictWarnings = conflict.warnings;
66
+ if (!conflict.proceed) {
67
+ return { success: false, skillName, conflictBlocked: true, error: "Blocked by conflict detection" };
68
+ }
69
+ }
70
+ // Install
71
+ installSkill(skillName, files);
72
+ const remote = await provider.info(skillName);
73
+ const version = remote?.version ?? "0.0.0";
74
+ const sizeBytes = files.reduce((s, f) => s + f.content.length, 0);
75
+ writeSkillMeta(skillName, {
76
+ version,
77
+ installedAt: new Date().toISOString(),
78
+ source: provider.name,
79
+ description: remote?.description,
80
+ fileCount: files.length,
81
+ sizeBytes,
82
+ });
83
+ updateLockEntry(skillName, version, provider.name, files);
84
+ const sizeKB = sizeBytes / 1024;
85
+ return { success: true, skillName, files, sizeKB, conflictWarnings };
86
+ }
87
+ /** Compute size warning message if skill exceeds threshold. */
88
+ export function sizeWarning(sizeKB) {
89
+ if (sizeKB <= LARGE_SKILL_KB_THRESHOLD)
90
+ return null;
91
+ return `Large skill (${sizeKB.toFixed(0)} KB, ~${Math.round(sizeKB * TOKENS_PER_KB)} tokens). May use significant context.`;
92
+ }
93
+ /** Check if a skill can be installed (not already present or force mode). */
94
+ export function canInstall(skillName, force) {
95
+ if (!isSkillInstalled(skillName))
96
+ return { proceed: true };
97
+ if (force)
98
+ return { proceed: true };
99
+ return { proceed: false, reason: `${skillName} is already installed. Use --force to reinstall.` };
100
+ }
101
+ /** Read existing meta to detect provider change on reinstall. */
102
+ export function detectProviderChange(skillName, newProvider) {
103
+ const meta = readSkillMeta(skillName);
104
+ if (meta?.source && meta.source !== newProvider) {
105
+ return `Overwriting ${skillName} (was from ${meta.source}, now from ${newProvider})`;
106
+ }
107
+ return null;
108
+ }
@@ -0,0 +1,17 @@
1
+ export interface LockEntry {
2
+ skill: string;
3
+ version: string;
4
+ hash: string;
5
+ source: string;
6
+ installedAt: string;
7
+ }
8
+ export declare function computeHash(content: string): string;
9
+ export declare function getLockfilePath(): string;
10
+ export declare function readLockfile(): LockEntry[];
11
+ export declare function writeLockfile(entries: LockEntry[]): void;
12
+ export declare function updateLockEntry(skill: string, version: string, source: string, files: Array<{
13
+ path: string;
14
+ content: string;
15
+ }>): void;
16
+ export declare function removeLockEntry(skill: string): void;
17
+ export declare function verifySkillIntegrity(skillName: string, installDir: string): "ok" | "modified" | "missing";
@@ -0,0 +1,84 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, mkdirSync, readdirSync, lstatSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { atomicWriteSync } from "./atomic.js";
6
+ export function computeHash(content) {
7
+ return createHash("sha256").update(content).digest("hex");
8
+ }
9
+ export function getLockfilePath() {
10
+ return join(homedir(), ".arcana", "arcana-lock.json");
11
+ }
12
+ export function readLockfile() {
13
+ try {
14
+ const raw = readFileSync(getLockfilePath(), "utf-8");
15
+ const parsed = JSON.parse(raw);
16
+ if (!Array.isArray(parsed))
17
+ return [];
18
+ return parsed;
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ }
24
+ export function writeLockfile(entries) {
25
+ const lockPath = getLockfilePath();
26
+ const dir = join(homedir(), ".arcana");
27
+ mkdirSync(dir, { recursive: true });
28
+ atomicWriteSync(lockPath, JSON.stringify(entries, null, 2) + "\n", 0o600);
29
+ }
30
+ export function updateLockEntry(skill, version, source, files) {
31
+ const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
32
+ const concatenated = sorted.map((f) => f.content).join("");
33
+ const hash = computeHash(concatenated);
34
+ const entries = readLockfile();
35
+ const idx = entries.findIndex((e) => e.skill === skill);
36
+ const entry = {
37
+ skill,
38
+ version,
39
+ hash,
40
+ source,
41
+ installedAt: new Date().toISOString(),
42
+ };
43
+ if (idx >= 0) {
44
+ entries[idx] = entry;
45
+ }
46
+ else {
47
+ entries.push(entry);
48
+ }
49
+ writeLockfile(entries);
50
+ }
51
+ export function removeLockEntry(skill) {
52
+ const entries = readLockfile();
53
+ const filtered = entries.filter((e) => e.skill !== skill);
54
+ writeLockfile(filtered);
55
+ }
56
+ function readDirRecursive(dir) {
57
+ const results = [];
58
+ const items = readdirSync(dir);
59
+ for (const item of items) {
60
+ const fullPath = join(dir, item);
61
+ const stat = lstatSync(fullPath);
62
+ if (stat.isDirectory()) {
63
+ results.push(...readDirRecursive(fullPath));
64
+ }
65
+ else {
66
+ results.push(fullPath);
67
+ }
68
+ }
69
+ return results;
70
+ }
71
+ export function verifySkillIntegrity(skillName, installDir) {
72
+ const entries = readLockfile();
73
+ const entry = entries.find((e) => e.skill === skillName);
74
+ if (!entry)
75
+ return "missing";
76
+ const skillDir = join(installDir, skillName);
77
+ if (!existsSync(skillDir))
78
+ return "modified";
79
+ const filePaths = readDirRecursive(skillDir);
80
+ const relativePaths = filePaths.map((fp) => fp.slice(skillDir.length + 1)).sort();
81
+ const concatenated = relativePaths.map((rel) => readFileSync(join(skillDir, rel), "utf-8")).join("");
82
+ const hash = computeHash(concatenated);
83
+ return hash === entry.hash ? "ok" : "modified";
84
+ }
@@ -1,2 +1 @@
1
1
  export declare function parallelMap<T, R>(items: T[], fn: (item: T) => Promise<R>, concurrency: number): Promise<R[]>;
2
- //# sourceMappingURL=parallel.d.ts.map
@@ -14,4 +14,3 @@ export async function parallelMap(items, fn, concurrency) {
14
14
  await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
15
15
  return results;
16
16
  }
17
- //# sourceMappingURL=parallel.js.map
@@ -0,0 +1,19 @@
1
+ export interface ProjectContext {
2
+ /** Project name from directory or package.json */
3
+ name: string;
4
+ /** Detected primary type */
5
+ type: string;
6
+ /** Primary language */
7
+ lang: string;
8
+ /** All detected tech tags */
9
+ tags: string[];
10
+ /** Extracted preferences from CLAUDE.md */
11
+ preferences: string[];
12
+ /** Names of existing .claude/rules/*.md files */
13
+ ruleFiles: string[];
14
+ /** Raw content of CLAUDE.md if it exists */
15
+ claudeMdContent: string | null;
16
+ /** Names of currently installed skills */
17
+ installedSkills: string[];
18
+ }
19
+ export declare function detectProjectContext(cwd: string): ProjectContext;