@sporesec/arcana 2.4.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +124 -9
- package/dist/command-registry.d.ts +10 -0
- package/dist/command-registry.js +65 -0
- package/dist/commands/audit.d.ts +2 -3
- package/dist/commands/audit.js +47 -14
- package/dist/commands/benchmark.d.ts +4 -0
- package/dist/commands/benchmark.js +178 -0
- package/dist/commands/clean.d.ts +0 -1
- package/dist/commands/clean.js +19 -8
- package/dist/commands/compact.d.ts +2 -1
- package/dist/commands/compact.js +74 -14
- 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 +64 -23
- 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 +26 -33
- 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 +0 -1
- package/dist/commands/optimize.js +111 -20
- 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 +24 -20
- 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 +3 -1
- package/dist/commands/validate.js +90 -15
- 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 +44 -14
- package/dist/utils/fs.d.ts +0 -1
- package/dist/utils/fs.js +30 -11
- 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/quality.d.ts +27 -0
- package/dist/utils/quality.js +174 -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 +74 -62
- 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/compact.d.ts.map +0 -1
- package/dist/commands/compact.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/optimize.d.ts.map +0 -1
- package/dist/commands/optimize.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/utils/errors.js
CHANGED
|
@@ -9,4 +9,3 @@ export declare function extractFrontmatter(content: string): {
|
|
|
9
9
|
export declare function parseFrontmatter(raw: string): SkillFrontmatter | null;
|
|
10
10
|
export declare function fixSkillFrontmatter(content: string): string;
|
|
11
11
|
export declare function validateSkillDir(skillDir: string, skillName: string): ValidationResult;
|
|
12
|
-
//# sourceMappingURL=frontmatter.d.ts.map
|
|
@@ -40,7 +40,13 @@ export function parseFrontmatter(raw) {
|
|
|
40
40
|
if (descMatch?.[1] !== undefined) {
|
|
41
41
|
let value = descMatch[1].trim();
|
|
42
42
|
// Handle YAML multiline: |, >, or bare indented continuation
|
|
43
|
-
if (value === "|" ||
|
|
43
|
+
if (value === "|" ||
|
|
44
|
+
value === ">" ||
|
|
45
|
+
value === "|-" ||
|
|
46
|
+
value === "|+" ||
|
|
47
|
+
value === ">-" ||
|
|
48
|
+
value === ">+" ||
|
|
49
|
+
value === "") {
|
|
44
50
|
const multilineLines = [];
|
|
45
51
|
for (let j = i + 1; j < lines.length; j++) {
|
|
46
52
|
const next = lines[j];
|
|
@@ -93,12 +99,7 @@ export function fixSkillFrontmatter(content) {
|
|
|
93
99
|
if (!parsed)
|
|
94
100
|
return content;
|
|
95
101
|
// Rebuild clean frontmatter with only name and description
|
|
96
|
-
const cleanFm = [
|
|
97
|
-
FM_DELIMITER,
|
|
98
|
-
`name: ${parsed.name}`,
|
|
99
|
-
`description: ${parsed.description}`,
|
|
100
|
-
FM_DELIMITER,
|
|
101
|
-
].join("\n");
|
|
102
|
+
const cleanFm = [FM_DELIMITER, `name: ${parsed.name}`, `description: ${parsed.description}`, FM_DELIMITER].join("\n");
|
|
102
103
|
return cleanFm + "\n" + extracted.body.replace(/^\n+/, "\n");
|
|
103
104
|
}
|
|
104
105
|
export function validateSkillDir(skillDir, skillName) {
|
|
@@ -137,20 +138,40 @@ export function validateSkillDir(skillDir, skillName) {
|
|
|
137
138
|
return result;
|
|
138
139
|
}
|
|
139
140
|
if (!parsed.description) {
|
|
140
|
-
result.
|
|
141
|
+
result.valid = false;
|
|
142
|
+
result.errors.push("Missing description in frontmatter");
|
|
141
143
|
}
|
|
142
144
|
else if (parsed.description.length < MIN_DESC_LENGTH) {
|
|
143
|
-
result.
|
|
145
|
+
result.valid = false;
|
|
146
|
+
result.errors.push(`Description too short (${parsed.description.length} chars, minimum ${MIN_DESC_LENGTH})`);
|
|
144
147
|
}
|
|
145
148
|
else if (parsed.description.length > MAX_DESC_LENGTH) {
|
|
146
|
-
result.
|
|
149
|
+
result.valid = false;
|
|
150
|
+
result.errors.push(`Description too long (${parsed.description.length} chars, max ${MAX_DESC_LENGTH})`);
|
|
147
151
|
}
|
|
148
|
-
// Check for non-standard fields
|
|
152
|
+
// Check for non-standard fields (metadata is invalid per spec)
|
|
149
153
|
const standardFields = ["name", "description"];
|
|
154
|
+
const VALID_FIELDS = [
|
|
155
|
+
"name",
|
|
156
|
+
"description",
|
|
157
|
+
"argument-hint",
|
|
158
|
+
"compatibility",
|
|
159
|
+
"disable-model-invocation",
|
|
160
|
+
"license",
|
|
161
|
+
"user-invokable",
|
|
162
|
+
];
|
|
150
163
|
for (const line of extracted.raw.split("\n")) {
|
|
151
164
|
const keyMatch = line.match(/^(\w[\w-]*):/);
|
|
152
165
|
if (keyMatch?.[1] && !standardFields.includes(keyMatch[1])) {
|
|
153
|
-
|
|
166
|
+
if (keyMatch[1] === "metadata") {
|
|
167
|
+
result.warnings.push("Invalid field: metadata (not allowed in frontmatter)");
|
|
168
|
+
}
|
|
169
|
+
else if (!VALID_FIELDS.includes(keyMatch[1])) {
|
|
170
|
+
result.infos.push(`Non-standard field: ${keyMatch[1]}`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
result.infos.push(`Optional field: ${keyMatch[1]}`);
|
|
174
|
+
}
|
|
154
175
|
}
|
|
155
176
|
}
|
|
156
177
|
if (parsed.name !== skillName) {
|
|
@@ -163,10 +184,19 @@ export function validateSkillDir(skillDir, skillName) {
|
|
|
163
184
|
result.warnings.push("SKILL.md body is very short (less than 50 chars)");
|
|
164
185
|
}
|
|
165
186
|
if (extracted.body.trim().length >= 50 && !extracted.body.includes("##")) {
|
|
166
|
-
result.
|
|
187
|
+
result.warnings.push("Body has no ## headings (required for structure)");
|
|
188
|
+
}
|
|
189
|
+
// Check for code blocks (quality signal)
|
|
190
|
+
if (extracted.body.trim().length >= 50 && !extracted.body.includes("```")) {
|
|
191
|
+
result.warnings.push("No code blocks found (skills must include code examples)");
|
|
192
|
+
}
|
|
193
|
+
// Check for BAD/GOOD pattern examples
|
|
194
|
+
const hasPattern = /(?:BAD|GOOD|WRONG|RIGHT|AVOID|PREFER|DO NOT|INSTEAD)/i.test(extracted.body) ||
|
|
195
|
+
/<!--\s*(?:bad|good)\s*-->/i.test(extracted.body);
|
|
196
|
+
if (extracted.body.trim().length >= 100 && !hasPattern) {
|
|
197
|
+
result.infos.push("No BAD/GOOD contrast patterns found (recommended for teaching)");
|
|
167
198
|
}
|
|
168
199
|
if (result.errors.length > 0)
|
|
169
200
|
result.valid = false;
|
|
170
201
|
return result;
|
|
171
202
|
}
|
|
172
|
-
//# sourceMappingURL=frontmatter.js.map
|
package/dist/utils/fs.d.ts
CHANGED
package/dist/utils/fs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, renameSync, lstatSync, readlinkSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, renameSync, lstatSync, readlinkSync, } from "node:fs";
|
|
2
2
|
import { join, dirname, resolve, sep } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { loadConfig } from "../utils/config.js";
|
|
@@ -26,10 +26,14 @@ export function getDirSize(dir) {
|
|
|
26
26
|
else
|
|
27
27
|
size += stat.size;
|
|
28
28
|
}
|
|
29
|
-
catch {
|
|
29
|
+
catch {
|
|
30
|
+
/* skip unreadable entries */
|
|
31
|
+
}
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
|
-
catch {
|
|
34
|
+
catch {
|
|
35
|
+
/* skip unreadable dirs */
|
|
36
|
+
}
|
|
33
37
|
}
|
|
34
38
|
return size;
|
|
35
39
|
}
|
|
@@ -45,14 +49,18 @@ export function installSkill(skillName, files) {
|
|
|
45
49
|
try {
|
|
46
50
|
for (const file of files) {
|
|
47
51
|
// Reject paths containing .. before resolving
|
|
48
|
-
if (file.path.includes("..") ||
|
|
52
|
+
if (file.path.includes("..") ||
|
|
53
|
+
file.path.includes("~") ||
|
|
54
|
+
file.path.startsWith("\\\\") ||
|
|
55
|
+
file.path.startsWith("//")) {
|
|
49
56
|
throw new Error(`Path traversal blocked: ${file.path}`);
|
|
50
57
|
}
|
|
51
58
|
const filePath = resolve(tempDir, file.path);
|
|
52
59
|
// Normalize to lowercase on Windows for case-insensitive comparison
|
|
53
60
|
const normalizedFile = process.platform === "win32" ? filePath.toLowerCase() : filePath;
|
|
54
61
|
const normalizedTemp = process.platform === "win32" ? (tempDir + sep).toLowerCase() : tempDir + sep;
|
|
55
|
-
if (!normalizedFile.startsWith(normalizedTemp) &&
|
|
62
|
+
if (!normalizedFile.startsWith(normalizedTemp) &&
|
|
63
|
+
normalizedFile !== (process.platform === "win32" ? tempDir.toLowerCase() : tempDir)) {
|
|
56
64
|
throw new Error(`Path traversal blocked: ${file.path}`);
|
|
57
65
|
}
|
|
58
66
|
const dir = dirname(filePath);
|
|
@@ -72,7 +80,9 @@ export function installSkill(skillName, files) {
|
|
|
72
80
|
try {
|
|
73
81
|
rmSync(tempDir, { recursive: true, force: true });
|
|
74
82
|
}
|
|
75
|
-
catch {
|
|
83
|
+
catch {
|
|
84
|
+
/* best-effort */
|
|
85
|
+
}
|
|
76
86
|
throw err;
|
|
77
87
|
}
|
|
78
88
|
return skillDir;
|
|
@@ -124,13 +134,21 @@ export function listFilesByAge(dir, ext, olderThanDays) {
|
|
|
124
134
|
continue;
|
|
125
135
|
const age = now - stat.mtimeMs;
|
|
126
136
|
if (age > cutoff) {
|
|
127
|
-
results.push({
|
|
137
|
+
results.push({
|
|
138
|
+
path: full,
|
|
139
|
+
sizeMB: stat.size / (1024 * 1024),
|
|
140
|
+
daysOld: Math.floor(age / (24 * 60 * 60 * 1000)),
|
|
141
|
+
});
|
|
128
142
|
}
|
|
129
143
|
}
|
|
130
|
-
catch {
|
|
144
|
+
catch {
|
|
145
|
+
/* skip */
|
|
146
|
+
}
|
|
131
147
|
}
|
|
132
148
|
}
|
|
133
|
-
catch {
|
|
149
|
+
catch {
|
|
150
|
+
/* skip */
|
|
151
|
+
}
|
|
134
152
|
}
|
|
135
153
|
return results;
|
|
136
154
|
}
|
|
@@ -189,8 +207,9 @@ export function listSymlinks() {
|
|
|
189
207
|
results.push({ name: entry, fullPath, target, broken: !existsSync(target) });
|
|
190
208
|
}
|
|
191
209
|
}
|
|
192
|
-
catch {
|
|
210
|
+
catch {
|
|
211
|
+
/* skip unreadable */
|
|
212
|
+
}
|
|
193
213
|
}
|
|
194
214
|
return results;
|
|
195
215
|
}
|
|
196
|
-
//# sourceMappingURL=fs.js.map
|
package/dist/utils/help.d.ts
CHANGED
package/dist/utils/help.js
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { ui } from "./ui.js";
|
|
7
|
+
import { getGroupedCommands } from "../command-registry.js";
|
|
7
8
|
const noColor = !!(process.env.NO_COLOR || process.env.TERM === "dumb");
|
|
8
9
|
function amberShade(hex, text) {
|
|
9
10
|
if (noColor)
|
|
@@ -32,30 +33,12 @@ export function renderBanner() {
|
|
|
32
33
|
}
|
|
33
34
|
return BANNER_LINES.map((line, i) => ` ${amberShade(AMBER_HEXES[i], line)}`).join("\n");
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
],
|
|
40
|
-
|
|
41
|
-
{ cmd: "list", desc: "List available skills" },
|
|
42
|
-
{ cmd: "search <query>", desc: "Search across providers" },
|
|
43
|
-
{ cmd: "info <skill>", desc: "Show skill details" },
|
|
44
|
-
{ cmd: "install [skills...]", desc: "Install one or more skills" },
|
|
45
|
-
{ cmd: "update [skills...]", desc: "Update installed skills" },
|
|
46
|
-
{ cmd: "uninstall [skills...]", desc: "Remove one or more skills" },
|
|
47
|
-
],
|
|
48
|
-
DEVELOPMENT: [
|
|
49
|
-
{ cmd: "create <name>", desc: "Create a new skill from template" },
|
|
50
|
-
{ cmd: "validate [skill]", desc: "Validate skill structure" },
|
|
51
|
-
{ cmd: "audit [skill]", desc: "Audit skill quality" },
|
|
52
|
-
],
|
|
53
|
-
CONFIGURATION: [
|
|
54
|
-
{ cmd: "config [key] [val]", desc: "View or modify configuration" },
|
|
55
|
-
{ cmd: "providers", desc: "Manage skill providers" },
|
|
56
|
-
{ cmd: "clean", desc: "Remove orphaned data" },
|
|
57
|
-
{ cmd: "stats", desc: "Show session analytics" },
|
|
58
|
-
],
|
|
36
|
+
// Help groups: subset of registry for --help display (keeps output scannable)
|
|
37
|
+
const HELP_GROUPS = {
|
|
38
|
+
"GETTING STARTED": ["init", "doctor"],
|
|
39
|
+
SKILLS: ["list", "search", "info", "install", "update", "uninstall", "recommend"],
|
|
40
|
+
DEVELOPMENT: ["create", "validate", "audit"],
|
|
41
|
+
CONFIGURATION: ["config", "providers", "clean", "stats"],
|
|
59
42
|
};
|
|
60
43
|
const EXAMPLES = [
|
|
61
44
|
"$ arcana install code-reviewer typescript golang",
|
|
@@ -74,11 +57,16 @@ export function buildCustomHelp(version) {
|
|
|
74
57
|
lines.push("");
|
|
75
58
|
lines.push(` ${ui.dim("USAGE")}`);
|
|
76
59
|
lines.push(" arcana <command> [options]");
|
|
77
|
-
|
|
60
|
+
const allCommands = getGroupedCommands();
|
|
61
|
+
const allFlat = Object.values(allCommands).flat();
|
|
62
|
+
for (const [group, names] of Object.entries(HELP_GROUPS)) {
|
|
78
63
|
lines.push("");
|
|
79
64
|
lines.push(` ${ui.dim(group)}`);
|
|
80
|
-
for (const
|
|
81
|
-
|
|
65
|
+
for (const name of names) {
|
|
66
|
+
const entry = allFlat.find((c) => c.name === name);
|
|
67
|
+
if (!entry)
|
|
68
|
+
continue;
|
|
69
|
+
lines.push(` ${ui.cyan(padRight(entry.usage, 22))}${ui.dim(entry.description)}`);
|
|
82
70
|
}
|
|
83
71
|
}
|
|
84
72
|
lines.push("");
|
|
@@ -114,4 +102,3 @@ export function showWelcome(version) {
|
|
|
114
102
|
p.log.info("They install on-demand and only load when relevant, not all at once.");
|
|
115
103
|
console.log();
|
|
116
104
|
}
|
|
117
|
-
//# sourceMappingURL=help.js.map
|
package/dist/utils/history.d.ts
CHANGED
|
@@ -7,4 +7,3 @@ export declare function readHistory(): HistoryEntry[];
|
|
|
7
7
|
export declare function appendHistory(action: string, target?: string): void;
|
|
8
8
|
export declare function clearHistory(): void;
|
|
9
9
|
export declare function getRecentSkills(limit?: number): string[];
|
|
10
|
-
//# sourceMappingURL=history.d.ts.map
|
package/dist/utils/history.js
CHANGED
package/dist/utils/http.d.ts
CHANGED
package/dist/utils/http.js
CHANGED
|
@@ -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" ||
|
|
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 {
|
|
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 = [
|
|
127
|
-
|
|
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
|
+
}
|
package/dist/utils/parallel.d.ts
CHANGED
package/dist/utils/parallel.js
CHANGED
|
@@ -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;
|