@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.
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +140 -10
- package/dist/command-registry.d.ts +10 -0
- package/dist/command-registry.js +65 -0
- package/dist/commands/audit.d.ts +0 -1
- package/dist/commands/audit.js +16 -6
- package/dist/commands/benchmark.d.ts +4 -0
- package/dist/commands/benchmark.js +178 -0
- package/dist/commands/clean.d.ts +2 -1
- package/dist/commands/clean.js +198 -47
- package/dist/commands/compact.d.ts +6 -0
- package/dist/commands/compact.js +239 -0
- package/dist/commands/completions.d.ts +3 -0
- package/dist/commands/completions.js +104 -0
- package/dist/commands/config.d.ts +0 -1
- package/dist/commands/config.js +15 -6
- package/dist/commands/create.d.ts +0 -1
- package/dist/commands/create.js +1 -1
- package/dist/commands/diff.d.ts +4 -0
- package/dist/commands/diff.js +166 -0
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/doctor.js +153 -24
- package/dist/commands/export-cmd.d.ts +4 -0
- package/dist/commands/export-cmd.js +66 -0
- package/dist/commands/import-cmd.d.ts +4 -0
- package/dist/commands/import-cmd.js +131 -0
- package/dist/commands/info.d.ts +0 -1
- package/dist/commands/info.js +29 -4
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +156 -117
- package/dist/commands/install.d.ts +1 -1
- package/dist/commands/install.js +118 -205
- package/dist/commands/list.d.ts +0 -1
- package/dist/commands/list.js +12 -4
- package/dist/commands/lock.d.ts +4 -0
- package/dist/commands/lock.js +171 -0
- package/dist/commands/optimize.d.ts +3 -0
- package/dist/commands/optimize.js +356 -0
- package/dist/commands/outdated.d.ts +4 -0
- package/dist/commands/outdated.js +159 -0
- package/dist/commands/profile.d.ts +3 -0
- package/dist/commands/profile.js +274 -0
- package/dist/commands/providers.d.ts +0 -1
- package/dist/commands/providers.js +1 -4
- package/dist/commands/recommend.d.ts +5 -0
- package/dist/commands/recommend.js +96 -0
- package/dist/commands/scan.d.ts +0 -1
- package/dist/commands/scan.js +13 -7
- package/dist/commands/search.d.ts +2 -1
- package/dist/commands/search.js +32 -9
- package/dist/commands/stats.d.ts +0 -1
- package/dist/commands/stats.js +83 -16
- package/dist/commands/team.d.ts +3 -0
- package/dist/commands/team.js +291 -0
- package/dist/commands/uninstall.d.ts +0 -1
- package/dist/commands/uninstall.js +18 -4
- package/dist/commands/update.d.ts +0 -1
- package/dist/commands/update.js +155 -155
- package/dist/commands/validate.d.ts +0 -1
- package/dist/commands/validate.js +14 -6
- package/dist/commands/verify.d.ts +4 -0
- package/dist/commands/verify.js +116 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +13 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/interactive/browse.d.ts +4 -0
- package/dist/interactive/browse.js +103 -0
- package/dist/interactive/categories.d.ts +4 -0
- package/dist/interactive/categories.js +87 -0
- package/dist/interactive/health.d.ts +1 -0
- package/dist/interactive/health.js +57 -0
- package/dist/interactive/helpers.d.ts +11 -0
- package/dist/interactive/helpers.js +66 -0
- package/dist/interactive/index.d.ts +1 -0
- package/dist/interactive/index.js +1 -0
- package/dist/interactive/manage.d.ts +2 -0
- package/dist/interactive/manage.js +187 -0
- package/dist/interactive/menu.d.ts +1 -0
- package/dist/interactive/menu.js +107 -0
- package/dist/interactive/search.d.ts +2 -0
- package/dist/interactive/search.js +66 -0
- package/dist/interactive/setup.d.ts +2 -0
- package/dist/interactive/setup.js +48 -0
- package/dist/interactive/skill-detail.d.ts +5 -0
- package/dist/interactive/skill-detail.js +126 -0
- package/dist/interactive.d.ts +0 -1
- package/dist/interactive.js +89 -66
- package/dist/providers/arcana.d.ts +0 -1
- package/dist/providers/arcana.js +0 -1
- package/dist/providers/base.d.ts +0 -1
- package/dist/providers/base.js +0 -1
- package/dist/providers/github.d.ts +0 -1
- package/dist/providers/github.js +8 -3
- package/dist/registry.d.ts +0 -1
- package/dist/registry.js +1 -4
- package/dist/types.d.ts +10 -1
- package/dist/types.js +0 -1
- package/dist/utils/atomic.d.ts +0 -1
- package/dist/utils/atomic.js +3 -2
- package/dist/utils/cache.d.ts +0 -1
- package/dist/utils/cache.js +3 -2
- package/dist/utils/config.d.ts +2 -1
- package/dist/utils/config.js +30 -5
- package/dist/utils/conflict-check.d.ts +8 -0
- package/dist/utils/conflict-check.js +72 -0
- package/dist/utils/errors.d.ts +0 -1
- package/dist/utils/errors.js +0 -1
- package/dist/utils/frontmatter.d.ts +0 -1
- package/dist/utils/frontmatter.js +37 -10
- package/dist/utils/fs.d.ts +19 -1
- package/dist/utils/fs.js +105 -8
- package/dist/utils/help.d.ts +0 -1
- package/dist/utils/help.js +15 -28
- package/dist/utils/history.d.ts +0 -1
- package/dist/utils/history.js +0 -1
- package/dist/utils/http.d.ts +0 -1
- package/dist/utils/http.js +14 -5
- package/dist/utils/install-core.d.ts +48 -0
- package/dist/utils/install-core.js +108 -0
- package/dist/utils/integrity.d.ts +17 -0
- package/dist/utils/integrity.js +84 -0
- package/dist/utils/parallel.d.ts +0 -1
- package/dist/utils/parallel.js +0 -1
- package/dist/utils/project-context.d.ts +19 -0
- package/dist/utils/project-context.js +283 -0
- package/dist/utils/scanner.d.ts +0 -1
- package/dist/utils/scanner.js +138 -10
- package/dist/utils/scoring.d.ts +10 -0
- package/dist/utils/scoring.js +84 -0
- package/dist/utils/ui.d.ts +0 -1
- package/dist/utils/ui.js +11 -4
- package/dist/utils/validate.d.ts +0 -1
- package/dist/utils/validate.js +4 -1
- package/package.json +19 -7
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/audit.d.ts.map +0 -1
- package/dist/commands/audit.js.map +0 -1
- package/dist/commands/audit.test.d.ts +0 -2
- package/dist/commands/audit.test.d.ts.map +0 -1
- package/dist/commands/audit.test.js +0 -217
- package/dist/commands/audit.test.js.map +0 -1
- package/dist/commands/clean.d.ts.map +0 -1
- package/dist/commands/clean.js.map +0 -1
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/create.d.ts.map +0 -1
- package/dist/commands/create.js.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/info.d.ts.map +0 -1
- package/dist/commands/info.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/install.d.ts.map +0 -1
- package/dist/commands/install.js.map +0 -1
- package/dist/commands/list.d.ts.map +0 -1
- package/dist/commands/list.js.map +0 -1
- package/dist/commands/providers.d.ts.map +0 -1
- package/dist/commands/providers.js.map +0 -1
- package/dist/commands/scan.d.ts.map +0 -1
- package/dist/commands/scan.js.map +0 -1
- package/dist/commands/search.d.ts.map +0 -1
- package/dist/commands/search.js.map +0 -1
- package/dist/commands/stats.d.ts.map +0 -1
- package/dist/commands/stats.js.map +0 -1
- package/dist/commands/uninstall.d.ts.map +0 -1
- package/dist/commands/uninstall.js.map +0 -1
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/validate.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/interactive.d.ts.map +0 -1
- package/dist/interactive.js.map +0 -1
- package/dist/providers/arcana.d.ts.map +0 -1
- package/dist/providers/arcana.js.map +0 -1
- package/dist/providers/base.d.ts.map +0 -1
- package/dist/providers/base.js.map +0 -1
- package/dist/providers/github.d.ts.map +0 -1
- package/dist/providers/github.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils/atomic.d.ts.map +0 -1
- package/dist/utils/atomic.js.map +0 -1
- package/dist/utils/atomic.test.d.ts +0 -2
- package/dist/utils/atomic.test.d.ts.map +0 -1
- package/dist/utils/atomic.test.js +0 -31
- package/dist/utils/atomic.test.js.map +0 -1
- package/dist/utils/cache.d.ts.map +0 -1
- package/dist/utils/cache.js.map +0 -1
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/config.test.d.ts +0 -2
- package/dist/utils/config.test.d.ts.map +0 -1
- package/dist/utils/config.test.js +0 -38
- package/dist/utils/config.test.js.map +0 -1
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/frontmatter.d.ts.map +0 -1
- package/dist/utils/frontmatter.js.map +0 -1
- package/dist/utils/frontmatter.test.d.ts +0 -2
- package/dist/utils/frontmatter.test.d.ts.map +0 -1
- package/dist/utils/frontmatter.test.js +0 -152
- package/dist/utils/frontmatter.test.js.map +0 -1
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/fs.test.d.ts +0 -2
- package/dist/utils/fs.test.d.ts.map +0 -1
- package/dist/utils/fs.test.js +0 -145
- package/dist/utils/fs.test.js.map +0 -1
- package/dist/utils/help.d.ts.map +0 -1
- package/dist/utils/help.js.map +0 -1
- package/dist/utils/help.test.d.ts +0 -2
- package/dist/utils/help.test.d.ts.map +0 -1
- package/dist/utils/help.test.js +0 -66
- package/dist/utils/help.test.js.map +0 -1
- package/dist/utils/history.d.ts.map +0 -1
- package/dist/utils/history.js.map +0 -1
- package/dist/utils/http.d.ts.map +0 -1
- package/dist/utils/http.js.map +0 -1
- package/dist/utils/http.test.d.ts +0 -2
- package/dist/utils/http.test.d.ts.map +0 -1
- package/dist/utils/http.test.js +0 -55
- package/dist/utils/http.test.js.map +0 -1
- package/dist/utils/parallel.d.ts.map +0 -1
- package/dist/utils/parallel.js.map +0 -1
- package/dist/utils/scanner.d.ts.map +0 -1
- package/dist/utils/scanner.js.map +0 -1
- package/dist/utils/ui.d.ts.map +0 -1
- package/dist/utils/ui.js.map +0 -1
- package/dist/utils/ui.test.d.ts +0 -2
- package/dist/utils/ui.test.d.ts.map +0 -1
- package/dist/utils/ui.test.js +0 -31
- package/dist/utils/ui.test.js.map +0 -1
- package/dist/utils/validate.d.ts.map +0 -1
- package/dist/utils/validate.js.map +0 -1
package/dist/commands/config.js
CHANGED
|
@@ -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
|
-
[
|
|
24
|
-
|
|
25
|
-
|
|
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({
|
|
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
|
package/dist/commands/create.js
CHANGED
|
@@ -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,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
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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
|
|
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({
|
|
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,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
|
+
}
|