@sporesec/arcana 2.3.1 → 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 (241) hide show
  1. package/dist/cli.d.ts +0 -1
  2. package/dist/cli.js +140 -10
  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 +2 -1
  10. package/dist/commands/clean.js +198 -47
  11. package/dist/commands/compact.d.ts +6 -0
  12. package/dist/commands/compact.js +239 -0
  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 +153 -24
  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 +156 -117
  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 +3 -0
  38. package/dist/commands/optimize.js +356 -0
  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 +83 -16
  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 +19 -1
  112. package/dist/utils/fs.js +105 -8
  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 +19 -7
  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/config.d.ts.map +0 -1
  147. package/dist/commands/config.js.map +0 -1
  148. package/dist/commands/create.d.ts.map +0 -1
  149. package/dist/commands/create.js.map +0 -1
  150. package/dist/commands/doctor.d.ts.map +0 -1
  151. package/dist/commands/doctor.js.map +0 -1
  152. package/dist/commands/info.d.ts.map +0 -1
  153. package/dist/commands/info.js.map +0 -1
  154. package/dist/commands/init.d.ts.map +0 -1
  155. package/dist/commands/init.js.map +0 -1
  156. package/dist/commands/install.d.ts.map +0 -1
  157. package/dist/commands/install.js.map +0 -1
  158. package/dist/commands/list.d.ts.map +0 -1
  159. package/dist/commands/list.js.map +0 -1
  160. package/dist/commands/providers.d.ts.map +0 -1
  161. package/dist/commands/providers.js.map +0 -1
  162. package/dist/commands/scan.d.ts.map +0 -1
  163. package/dist/commands/scan.js.map +0 -1
  164. package/dist/commands/search.d.ts.map +0 -1
  165. package/dist/commands/search.js.map +0 -1
  166. package/dist/commands/stats.d.ts.map +0 -1
  167. package/dist/commands/stats.js.map +0 -1
  168. package/dist/commands/uninstall.d.ts.map +0 -1
  169. package/dist/commands/uninstall.js.map +0 -1
  170. package/dist/commands/update.d.ts.map +0 -1
  171. package/dist/commands/update.js.map +0 -1
  172. package/dist/commands/validate.d.ts.map +0 -1
  173. package/dist/commands/validate.js.map +0 -1
  174. package/dist/index.d.ts.map +0 -1
  175. package/dist/index.js.map +0 -1
  176. package/dist/interactive.d.ts.map +0 -1
  177. package/dist/interactive.js.map +0 -1
  178. package/dist/providers/arcana.d.ts.map +0 -1
  179. package/dist/providers/arcana.js.map +0 -1
  180. package/dist/providers/base.d.ts.map +0 -1
  181. package/dist/providers/base.js.map +0 -1
  182. package/dist/providers/github.d.ts.map +0 -1
  183. package/dist/providers/github.js.map +0 -1
  184. package/dist/registry.d.ts.map +0 -1
  185. package/dist/registry.js.map +0 -1
  186. package/dist/types.d.ts.map +0 -1
  187. package/dist/types.js.map +0 -1
  188. package/dist/utils/atomic.d.ts.map +0 -1
  189. package/dist/utils/atomic.js.map +0 -1
  190. package/dist/utils/atomic.test.d.ts +0 -2
  191. package/dist/utils/atomic.test.d.ts.map +0 -1
  192. package/dist/utils/atomic.test.js +0 -31
  193. package/dist/utils/atomic.test.js.map +0 -1
  194. package/dist/utils/cache.d.ts.map +0 -1
  195. package/dist/utils/cache.js.map +0 -1
  196. package/dist/utils/config.d.ts.map +0 -1
  197. package/dist/utils/config.js.map +0 -1
  198. package/dist/utils/config.test.d.ts +0 -2
  199. package/dist/utils/config.test.d.ts.map +0 -1
  200. package/dist/utils/config.test.js +0 -38
  201. package/dist/utils/config.test.js.map +0 -1
  202. package/dist/utils/errors.d.ts.map +0 -1
  203. package/dist/utils/errors.js.map +0 -1
  204. package/dist/utils/frontmatter.d.ts.map +0 -1
  205. package/dist/utils/frontmatter.js.map +0 -1
  206. package/dist/utils/frontmatter.test.d.ts +0 -2
  207. package/dist/utils/frontmatter.test.d.ts.map +0 -1
  208. package/dist/utils/frontmatter.test.js +0 -152
  209. package/dist/utils/frontmatter.test.js.map +0 -1
  210. package/dist/utils/fs.d.ts.map +0 -1
  211. package/dist/utils/fs.js.map +0 -1
  212. package/dist/utils/fs.test.d.ts +0 -2
  213. package/dist/utils/fs.test.d.ts.map +0 -1
  214. package/dist/utils/fs.test.js +0 -145
  215. package/dist/utils/fs.test.js.map +0 -1
  216. package/dist/utils/help.d.ts.map +0 -1
  217. package/dist/utils/help.js.map +0 -1
  218. package/dist/utils/help.test.d.ts +0 -2
  219. package/dist/utils/help.test.d.ts.map +0 -1
  220. package/dist/utils/help.test.js +0 -66
  221. package/dist/utils/help.test.js.map +0 -1
  222. package/dist/utils/history.d.ts.map +0 -1
  223. package/dist/utils/history.js.map +0 -1
  224. package/dist/utils/http.d.ts.map +0 -1
  225. package/dist/utils/http.js.map +0 -1
  226. package/dist/utils/http.test.d.ts +0 -2
  227. package/dist/utils/http.test.d.ts.map +0 -1
  228. package/dist/utils/http.test.js +0 -55
  229. package/dist/utils/http.test.js.map +0 -1
  230. package/dist/utils/parallel.d.ts.map +0 -1
  231. package/dist/utils/parallel.js.map +0 -1
  232. package/dist/utils/scanner.d.ts.map +0 -1
  233. package/dist/utils/scanner.js.map +0 -1
  234. package/dist/utils/ui.d.ts.map +0 -1
  235. package/dist/utils/ui.js.map +0 -1
  236. package/dist/utils/ui.test.d.ts +0 -2
  237. package/dist/utils/ui.test.d.ts.map +0 -1
  238. package/dist/utils/ui.test.js +0 -31
  239. package/dist/utils/ui.test.js.map +0 -1
  240. package/dist/utils/validate.d.ts.map +0 -1
  241. package/dist/utils/validate.js.map +0 -1
@@ -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;
@@ -0,0 +1,283 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { join, basename } from "node:path";
3
+ import { getInstallDir } from "./fs.js";
4
+ /** Map npm package names to tech tags */
5
+ const PACKAGE_TAG_MAP = {
6
+ next: ["next", "react", "typescript"],
7
+ react: ["react"],
8
+ "react-dom": ["react"],
9
+ vue: ["vue"],
10
+ svelte: ["svelte"],
11
+ angular: ["angular"],
12
+ tailwindcss: ["tailwind"],
13
+ prisma: ["prisma", "database"],
14
+ "@prisma/client": ["prisma", "database"],
15
+ "drizzle-orm": ["drizzle", "database"],
16
+ express: ["express", "node"],
17
+ fastify: ["fastify", "node"],
18
+ hono: ["hono", "node"],
19
+ vitest: ["testing"],
20
+ jest: ["testing"],
21
+ mocha: ["testing"],
22
+ playwright: ["playwright", "testing"],
23
+ cypress: ["cypress", "testing"],
24
+ remotion: ["remotion", "react"],
25
+ three: ["threejs"],
26
+ docker: ["docker"],
27
+ electron: ["electron"],
28
+ "react-native": ["react-native", "react", "mobile"],
29
+ expo: ["expo", "react-native", "mobile"],
30
+ graphql: ["graphql"],
31
+ "@apollo/client": ["graphql", "apollo"],
32
+ trpc: ["trpc"],
33
+ "@trpc/server": ["trpc"],
34
+ mongoose: ["mongodb", "database"],
35
+ pg: ["postgresql", "database"],
36
+ redis: ["redis"],
37
+ ioredis: ["redis"],
38
+ "socket.io": ["websocket"],
39
+ ws: ["websocket"],
40
+ webpack: ["webpack"],
41
+ vite: ["vite"],
42
+ tsup: ["tsup"],
43
+ eslint: ["linting"],
44
+ prettier: ["formatting"],
45
+ storybook: ["storybook"],
46
+ "@storybook/react": ["storybook", "react"],
47
+ };
48
+ /** Map Go module paths to tags */
49
+ const GO_MODULE_TAG_MAP = {
50
+ "github.com/gin-gonic/gin": ["gin", "web"],
51
+ "github.com/gofiber/fiber": ["fiber", "web"],
52
+ "github.com/labstack/echo": ["echo", "web"],
53
+ "github.com/gorilla/mux": ["gorilla", "web"],
54
+ "gorm.io/gorm": ["gorm", "database"],
55
+ "github.com/jmoiron/sqlx": ["sqlx", "database"],
56
+ "github.com/jackc/pgx": ["postgresql", "database"],
57
+ "github.com/go-redis/redis": ["redis"],
58
+ "github.com/rs/zerolog": ["zerolog", "logging"],
59
+ "go.uber.org/zap": ["zap", "logging"],
60
+ "github.com/stretchr/testify": ["testing"],
61
+ "google.golang.org/grpc": ["grpc"],
62
+ "google.golang.org/protobuf": ["protobuf"],
63
+ };
64
+ /** Map Python packages to tags */
65
+ const PYTHON_PACKAGE_TAG_MAP = {
66
+ django: ["django", "web"],
67
+ flask: ["flask", "web"],
68
+ fastapi: ["fastapi", "web"],
69
+ pytorch: ["pytorch", "ml"],
70
+ torch: ["pytorch", "ml"],
71
+ tensorflow: ["tensorflow", "ml"],
72
+ numpy: ["numpy"],
73
+ pandas: ["pandas"],
74
+ sqlalchemy: ["sqlalchemy", "database"],
75
+ pytest: ["testing"],
76
+ celery: ["celery", "async"],
77
+ scrapy: ["scrapy", "scraping"],
78
+ playwright: ["playwright", "testing"],
79
+ requests: ["requests"],
80
+ httpx: ["httpx"],
81
+ };
82
+ function readJsonSafe(filePath) {
83
+ try {
84
+ return JSON.parse(readFileSync(filePath, "utf-8"));
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ }
90
+ function readTextSafe(filePath) {
91
+ try {
92
+ return readFileSync(filePath, "utf-8");
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
98
+ function detectTypeAndLang(cwd) {
99
+ const name = basename(cwd);
100
+ if (existsSync(join(cwd, "go.mod")))
101
+ return { name, type: "Go", lang: "go" };
102
+ if (existsSync(join(cwd, "Cargo.toml")))
103
+ return { name, type: "Rust", lang: "rust" };
104
+ if (existsSync(join(cwd, "requirements.txt")) || existsSync(join(cwd, "pyproject.toml")))
105
+ return { name, type: "Python", lang: "python" };
106
+ if (existsSync(join(cwd, "package.json"))) {
107
+ const pkg = readJsonSafe(join(cwd, "package.json"));
108
+ if (pkg?.dependencies?.next || pkg?.devDependencies?.next)
109
+ return { name, type: "Next.js", lang: "typescript" };
110
+ if (pkg?.dependencies?.react || pkg?.devDependencies?.react)
111
+ return { name, type: "React", lang: "typescript" };
112
+ return { name, type: "Node.js", lang: "typescript" };
113
+ }
114
+ return { name, type: "Unknown", lang: "general" };
115
+ }
116
+ function extractNpmTags(cwd) {
117
+ const pkgPath = join(cwd, "package.json");
118
+ const pkg = readJsonSafe(pkgPath);
119
+ if (!pkg)
120
+ return [];
121
+ const tags = new Set();
122
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
123
+ for (const dep of Object.keys(allDeps)) {
124
+ const mapped = PACKAGE_TAG_MAP[dep];
125
+ if (mapped)
126
+ mapped.forEach((t) => tags.add(t));
127
+ }
128
+ // Check for TypeScript
129
+ if (existsSync(join(cwd, "tsconfig.json")) || allDeps.typescript) {
130
+ tags.add("typescript");
131
+ }
132
+ return [...tags];
133
+ }
134
+ function extractGoTags(cwd) {
135
+ const goModPath = join(cwd, "go.mod");
136
+ const content = readTextSafe(goModPath);
137
+ if (!content)
138
+ return ["go"];
139
+ const tags = new Set(["go"]);
140
+ for (const [modulePath, moduleTags] of Object.entries(GO_MODULE_TAG_MAP)) {
141
+ if (content.includes(modulePath)) {
142
+ moduleTags.forEach((t) => tags.add(t));
143
+ }
144
+ }
145
+ return [...tags];
146
+ }
147
+ function extractPythonTags(cwd) {
148
+ const tags = new Set(["python"]);
149
+ // Read requirements.txt
150
+ const reqContent = readTextSafe(join(cwd, "requirements.txt"));
151
+ if (reqContent) {
152
+ for (const line of reqContent.split("\n")) {
153
+ const pkg = line
154
+ .trim()
155
+ .split(/[=<>!~[]/)[0]
156
+ ?.toLowerCase();
157
+ if (pkg) {
158
+ const mapped = PYTHON_PACKAGE_TAG_MAP[pkg];
159
+ if (mapped)
160
+ mapped.forEach((t) => tags.add(t));
161
+ }
162
+ }
163
+ }
164
+ // Read pyproject.toml (simple scan, not full TOML parse)
165
+ const pyprojectContent = readTextSafe(join(cwd, "pyproject.toml"));
166
+ if (pyprojectContent) {
167
+ for (const [pkgName, pkgTags] of Object.entries(PYTHON_PACKAGE_TAG_MAP)) {
168
+ if (pyprojectContent.includes(`"${pkgName}"`) || pyprojectContent.includes(`'${pkgName}'`)) {
169
+ pkgTags.forEach((t) => tags.add(t));
170
+ }
171
+ }
172
+ }
173
+ return [...tags];
174
+ }
175
+ function extractInfraTags(cwd) {
176
+ const tags = [];
177
+ if (existsSync(join(cwd, "Dockerfile")) ||
178
+ existsSync(join(cwd, "docker-compose.yml")) ||
179
+ existsSync(join(cwd, "docker-compose.yaml"))) {
180
+ tags.push("docker");
181
+ }
182
+ if (existsSync(join(cwd, ".github", "workflows"))) {
183
+ tags.push("ci-cd", "github-actions");
184
+ }
185
+ if (existsSync(join(cwd, ".gitlab-ci.yml"))) {
186
+ tags.push("ci-cd", "gitlab-ci");
187
+ }
188
+ if (existsSync(join(cwd, "k8s")) || existsSync(join(cwd, "kubernetes"))) {
189
+ tags.push("kubernetes");
190
+ }
191
+ if (existsSync(join(cwd, "terraform")) || existsSync(join(cwd, "main.tf"))) {
192
+ tags.push("terraform");
193
+ }
194
+ return tags;
195
+ }
196
+ function extractPreferences(claudeMdContent) {
197
+ const prefs = [];
198
+ const lines = claudeMdContent.split("\n");
199
+ let inPrefsSection = false;
200
+ for (const line of lines) {
201
+ const lower = line.toLowerCase();
202
+ if (lower.includes("## coding") || lower.includes("## preferences") || lower.includes("## style")) {
203
+ inPrefsSection = true;
204
+ continue;
205
+ }
206
+ if (inPrefsSection && line.startsWith("## ")) {
207
+ inPrefsSection = false;
208
+ continue;
209
+ }
210
+ if (inPrefsSection && line.trim().startsWith("-")) {
211
+ prefs.push(line.trim().replace(/^-\s*/, ""));
212
+ }
213
+ }
214
+ return prefs;
215
+ }
216
+ function readRuleFiles(cwd) {
217
+ const rulesDir = join(cwd, ".claude", "rules");
218
+ if (!existsSync(rulesDir))
219
+ return [];
220
+ try {
221
+ return readdirSync(rulesDir)
222
+ .filter((f) => f.endsWith(".md"))
223
+ .sort();
224
+ }
225
+ catch {
226
+ return [];
227
+ }
228
+ }
229
+ function getInstalledSkillNames() {
230
+ const dir = getInstallDir();
231
+ if (!existsSync(dir))
232
+ return [];
233
+ try {
234
+ return readdirSync(dir)
235
+ .filter((d) => {
236
+ try {
237
+ return statSync(join(dir, d)).isDirectory();
238
+ }
239
+ catch {
240
+ return false;
241
+ }
242
+ })
243
+ .sort();
244
+ }
245
+ catch {
246
+ return [];
247
+ }
248
+ }
249
+ export function detectProjectContext(cwd) {
250
+ const { name, type, lang } = detectTypeAndLang(cwd);
251
+ // Collect tags based on language
252
+ const tagSet = new Set();
253
+ if (lang !== "general" && lang !== "unknown")
254
+ tagSet.add(lang);
255
+ if (lang === "typescript" || lang === "javascript") {
256
+ extractNpmTags(cwd).forEach((t) => tagSet.add(t));
257
+ }
258
+ if (lang === "go" || type === "Go") {
259
+ extractGoTags(cwd).forEach((t) => tagSet.add(t));
260
+ }
261
+ if (lang === "python" || type === "Python") {
262
+ extractPythonTags(cwd).forEach((t) => tagSet.add(t));
263
+ }
264
+ extractInfraTags(cwd).forEach((t) => tagSet.add(t));
265
+ // Read CLAUDE.md
266
+ const claudeMdPath = join(cwd, "CLAUDE.md");
267
+ const claudeMdContent = readTextSafe(claudeMdPath);
268
+ const preferences = claudeMdContent ? extractPreferences(claudeMdContent) : [];
269
+ // Read rule files
270
+ const ruleFiles = readRuleFiles(cwd);
271
+ // Get installed skills
272
+ const installedSkills = getInstalledSkillNames();
273
+ return {
274
+ name,
275
+ type,
276
+ lang,
277
+ tags: [...tagSet],
278
+ preferences,
279
+ ruleFiles,
280
+ claudeMdContent,
281
+ installedSkills,
282
+ };
283
+ }
@@ -24,4 +24,3 @@ export declare function hasCriticalIssues(content: string): boolean;
24
24
  * Format scan results for display.
25
25
  */
26
26
  export declare function formatScanResults(skillName: string, issues: ScanIssue[]): string;
27
- //# sourceMappingURL=scanner.d.ts.map