@sporesec/arcana 2.4.0 → 3.0.0

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 (245) hide show
  1. package/dist/cli.d.ts +0 -1
  2. package/dist/cli.js +120 -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 +0 -1
  6. package/dist/commands/audit.js +16 -6
  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 +0 -1
  60. package/dist/commands/validate.js +14 -6
  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 +37 -10
  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/scanner.d.ts +0 -1
  128. package/dist/utils/scanner.js +138 -10
  129. package/dist/utils/scoring.d.ts +10 -0
  130. package/dist/utils/scoring.js +84 -0
  131. package/dist/utils/ui.d.ts +0 -1
  132. package/dist/utils/ui.js +11 -4
  133. package/dist/utils/validate.d.ts +0 -1
  134. package/dist/utils/validate.js +4 -1
  135. package/package.json +74 -62
  136. package/dist/cli.d.ts.map +0 -1
  137. package/dist/cli.js.map +0 -1
  138. package/dist/commands/audit.d.ts.map +0 -1
  139. package/dist/commands/audit.js.map +0 -1
  140. package/dist/commands/audit.test.d.ts +0 -2
  141. package/dist/commands/audit.test.d.ts.map +0 -1
  142. package/dist/commands/audit.test.js +0 -217
  143. package/dist/commands/audit.test.js.map +0 -1
  144. package/dist/commands/clean.d.ts.map +0 -1
  145. package/dist/commands/clean.js.map +0 -1
  146. package/dist/commands/compact.d.ts.map +0 -1
  147. package/dist/commands/compact.js.map +0 -1
  148. package/dist/commands/config.d.ts.map +0 -1
  149. package/dist/commands/config.js.map +0 -1
  150. package/dist/commands/create.d.ts.map +0 -1
  151. package/dist/commands/create.js.map +0 -1
  152. package/dist/commands/doctor.d.ts.map +0 -1
  153. package/dist/commands/doctor.js.map +0 -1
  154. package/dist/commands/info.d.ts.map +0 -1
  155. package/dist/commands/info.js.map +0 -1
  156. package/dist/commands/init.d.ts.map +0 -1
  157. package/dist/commands/init.js.map +0 -1
  158. package/dist/commands/install.d.ts.map +0 -1
  159. package/dist/commands/install.js.map +0 -1
  160. package/dist/commands/list.d.ts.map +0 -1
  161. package/dist/commands/list.js.map +0 -1
  162. package/dist/commands/optimize.d.ts.map +0 -1
  163. package/dist/commands/optimize.js.map +0 -1
  164. package/dist/commands/providers.d.ts.map +0 -1
  165. package/dist/commands/providers.js.map +0 -1
  166. package/dist/commands/scan.d.ts.map +0 -1
  167. package/dist/commands/scan.js.map +0 -1
  168. package/dist/commands/search.d.ts.map +0 -1
  169. package/dist/commands/search.js.map +0 -1
  170. package/dist/commands/stats.d.ts.map +0 -1
  171. package/dist/commands/stats.js.map +0 -1
  172. package/dist/commands/uninstall.d.ts.map +0 -1
  173. package/dist/commands/uninstall.js.map +0 -1
  174. package/dist/commands/update.d.ts.map +0 -1
  175. package/dist/commands/update.js.map +0 -1
  176. package/dist/commands/validate.d.ts.map +0 -1
  177. package/dist/commands/validate.js.map +0 -1
  178. package/dist/index.d.ts.map +0 -1
  179. package/dist/index.js.map +0 -1
  180. package/dist/interactive.d.ts.map +0 -1
  181. package/dist/interactive.js.map +0 -1
  182. package/dist/providers/arcana.d.ts.map +0 -1
  183. package/dist/providers/arcana.js.map +0 -1
  184. package/dist/providers/base.d.ts.map +0 -1
  185. package/dist/providers/base.js.map +0 -1
  186. package/dist/providers/github.d.ts.map +0 -1
  187. package/dist/providers/github.js.map +0 -1
  188. package/dist/registry.d.ts.map +0 -1
  189. package/dist/registry.js.map +0 -1
  190. package/dist/types.d.ts.map +0 -1
  191. package/dist/types.js.map +0 -1
  192. package/dist/utils/atomic.d.ts.map +0 -1
  193. package/dist/utils/atomic.js.map +0 -1
  194. package/dist/utils/atomic.test.d.ts +0 -2
  195. package/dist/utils/atomic.test.d.ts.map +0 -1
  196. package/dist/utils/atomic.test.js +0 -31
  197. package/dist/utils/atomic.test.js.map +0 -1
  198. package/dist/utils/cache.d.ts.map +0 -1
  199. package/dist/utils/cache.js.map +0 -1
  200. package/dist/utils/config.d.ts.map +0 -1
  201. package/dist/utils/config.js.map +0 -1
  202. package/dist/utils/config.test.d.ts +0 -2
  203. package/dist/utils/config.test.d.ts.map +0 -1
  204. package/dist/utils/config.test.js +0 -38
  205. package/dist/utils/config.test.js.map +0 -1
  206. package/dist/utils/errors.d.ts.map +0 -1
  207. package/dist/utils/errors.js.map +0 -1
  208. package/dist/utils/frontmatter.d.ts.map +0 -1
  209. package/dist/utils/frontmatter.js.map +0 -1
  210. package/dist/utils/frontmatter.test.d.ts +0 -2
  211. package/dist/utils/frontmatter.test.d.ts.map +0 -1
  212. package/dist/utils/frontmatter.test.js +0 -152
  213. package/dist/utils/frontmatter.test.js.map +0 -1
  214. package/dist/utils/fs.d.ts.map +0 -1
  215. package/dist/utils/fs.js.map +0 -1
  216. package/dist/utils/fs.test.d.ts +0 -2
  217. package/dist/utils/fs.test.d.ts.map +0 -1
  218. package/dist/utils/fs.test.js +0 -145
  219. package/dist/utils/fs.test.js.map +0 -1
  220. package/dist/utils/help.d.ts.map +0 -1
  221. package/dist/utils/help.js.map +0 -1
  222. package/dist/utils/help.test.d.ts +0 -2
  223. package/dist/utils/help.test.d.ts.map +0 -1
  224. package/dist/utils/help.test.js +0 -66
  225. package/dist/utils/help.test.js.map +0 -1
  226. package/dist/utils/history.d.ts.map +0 -1
  227. package/dist/utils/history.js.map +0 -1
  228. package/dist/utils/http.d.ts.map +0 -1
  229. package/dist/utils/http.js.map +0 -1
  230. package/dist/utils/http.test.d.ts +0 -2
  231. package/dist/utils/http.test.d.ts.map +0 -1
  232. package/dist/utils/http.test.js +0 -55
  233. package/dist/utils/http.test.js.map +0 -1
  234. package/dist/utils/parallel.d.ts.map +0 -1
  235. package/dist/utils/parallel.js.map +0 -1
  236. package/dist/utils/scanner.d.ts.map +0 -1
  237. package/dist/utils/scanner.js.map +0 -1
  238. package/dist/utils/ui.d.ts.map +0 -1
  239. package/dist/utils/ui.js.map +0 -1
  240. package/dist/utils/ui.test.d.ts +0 -2
  241. package/dist/utils/ui.test.d.ts.map +0 -1
  242. package/dist/utils/ui.test.js +0 -31
  243. package/dist/utils/ui.test.js.map +0 -1
  244. package/dist/utils/validate.d.ts.map +0 -1
  245. package/dist/utils/validate.js.map +0 -1
@@ -6,4 +6,3 @@ export declare function parseProviderSlug(input: string): {
6
6
  };
7
7
  export declare function getProvider(name?: string): Provider;
8
8
  export declare function getProviders(name?: string): Provider[];
9
- //# sourceMappingURL=registry.d.ts.map
package/dist/registry.js CHANGED
@@ -64,8 +64,5 @@ export function getProviders(name) {
64
64
  if (name)
65
65
  return [getProvider(name)];
66
66
  const config = loadConfig();
67
- return config.providers
68
- .filter((p) => p.enabled)
69
- .map((p) => createProvider(p.name, p.type, p.url));
67
+ return config.providers.filter((p) => p.enabled).map((p) => createProvider(p.name, p.type, p.url));
70
68
  }
71
- //# sourceMappingURL=registry.js.map
package/dist/types.d.ts CHANGED
@@ -4,6 +4,11 @@ export interface SkillInfo {
4
4
  version: string;
5
5
  source: string;
6
6
  repo?: string;
7
+ tags?: string[];
8
+ conflicts?: string[];
9
+ companions?: string[];
10
+ verified?: boolean;
11
+ author?: string;
7
12
  }
8
13
  export interface SkillFile {
9
14
  path: string;
@@ -26,6 +31,11 @@ export interface MarketplacePlugin {
26
31
  source: string;
27
32
  description: string;
28
33
  version: string;
34
+ tags?: string[];
35
+ conflicts?: string[];
36
+ companions?: string[];
37
+ verified?: boolean;
38
+ author?: string;
29
39
  }
30
40
  export interface ProviderConfig {
31
41
  name: string;
@@ -64,4 +74,3 @@ export interface DoctorCheck {
64
74
  message: string;
65
75
  fix?: string;
66
76
  }
67
- //# sourceMappingURL=types.d.ts.map
package/dist/types.js CHANGED
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types.js.map
@@ -1,2 +1 @@
1
1
  export declare function atomicWriteSync(filePath: string, content: string, mode?: number): void;
2
- //# sourceMappingURL=atomic.d.ts.map
@@ -13,8 +13,9 @@ export function atomicWriteSync(filePath, content, mode = 0o644) {
13
13
  try {
14
14
  unlinkSync(tmpPath);
15
15
  }
16
- catch { /* cleanup best-effort */ }
16
+ catch {
17
+ /* cleanup best-effort */
18
+ }
17
19
  throw err;
18
20
  }
19
21
  }
20
- //# sourceMappingURL=atomic.js.map
@@ -1,4 +1,3 @@
1
1
  export declare function readCache<T>(key: string, maxAgeMs?: number): T | null;
2
2
  export declare function writeCache<T>(key: string, data: T): void;
3
3
  export declare function clearCacheFile(key: string): void;
4
- //# sourceMappingURL=cache.d.ts.map
@@ -41,7 +41,8 @@ export function clearCacheFile(key) {
41
41
  try {
42
42
  unlinkSync(file);
43
43
  }
44
- catch { /* best-effort */ }
44
+ catch {
45
+ /* best-effort */
46
+ }
45
47
  }
46
48
  }
47
- //# sourceMappingURL=cache.js.map
@@ -1,6 +1,7 @@
1
1
  import type { ArcanaConfig, ProviderConfig } from "../types.js";
2
+ /** Validate config and return warnings for invalid fields. */
3
+ export declare function validateConfig(config: ArcanaConfig): string[];
2
4
  export declare function loadConfig(): ArcanaConfig;
3
5
  export declare function saveConfig(config: ArcanaConfig): void;
4
6
  export declare function addProvider(provider: ProviderConfig): void;
5
7
  export declare function removeProvider(name: string): boolean;
6
- //# sourceMappingURL=config.d.ts.map
@@ -4,6 +4,8 @@ import { homedir } from "node:os";
4
4
  import { ui } from "./ui.js";
5
5
  import { atomicWriteSync } from "./atomic.js";
6
6
  const CONFIG_PATH = join(homedir(), ".arcana", "config.json");
7
+ /** Matches owner/repo slug format (e.g. "medy-gribkov/arcana") */
8
+ const SLUG_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
7
9
  const DEFAULT_CONFIG = {
8
10
  defaultProvider: "arcana",
9
11
  installDir: join(homedir(), ".agents", "skills"),
@@ -17,7 +19,27 @@ const DEFAULT_CONFIG = {
17
19
  ],
18
20
  };
19
21
  function cloneConfig(config) {
20
- return { ...config, providers: config.providers.map(p => ({ ...p })) };
22
+ return { ...config, providers: config.providers.map((p) => ({ ...p })) };
23
+ }
24
+ /** Validate config and return warnings for invalid fields. */
25
+ export function validateConfig(config) {
26
+ const warnings = [];
27
+ // Validate providers have valid owner/repo slugs
28
+ for (const p of config.providers) {
29
+ if (p.type === "github" && !SLUG_RE.test(p.url)) {
30
+ warnings.push(`Provider "${p.name}" has invalid URL "${p.url}". Expected owner/repo format.`);
31
+ }
32
+ }
33
+ // Validate installDir is absolute
34
+ if (!isAbsolute(config.installDir)) {
35
+ warnings.push(`installDir "${config.installDir}" is not an absolute path.`);
36
+ }
37
+ // Validate defaultProvider matches a configured provider or valid slug
38
+ const providerNames = config.providers.map((p) => p.name);
39
+ if (!providerNames.includes(config.defaultProvider) && !SLUG_RE.test(config.defaultProvider)) {
40
+ warnings.push(`defaultProvider "${config.defaultProvider}" doesn't match any configured provider and isn't a valid slug.`);
41
+ }
42
+ return warnings;
21
43
  }
22
44
  export function loadConfig() {
23
45
  if (!existsSync(CONFIG_PATH)) {
@@ -29,7 +51,7 @@ export function loadConfig() {
29
51
  const config = {
30
52
  ...DEFAULT_CONFIG,
31
53
  ...loaded,
32
- providers: loaded.providers ?? DEFAULT_CONFIG.providers.map(p => ({ ...p })),
54
+ providers: loaded.providers ?? DEFAULT_CONFIG.providers.map((p) => ({ ...p })),
33
55
  };
34
56
  return applyEnvOverrides(config);
35
57
  }
@@ -51,11 +73,15 @@ function applyEnvOverrides(base) {
51
73
  }
52
74
  const envProvider = process.env.ARCANA_DEFAULT_PROVIDER;
53
75
  if (envProvider) {
54
- if (envProvider.trim().length === 0) {
76
+ const trimmed = envProvider.trim();
77
+ if (trimmed.length === 0) {
55
78
  console.error(ui.warn(" Warning: ARCANA_DEFAULT_PROVIDER is empty. Ignoring."));
56
79
  }
80
+ else if (!SLUG_RE.test(trimmed) && !config.providers.some((p) => p.name === trimmed)) {
81
+ console.error(ui.warn(` Warning: ARCANA_DEFAULT_PROVIDER "${trimmed}" is not a valid slug. Ignoring.`));
82
+ }
57
83
  else {
58
- config.defaultProvider = envProvider.trim();
84
+ config.defaultProvider = trimmed;
59
85
  }
60
86
  }
61
87
  return config;
@@ -87,4 +113,3 @@ export function removeProvider(name) {
87
113
  saveConfig(config);
88
114
  return true;
89
115
  }
90
- //# sourceMappingURL=config.js.map
@@ -0,0 +1,8 @@
1
+ import type { SkillInfo } from "../types.js";
2
+ import type { ProjectContext } from "./project-context.js";
3
+ export interface ConflictWarning {
4
+ type: "rule-overlap" | "skill-conflict" | "preference-clash";
5
+ message: string;
6
+ severity: "warn" | "block";
7
+ }
8
+ export declare function checkConflicts(skillName: string, skillInfo: SkillInfo | null, skillContent: string | null, context: ProjectContext): ConflictWarning[];
@@ -0,0 +1,72 @@
1
+ /** Known opposing preference pairs. If a skill promotes one, and CLAUDE.md has the other, warn. */
2
+ const OPPOSING_PAIRS = [
3
+ ["callbacks", "async/await"],
4
+ ["any", "strict typing"],
5
+ ["abbreviations", "meaningful names"],
6
+ ["classes", "functional"],
7
+ ["oop", "functional programming"],
8
+ ["semicolons", "no semicolons"],
9
+ ["tabs", "spaces"],
10
+ ];
11
+ export function checkConflicts(skillName, skillInfo, skillContent, context) {
12
+ const warnings = [];
13
+ // 1. Explicit skill-level conflicts from marketplace metadata
14
+ if (skillInfo?.conflicts && skillInfo.conflicts.length > 0) {
15
+ const installed = context.installedSkills;
16
+ const conflicting = skillInfo.conflicts.filter((c) => installed.includes(c));
17
+ for (const c of conflicting) {
18
+ warnings.push({
19
+ type: "skill-conflict",
20
+ message: `"${skillName}" conflicts with installed skill "${c}".`,
21
+ severity: "block",
22
+ });
23
+ }
24
+ }
25
+ // 2. Rule overlap: skill name matches existing .claude/rules/*.md filename
26
+ const ruleBasenames = context.ruleFiles.map((f) => f.replace(/\.md$/, "").toLowerCase());
27
+ if (ruleBasenames.includes(skillName.toLowerCase())) {
28
+ warnings.push({
29
+ type: "rule-overlap",
30
+ message: `A rule file "${skillName}.md" already exists in .claude/rules/. This skill may duplicate existing instructions.`,
31
+ severity: "warn",
32
+ });
33
+ }
34
+ // Also check if skill tags overlap heavily with rule file names
35
+ if (skillInfo?.tags) {
36
+ for (const tag of skillInfo.tags) {
37
+ if (ruleBasenames.includes(tag.toLowerCase()) && tag.toLowerCase() !== skillName.toLowerCase()) {
38
+ warnings.push({
39
+ type: "rule-overlap",
40
+ message: `Skill tag "${tag}" matches existing rule "${tag}.md". May overlap.`,
41
+ severity: "warn",
42
+ });
43
+ break; // one warning is enough
44
+ }
45
+ }
46
+ }
47
+ // 3. Preference clash: check skill content against CLAUDE.md preferences
48
+ if (skillContent && context.preferences.length > 0) {
49
+ const contentLower = skillContent.toLowerCase();
50
+ for (const [a, b] of OPPOSING_PAIRS) {
51
+ const skillHasA = contentLower.includes(a);
52
+ const skillHasB = contentLower.includes(b);
53
+ const prefsHaveA = context.preferences.some((p) => p.toLowerCase().includes(a));
54
+ const prefsHaveB = context.preferences.some((p) => p.toLowerCase().includes(b));
55
+ if (skillHasA && prefsHaveB) {
56
+ warnings.push({
57
+ type: "preference-clash",
58
+ message: `Skill mentions "${a}" but your project prefers "${b}".`,
59
+ severity: "warn",
60
+ });
61
+ }
62
+ else if (skillHasB && prefsHaveA) {
63
+ warnings.push({
64
+ type: "preference-clash",
65
+ message: `Skill mentions "${b}" but your project prefers "${a}".`,
66
+ severity: "warn",
67
+ });
68
+ }
69
+ }
70
+ }
71
+ return warnings;
72
+ }
@@ -3,4 +3,3 @@ export declare class CliError extends Error {
3
3
  readonly exitCode: number;
4
4
  constructor(message: string, code?: string, exitCode?: number);
5
5
  }
6
- //# sourceMappingURL=errors.d.ts.map
@@ -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) {
@@ -145,12 +146,29 @@ export function validateSkillDir(skillDir, skillName) {
145
146
  else if (parsed.description.length > MAX_DESC_LENGTH) {
146
147
  result.warnings.push(`Description too long (${parsed.description.length} chars, max ${MAX_DESC_LENGTH})`);
147
148
  }
148
- // Check for non-standard fields
149
+ // Check for non-standard fields (metadata is invalid per spec)
149
150
  const standardFields = ["name", "description"];
151
+ const VALID_FIELDS = [
152
+ "name",
153
+ "description",
154
+ "argument-hint",
155
+ "compatibility",
156
+ "disable-model-invocation",
157
+ "license",
158
+ "user-invokable",
159
+ ];
150
160
  for (const line of extracted.raw.split("\n")) {
151
161
  const keyMatch = line.match(/^(\w[\w-]*):/);
152
162
  if (keyMatch?.[1] && !standardFields.includes(keyMatch[1])) {
153
- result.infos.push(`Non-standard field: ${keyMatch[1]}`);
163
+ if (keyMatch[1] === "metadata") {
164
+ result.warnings.push("Invalid field: metadata (not allowed in frontmatter)");
165
+ }
166
+ else if (!VALID_FIELDS.includes(keyMatch[1])) {
167
+ result.infos.push(`Non-standard field: ${keyMatch[1]}`);
168
+ }
169
+ else {
170
+ result.infos.push(`Optional field: ${keyMatch[1]}`);
171
+ }
154
172
  }
155
173
  }
156
174
  if (parsed.name !== skillName) {
@@ -165,8 +183,17 @@ export function validateSkillDir(skillDir, skillName) {
165
183
  if (extracted.body.trim().length >= 50 && !extracted.body.includes("##")) {
166
184
  result.infos.push("Body has no ## headings (recommended for structure)");
167
185
  }
186
+ // Check for code blocks (quality signal)
187
+ if (extracted.body.trim().length >= 50 && !extracted.body.includes("```")) {
188
+ result.infos.push("No code blocks found (procedural skills should include code examples)");
189
+ }
190
+ // Check for BAD/GOOD pattern examples
191
+ const hasPattern = /(?:BAD|GOOD|WRONG|RIGHT|AVOID|PREFER|DO NOT|INSTEAD)/i.test(extracted.body) ||
192
+ /<!--\s*(?:bad|good)\s*-->/i.test(extracted.body);
193
+ if (extracted.body.trim().length >= 100 && !hasPattern) {
194
+ result.infos.push("No BAD/GOOD contrast patterns found (recommended for teaching)");
195
+ }
168
196
  if (result.errors.length > 0)
169
197
  result.valid = false;
170
198
  return result;
171
199
  }
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;