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