@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
@@ -20,9 +20,15 @@ export async function configCommand(action, value, opts) {
20
20
  const envInstallDir = process.env.ARCANA_INSTALL_DIR;
21
21
  const envProvider = process.env.ARCANA_DEFAULT_PROVIDER;
22
22
  const rows = [
23
- [ui.dim("defaultProvider"), config.defaultProvider + (envProvider ? ` ${ui.warn("(overridden by ARCANA_DEFAULT_PROVIDER)")}` : "")],
24
- [ui.dim("installDir"), config.installDir + (envInstallDir ? ` ${ui.warn("(overridden by ARCANA_INSTALL_DIR)")}` : "")],
25
- [ui.dim("providers"), config.providers.map(p => p.name).join(", ")],
23
+ [
24
+ ui.dim("defaultProvider"),
25
+ config.defaultProvider + (envProvider ? ` ${ui.warn("(overridden by ARCANA_DEFAULT_PROVIDER)")}` : ""),
26
+ ],
27
+ [
28
+ ui.dim("installDir"),
29
+ config.installDir + (envInstallDir ? ` ${ui.warn("(overridden by ARCANA_INSTALL_DIR)")}` : ""),
30
+ ],
31
+ [ui.dim("providers"), config.providers.map((p) => p.name).join(", ")],
26
32
  ];
27
33
  table(rows);
28
34
  console.log();
@@ -58,7 +64,11 @@ export async function configCommand(action, value, opts) {
58
64
  }
59
65
  catch (err) {
60
66
  if (opts?.json) {
61
- console.log(JSON.stringify({ action: "reset", success: false, error: err instanceof Error ? err.message : "Failed to remove config" }));
67
+ console.log(JSON.stringify({
68
+ action: "reset",
69
+ success: false,
70
+ error: err instanceof Error ? err.message : "Failed to remove config",
71
+ }));
62
72
  }
63
73
  else {
64
74
  console.log(ui.error(` Failed to reset config: ${err instanceof Error ? err.message : "unknown error"}`));
@@ -115,7 +125,7 @@ export async function configCommand(action, value, opts) {
115
125
  }
116
126
  }
117
127
  if (key === "defaultProvider") {
118
- const providerNames = config.providers.map(p => p.name);
128
+ const providerNames = config.providers.map((p) => p.name);
119
129
  if (!providerNames.includes(value)) {
120
130
  console.log(ui.error(` Provider '${value}' not configured. Add it first with: arcana providers --add owner/repo`));
121
131
  console.log();
@@ -132,4 +142,3 @@ export async function configCommand(action, value, opts) {
132
142
  console.log();
133
143
  }
134
144
  }
135
- //# sourceMappingURL=config.js.map
@@ -1,2 +1 @@
1
1
  export declare function createCommand(name: string): Promise<void>;
2
- //# sourceMappingURL=create.d.ts.map
@@ -65,6 +65,7 @@ export async function createCommand(name) {
65
65
  return "Description is required";
66
66
  if (val.length < 10)
67
67
  return "Too short (minimum 10 chars)";
68
+ return undefined;
68
69
  },
69
70
  });
70
71
  if (p.isCancel(description)) {
@@ -97,4 +98,3 @@ export async function createCommand(name) {
97
98
  p.log.info("Edit SKILL.md to add your skill instructions.");
98
99
  p.outro(`Next: ${chalk.cyan("arcana validate " + name)}`);
99
100
  }
100
- //# sourceMappingURL=create.js.map
@@ -0,0 +1,4 @@
1
+ export declare function diffCommand(skill: string, opts: {
2
+ provider?: string;
3
+ json?: boolean;
4
+ }): Promise<void>;
@@ -0,0 +1,166 @@
1
+ import { existsSync, readFileSync, readdirSync, lstatSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getInstallDir, readSkillMeta } from "../utils/fs.js";
4
+ import { getProvider } from "../registry.js";
5
+ import { loadConfig } from "../utils/config.js";
6
+ import { validateSlug } from "../utils/validate.js";
7
+ function readDirRecursive(dir) {
8
+ const results = [];
9
+ const queue = [{ fullDir: dir, relPrefix: "" }];
10
+ while (queue.length > 0) {
11
+ const { fullDir, relPrefix } = queue.pop();
12
+ let entries;
13
+ try {
14
+ entries = readdirSync(fullDir);
15
+ }
16
+ catch {
17
+ continue;
18
+ }
19
+ for (const entry of entries) {
20
+ const fullPath = join(fullDir, entry);
21
+ try {
22
+ const stat = lstatSync(fullPath);
23
+ if (stat.isSymbolicLink())
24
+ continue;
25
+ const relPath = relPrefix ? `${relPrefix}/${entry}` : entry;
26
+ if (stat.isDirectory()) {
27
+ queue.push({ fullDir: fullPath, relPrefix: relPath });
28
+ }
29
+ else {
30
+ results.push({
31
+ path: relPath,
32
+ content: readFileSync(fullPath, "utf-8"),
33
+ });
34
+ }
35
+ }
36
+ catch {
37
+ // skip unreadable entries
38
+ }
39
+ }
40
+ }
41
+ return results;
42
+ }
43
+ function computeLineDiff(localContent, remoteContent) {
44
+ const localLines = localContent.split("\n");
45
+ const remoteLines = remoteContent.split("\n");
46
+ const localSet = new Set(localLines);
47
+ const remoteSet = new Set(remoteLines);
48
+ let linesAdded = 0;
49
+ let linesRemoved = 0;
50
+ for (const line of remoteLines) {
51
+ if (!localSet.has(line)) {
52
+ linesAdded++;
53
+ }
54
+ }
55
+ for (const line of localLines) {
56
+ if (!remoteSet.has(line)) {
57
+ linesRemoved++;
58
+ }
59
+ }
60
+ return { linesAdded, linesRemoved };
61
+ }
62
+ export async function diffCommand(skill, opts) {
63
+ try {
64
+ validateSlug(skill, "skill name");
65
+ }
66
+ catch (err) {
67
+ console.error(err instanceof Error ? err.message : "Invalid skill name");
68
+ process.exit(1);
69
+ }
70
+ const installDir = getInstallDir();
71
+ const skillDir = join(installDir, skill);
72
+ if (!existsSync(skillDir)) {
73
+ console.error(`Skill "${skill}" is not installed.`);
74
+ process.exit(1);
75
+ }
76
+ const providerName = opts.provider ?? loadConfig().defaultProvider;
77
+ const provider = getProvider(providerName);
78
+ let remoteFiles;
79
+ try {
80
+ remoteFiles = await provider.fetch(skill);
81
+ }
82
+ catch (err) {
83
+ console.error(`Failed to fetch remote skill "${skill}": ${err instanceof Error ? err.message : "unknown error"}`);
84
+ process.exit(1);
85
+ }
86
+ const localFiles = readDirRecursive(skillDir);
87
+ const meta = readSkillMeta(skill);
88
+ const localVersion = meta?.version ?? "0.0.0";
89
+ let remoteVersion = "0.0.0";
90
+ try {
91
+ const remoteInfo = await provider.info(skill);
92
+ if (remoteInfo) {
93
+ remoteVersion = remoteInfo.version;
94
+ }
95
+ }
96
+ catch {
97
+ // keep default
98
+ }
99
+ const localMap = new Map();
100
+ for (const file of localFiles) {
101
+ localMap.set(file.path, file.content);
102
+ }
103
+ const remoteMap = new Map();
104
+ for (const file of remoteFiles) {
105
+ remoteMap.set(file.path, file.content);
106
+ }
107
+ const added = [];
108
+ const removed = [];
109
+ const modified = [];
110
+ for (const [path] of remoteMap) {
111
+ if (!localMap.has(path)) {
112
+ added.push(path);
113
+ }
114
+ }
115
+ for (const [path] of localMap) {
116
+ if (!remoteMap.has(path)) {
117
+ removed.push(path);
118
+ }
119
+ }
120
+ for (const [path, remoteContent] of remoteMap) {
121
+ const localContent = localMap.get(path);
122
+ if (localContent !== undefined && localContent !== remoteContent) {
123
+ const { linesAdded, linesRemoved } = computeLineDiff(localContent, remoteContent);
124
+ modified.push({ path, linesAdded, linesRemoved });
125
+ }
126
+ }
127
+ const result = {
128
+ skill,
129
+ localVersion,
130
+ remoteVersion,
131
+ added,
132
+ removed,
133
+ modified,
134
+ };
135
+ if (opts.json) {
136
+ console.log(JSON.stringify(result, null, 2));
137
+ return;
138
+ }
139
+ // Console output
140
+ console.log(`Diff: ${skill}`);
141
+ console.log(` Local version: ${localVersion}`);
142
+ console.log(` Remote version: ${remoteVersion}`);
143
+ console.log();
144
+ if (added.length === 0 && removed.length === 0 && modified.length === 0) {
145
+ console.log(" No differences found.");
146
+ return;
147
+ }
148
+ if (added.length > 0) {
149
+ console.log(` Added (${added.length}):`);
150
+ for (const path of added) {
151
+ console.log(` + ${path}`);
152
+ }
153
+ }
154
+ if (removed.length > 0) {
155
+ console.log(` Removed (${removed.length}):`);
156
+ for (const path of removed) {
157
+ console.log(` - ${path}`);
158
+ }
159
+ }
160
+ if (modified.length > 0) {
161
+ console.log(` Modified (${modified.length}):`);
162
+ for (const entry of modified) {
163
+ console.log(` ~ ${entry.path} (+${entry.linesAdded} / -${entry.linesRemoved})`);
164
+ }
165
+ }
166
+ }
@@ -3,4 +3,3 @@ export declare function runDoctorChecks(): DoctorCheck[];
3
3
  export declare function doctorCommand(opts?: {
4
4
  json?: boolean;
5
5
  }): Promise<void>;
6
- //# sourceMappingURL=doctor.d.ts.map
@@ -3,20 +3,30 @@ import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { execSync } from "node:child_process";
5
5
  import { ui, banner, suggest } from "../utils/ui.js";
6
- import { getInstallDir, getDirSize, listSymlinks } from "../utils/fs.js";
6
+ import { getInstallDir, getDirSize, listSymlinks, isOrphanedProject } from "../utils/fs.js";
7
7
  function checkNodeVersion() {
8
8
  const major = parseInt(process.version.slice(1));
9
9
  if (major >= 18) {
10
10
  return { name: "Node.js", status: "pass", message: `${process.version}` };
11
11
  }
12
- return { name: "Node.js", status: "fail", message: `${process.version} (need 18+)`, fix: "Install Node.js 18 or later" };
12
+ return {
13
+ name: "Node.js",
14
+ status: "fail",
15
+ message: `${process.version} (need 18+)`,
16
+ fix: "Install Node.js 18 or later",
17
+ };
13
18
  }
14
19
  function checkInstallDir() {
15
20
  const dir = getInstallDir();
16
21
  if (!existsSync(dir)) {
17
- return { name: "Skills directory", status: "warn", message: "~/.agents/skills/ not found", fix: "Run: arcana install --all" };
22
+ return {
23
+ name: "Skills directory",
24
+ status: "warn",
25
+ message: "~/.agents/skills/ not found",
26
+ fix: "Run: arcana install --all",
27
+ };
18
28
  }
19
- const skills = readdirSync(dir).filter(d => statSync(join(dir, d)).isDirectory());
29
+ const skills = readdirSync(dir).filter((d) => statSync(join(dir, d)).isDirectory());
20
30
  return { name: "Skills directory", status: "pass", message: `${skills.length} skills installed` };
21
31
  }
22
32
  function checkBrokenSymlinks() {
@@ -24,9 +34,14 @@ function checkBrokenSymlinks() {
24
34
  if (symlinks.length === 0) {
25
35
  return { name: "Symlinks", status: "pass", message: "No symlink directory" };
26
36
  }
27
- const broken = symlinks.filter(s => s.broken).length;
37
+ const broken = symlinks.filter((s) => s.broken).length;
28
38
  if (broken > 0) {
29
- return { name: "Symlinks", status: "warn", message: `${broken}/${symlinks.length} broken symlinks`, fix: "Run: arcana clean" };
39
+ return {
40
+ name: "Symlinks",
41
+ status: "warn",
42
+ message: `${broken}/${symlinks.length} broken symlinks`,
43
+ fix: "Run: arcana clean",
44
+ };
30
45
  }
31
46
  return { name: "Symlinks", status: "pass", message: `${symlinks.length} symlinks ok` };
32
47
  }
@@ -35,12 +50,22 @@ function checkGitConfig() {
35
50
  const name = execSync("git config user.name", { encoding: "utf-8" }).trim();
36
51
  const email = execSync("git config user.email", { encoding: "utf-8" }).trim();
37
52
  if (!name || !email) {
38
- return { name: "Git config", status: "warn", message: "Missing user.name or user.email", fix: "git config --global user.name \"Your Name\"" };
53
+ return {
54
+ name: "Git config",
55
+ status: "warn",
56
+ message: "Missing user.name or user.email",
57
+ fix: 'git config --global user.name "Your Name"',
58
+ };
39
59
  }
40
60
  return { name: "Git config", status: "pass", message: `${name} <${email}>` };
41
61
  }
42
62
  catch {
43
- return { name: "Git config", status: "warn", message: "Git not found", fix: "Install Git from https://git-scm.com" };
63
+ return {
64
+ name: "Git config",
65
+ status: "warn",
66
+ message: "Git not found",
67
+ fix: "Install Git from https://git-scm.com",
68
+ };
44
69
  }
45
70
  }
46
71
  function checkArcanaConfig() {
@@ -73,10 +98,16 @@ function checkDiskUsage() {
73
98
  }
74
99
  }
75
100
  }
76
- catch { /* skip */ }
101
+ catch {
102
+ /* skip */
103
+ }
77
104
  const mb = (totalSize / (1024 * 1024)).toFixed(1);
78
105
  if (totalSize > DISK_USAGE_THRESHOLD_MB * 1024 * 1024) {
79
- return { name: "Disk usage", status: "warn", message: `${mb} MB across ${dirCount} projects (threshold: ${DISK_USAGE_THRESHOLD_MB} MB). Try: arcana clean` };
106
+ return {
107
+ name: "Disk usage",
108
+ status: "warn",
109
+ message: `${mb} MB across ${dirCount} projects (threshold: ${DISK_USAGE_THRESHOLD_MB} MB). Try: arcana clean`,
110
+ };
80
111
  }
81
112
  return { name: "Disk usage", status: "pass", message: `${mb} MB across ${dirCount} projects` };
82
113
  }
@@ -114,14 +145,12 @@ function checkSkillValidity() {
114
145
  if (invalid === 0) {
115
146
  return { name: "Skill health", status: "pass", message: `${total} skills valid` };
116
147
  }
117
- // Pick the most useful fix command
118
148
  const details = [];
119
149
  let fix;
120
150
  if (missingMd.length > 0) {
121
151
  details.push(`${missingMd.length} missing SKILL.md (${missingMd.join(", ")})`);
122
- fix = missingMd.length === 1
123
- ? `Run: arcana uninstall ${missingMd[0]}`
124
- : `Run: arcana uninstall ${missingMd.join(" ")}`;
152
+ fix =
153
+ missingMd.length === 1 ? `Run: arcana uninstall ${missingMd[0]}` : `Run: arcana uninstall ${missingMd.join(" ")}`;
125
154
  }
126
155
  else {
127
156
  fix = "Run: arcana validate --all --fix";
@@ -142,24 +171,117 @@ function checkSkillSizes() {
142
171
  return { name: "Skill sizes", status: "pass", message: "No skills to check" };
143
172
  }
144
173
  const large = [];
174
+ let totalKB = 0;
145
175
  for (const entry of readdirSync(dir)) {
146
176
  const skillDir = join(dir, entry);
147
177
  if (!statSync(skillDir).isDirectory())
148
178
  continue;
149
179
  const size = getDirSize(skillDir);
150
180
  const kb = size / 1024;
181
+ totalKB += kb;
151
182
  if (kb > 50)
152
183
  large.push({ name: entry, kb });
153
184
  }
154
185
  if (large.length === 0) {
155
- return { name: "Skill sizes", status: "pass", message: "All skills under 50 KB" };
186
+ return {
187
+ name: "Skill sizes",
188
+ status: "pass",
189
+ message: `All skills under 50 KB (total: ${totalKB.toFixed(0)} KB, ~${Math.round((totalKB * 256) / 1000)}K tokens)`,
190
+ };
156
191
  }
157
192
  large.sort((a, b) => b.kb - a.kb);
158
- const top3 = large.slice(0, 3).map(s => `${s.name} (${s.kb.toFixed(0)} KB)`).join(", ");
193
+ const top3 = large
194
+ .slice(0, 3)
195
+ .map((s) => `${s.name} (${s.kb.toFixed(0)} KB)`)
196
+ .join(", ");
159
197
  return {
160
198
  name: "Skill sizes",
161
199
  status: "warn",
162
- message: `${large.length} skills >50 KB (high token usage). Largest: ${top3}`,
200
+ message: `${large.length} skills >50 KB (high token usage). Total: ${totalKB.toFixed(0)} KB (~${Math.round((totalKB * 256) / 1000)}K tokens). Largest: ${top3}`,
201
+ };
202
+ }
203
+ function checkOrphanedProjects() {
204
+ const projectsDir = join(homedir(), ".claude", "projects");
205
+ if (!existsSync(projectsDir)) {
206
+ return { name: "Orphaned projects", status: "pass", message: "No project data" };
207
+ }
208
+ const orphans = [];
209
+ for (const entry of readdirSync(projectsDir)) {
210
+ const full = join(projectsDir, entry);
211
+ if (!statSync(full).isDirectory())
212
+ continue;
213
+ if (entry === "memory" || entry.startsWith("."))
214
+ continue;
215
+ if (isOrphanedProject(entry)) {
216
+ orphans.push({ name: entry, sizeMB: getDirSize(full) / (1024 * 1024) });
217
+ }
218
+ }
219
+ if (orphans.length === 0) {
220
+ return { name: "Orphaned projects", status: "pass", message: "All project dirs have matching source" };
221
+ }
222
+ const totalMB = orphans.reduce((sum, o) => sum + o.sizeMB, 0).toFixed(1);
223
+ return {
224
+ name: "Orphaned projects",
225
+ status: "warn",
226
+ message: `${orphans.length} orphaned project dirs (${totalMB} MB). Source code no longer exists.`,
227
+ fix: "Run: arcana clean",
228
+ };
229
+ }
230
+ function checkSessionBloat() {
231
+ const projectsDir = join(homedir(), ".claude", "projects");
232
+ if (!existsSync(projectsDir)) {
233
+ return { name: "Session bloat", status: "pass", message: "No session data" };
234
+ }
235
+ const bloated = [];
236
+ for (const project of readdirSync(projectsDir)) {
237
+ const projDir = join(projectsDir, project);
238
+ if (!statSync(projDir).isDirectory())
239
+ continue;
240
+ for (const file of readdirSync(projDir)) {
241
+ if (!file.endsWith(".jsonl"))
242
+ continue;
243
+ const stat = statSync(join(projDir, file));
244
+ const mb = stat.size / (1024 * 1024);
245
+ if (mb > 50) {
246
+ bloated.push({ project, file, sizeMB: mb });
247
+ }
248
+ }
249
+ }
250
+ if (bloated.length === 0) {
251
+ return { name: "Session bloat", status: "pass", message: "No session files >50 MB" };
252
+ }
253
+ bloated.sort((a, b) => b.sizeMB - a.sizeMB);
254
+ const top = bloated[0];
255
+ const totalMB = bloated.reduce((sum, b) => sum + b.sizeMB, 0).toFixed(0);
256
+ return {
257
+ name: "Session bloat",
258
+ status: "warn",
259
+ message: `${bloated.length} session files >50 MB (${totalMB} MB total). Largest: ${top.project} (${top.sizeMB.toFixed(0)} MB)`,
260
+ fix: "Run: arcana clean --aggressive",
261
+ };
262
+ }
263
+ function checkAuxiliaryBloat() {
264
+ const claudeDir = join(homedir(), ".claude");
265
+ const dirs = ["file-history", "debug", "shell-snapshots", "todos", "plans"];
266
+ let totalMB = 0;
267
+ const bloated = [];
268
+ for (const dirName of dirs) {
269
+ const dir = join(claudeDir, dirName);
270
+ if (!existsSync(dir))
271
+ continue;
272
+ const sizeMB = getDirSize(dir) / (1024 * 1024);
273
+ totalMB += sizeMB;
274
+ if (sizeMB > 10)
275
+ bloated.push(`${dirName} (${sizeMB.toFixed(0)} MB)`);
276
+ }
277
+ if (totalMB < 10) {
278
+ return { name: "Auxiliary data", status: "pass", message: `${totalMB.toFixed(1)} MB across temp directories` };
279
+ }
280
+ return {
281
+ name: "Auxiliary data",
282
+ status: "warn",
283
+ message: `${totalMB.toFixed(0)} MB in temp directories${bloated.length > 0 ? `. Bloated: ${bloated.join(", ")}` : ""}`,
284
+ fix: "Run: arcana clean",
163
285
  };
164
286
  }
165
287
  export function runDoctorChecks() {
@@ -172,6 +294,9 @@ export function runDoctorChecks() {
172
294
  checkGitConfig(),
173
295
  checkArcanaConfig(),
174
296
  checkDiskUsage(),
297
+ checkOrphanedProjects(),
298
+ checkSessionBloat(),
299
+ checkAuxiliaryBloat(),
175
300
  ];
176
301
  }
177
302
  export async function doctorCommand(opts = {}) {
@@ -181,20 +306,25 @@ export async function doctorCommand(opts = {}) {
181
306
  }
182
307
  const checks = runDoctorChecks();
183
308
  if (opts.json) {
184
- console.log(JSON.stringify({ checks: checks.map((c) => ({ name: c.name, status: c.status, message: c.message, ...(c.fix ? { fix: c.fix } : {}) })) }, null, 2));
309
+ console.log(JSON.stringify({
310
+ checks: checks.map((c) => ({
311
+ name: c.name,
312
+ status: c.status,
313
+ message: c.message,
314
+ ...(c.fix ? { fix: c.fix } : {}),
315
+ })),
316
+ }, null, 2));
185
317
  return;
186
318
  }
187
319
  for (const check of checks) {
188
- const icon = check.status === "pass" ? ui.success("[OK]")
189
- : check.status === "warn" ? ui.warn("[!!]")
190
- : ui.error("[XX]");
320
+ const icon = check.status === "pass" ? ui.success("[OK]") : check.status === "warn" ? ui.warn("[!!]") : ui.error("[XX]");
191
321
  console.log(` ${icon} ${ui.bold(check.name)}: ${check.message}`);
192
322
  if (check.fix) {
193
323
  console.log(ui.dim(` Fix: ${check.fix}`));
194
324
  }
195
325
  }
196
- const fails = checks.filter(c => c.status === "fail").length;
197
- const warns = checks.filter(c => c.status === "warn").length;
326
+ const fails = checks.filter((c) => c.status === "fail").length;
327
+ const warns = checks.filter((c) => c.status === "warn").length;
198
328
  console.log();
199
329
  if (fails > 0) {
200
330
  console.log(ui.error(` ${fails} issue${fails > 1 ? "s" : ""} found`));
@@ -210,4 +340,3 @@ export async function doctorCommand(opts = {}) {
210
340
  suggest("arcana list");
211
341
  }
212
342
  }
213
- //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,4 @@
1
+ export declare function exportCommand(opts: {
2
+ json?: boolean;
3
+ sbom?: boolean;
4
+ }): Promise<void>;
@@ -0,0 +1,66 @@
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getInstallDir, readSkillMeta } from "../utils/fs.js";
4
+ import { readLockfile } from "../utils/integrity.js";
5
+ function getInstalledSkillDirs() {
6
+ const installDir = getInstallDir();
7
+ try {
8
+ return readdirSync(installDir).filter((d) => statSync(join(installDir, d)).isDirectory());
9
+ }
10
+ catch {
11
+ return [];
12
+ }
13
+ }
14
+ export async function exportCommand(opts) {
15
+ const dirs = getInstalledSkillDirs();
16
+ if (dirs.length === 0) {
17
+ console.log(JSON.stringify({ skills: [], message: "No skills installed" }));
18
+ return;
19
+ }
20
+ if (opts.sbom) {
21
+ return exportSbom(dirs);
22
+ }
23
+ return exportManifest(dirs);
24
+ }
25
+ function exportManifest(dirs) {
26
+ const skills = [];
27
+ for (const name of dirs) {
28
+ const meta = readSkillMeta(name);
29
+ skills.push({
30
+ name,
31
+ version: meta?.version ?? "unknown",
32
+ source: meta?.source ?? "local",
33
+ description: meta?.description ?? "",
34
+ });
35
+ }
36
+ const manifest = {
37
+ exportedAt: new Date().toISOString(),
38
+ skillCount: skills.length,
39
+ skills,
40
+ };
41
+ console.log(JSON.stringify(manifest, null, 2));
42
+ }
43
+ function exportSbom(dirs) {
44
+ const lockEntries = readLockfile();
45
+ const lockMap = new Map(lockEntries.map((e) => [e.skill, e]));
46
+ const packages = [];
47
+ for (const name of dirs) {
48
+ const meta = readSkillMeta(name);
49
+ const lock = lockMap.get(name);
50
+ packages.push({
51
+ name,
52
+ version: meta?.version ?? lock?.version ?? "unknown",
53
+ source: meta?.source ?? lock?.source ?? "local",
54
+ hash: lock?.hash ?? "unknown",
55
+ });
56
+ }
57
+ const sbom = {
58
+ spdxVersion: "SPDX-2.3",
59
+ dataLicense: "CC0-1.0",
60
+ name: "arcana-skills-sbom",
61
+ documentNamespace: `https://arcana.dev/sbom/${Date.now()}`,
62
+ createdAt: new Date().toISOString(),
63
+ packages,
64
+ };
65
+ console.log(JSON.stringify(sbom, null, 2));
66
+ }
@@ -0,0 +1,4 @@
1
+ export declare function importCommand(file: string, opts: {
2
+ json?: boolean;
3
+ force?: boolean;
4
+ }): Promise<void>;