@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MarketplacePlugin } from "../types.js";
|
|
2
|
+
export interface CrossValidationIssue {
|
|
3
|
+
level: "error" | "warning";
|
|
4
|
+
category: "marketplace-drift" | "orphan" | "companion" | "duplicate-desc";
|
|
5
|
+
skill: string;
|
|
6
|
+
detail: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Jaccard word-level similarity between two strings.
|
|
10
|
+
* Returns 0.0 (completely different) to 1.0 (identical).
|
|
11
|
+
*/
|
|
12
|
+
export declare function jaccardSimilarity(a: string, b: string): number;
|
|
13
|
+
/**
|
|
14
|
+
* Validate companion references in marketplace plugins.
|
|
15
|
+
* Every companion must reference an existing plugin name.
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateCompanions(plugins: MarketplacePlugin[]): CrossValidationIssue[];
|
|
18
|
+
/**
|
|
19
|
+
* Check description sync between SKILL.md frontmatter and marketplace.json.
|
|
20
|
+
* Returns an issue if similarity is below 0.5.
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateDescriptionSync(skillName: string, frontmatterDesc: string, marketplaceDesc: string): CrossValidationIssue | null;
|
|
23
|
+
/**
|
|
24
|
+
* Cross-validate skill directories against marketplace.json.
|
|
25
|
+
* Checks: orphans, companions, description drift, near-duplicates.
|
|
26
|
+
*/
|
|
27
|
+
export declare function crossValidate(skillsDir: string, marketplacePath: string): CrossValidationIssue[];
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { extractFrontmatter, parseFrontmatter } from "./frontmatter.js";
|
|
4
|
+
/**
|
|
5
|
+
* Jaccard word-level similarity between two strings.
|
|
6
|
+
* Returns 0.0 (completely different) to 1.0 (identical).
|
|
7
|
+
*/
|
|
8
|
+
export function jaccardSimilarity(a, b) {
|
|
9
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
|
|
10
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
|
|
11
|
+
if (wordsA.size === 0 && wordsB.size === 0)
|
|
12
|
+
return 1.0;
|
|
13
|
+
if (wordsA.size === 0 || wordsB.size === 0)
|
|
14
|
+
return 0.0;
|
|
15
|
+
let intersection = 0;
|
|
16
|
+
for (const w of wordsA) {
|
|
17
|
+
if (wordsB.has(w))
|
|
18
|
+
intersection++;
|
|
19
|
+
}
|
|
20
|
+
const union = wordsA.size + wordsB.size - intersection;
|
|
21
|
+
return union === 0 ? 0 : intersection / union;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validate companion references in marketplace plugins.
|
|
25
|
+
* Every companion must reference an existing plugin name.
|
|
26
|
+
*/
|
|
27
|
+
export function validateCompanions(plugins) {
|
|
28
|
+
const issues = [];
|
|
29
|
+
const names = new Set(plugins.map((p) => p.name));
|
|
30
|
+
for (const plugin of plugins) {
|
|
31
|
+
if (!plugin.companions)
|
|
32
|
+
continue;
|
|
33
|
+
for (const companion of plugin.companions) {
|
|
34
|
+
if (!names.has(companion)) {
|
|
35
|
+
issues.push({
|
|
36
|
+
level: "error",
|
|
37
|
+
category: "companion",
|
|
38
|
+
skill: plugin.name,
|
|
39
|
+
detail: `Companion "${companion}" does not exist in marketplace`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return issues;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check description sync between SKILL.md frontmatter and marketplace.json.
|
|
48
|
+
* Returns an issue if similarity is below 0.5.
|
|
49
|
+
*/
|
|
50
|
+
export function validateDescriptionSync(skillName, frontmatterDesc, marketplaceDesc) {
|
|
51
|
+
if (!frontmatterDesc || !marketplaceDesc)
|
|
52
|
+
return null;
|
|
53
|
+
const similarity = jaccardSimilarity(frontmatterDesc, marketplaceDesc);
|
|
54
|
+
if (similarity < 0.5) {
|
|
55
|
+
return {
|
|
56
|
+
level: "warning",
|
|
57
|
+
category: "marketplace-drift",
|
|
58
|
+
skill: skillName,
|
|
59
|
+
detail: `Description drift (${Math.round(similarity * 100)}% similarity) between SKILL.md and marketplace.json`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Cross-validate skill directories against marketplace.json.
|
|
66
|
+
* Checks: orphans, companions, description drift, near-duplicates.
|
|
67
|
+
*/
|
|
68
|
+
export function crossValidate(skillsDir, marketplacePath) {
|
|
69
|
+
const issues = [];
|
|
70
|
+
// Load marketplace
|
|
71
|
+
let marketplace;
|
|
72
|
+
try {
|
|
73
|
+
marketplace = JSON.parse(readFileSync(marketplacePath, "utf-8"));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
issues.push({
|
|
77
|
+
level: "error",
|
|
78
|
+
category: "orphan",
|
|
79
|
+
skill: "marketplace.json",
|
|
80
|
+
detail: "Cannot read or parse marketplace.json",
|
|
81
|
+
});
|
|
82
|
+
return issues;
|
|
83
|
+
}
|
|
84
|
+
const pluginNames = new Set(marketplace.plugins.map((p) => p.name));
|
|
85
|
+
const pluginMap = new Map(marketplace.plugins.map((p) => [p.name, p]));
|
|
86
|
+
// Get skill directories
|
|
87
|
+
let skillDirs;
|
|
88
|
+
try {
|
|
89
|
+
skillDirs = readdirSync(skillsDir).filter((d) => {
|
|
90
|
+
try {
|
|
91
|
+
return statSync(join(skillsDir, d)).isDirectory();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
issues.push({
|
|
100
|
+
level: "error",
|
|
101
|
+
category: "orphan",
|
|
102
|
+
skill: "skills/",
|
|
103
|
+
detail: "Cannot read skills directory",
|
|
104
|
+
});
|
|
105
|
+
return issues;
|
|
106
|
+
}
|
|
107
|
+
const dirNames = new Set(skillDirs);
|
|
108
|
+
// 1. Orphan directories (dir exists, no marketplace entry)
|
|
109
|
+
for (const dir of skillDirs) {
|
|
110
|
+
if (!pluginNames.has(dir)) {
|
|
111
|
+
issues.push({
|
|
112
|
+
level: "error",
|
|
113
|
+
category: "orphan",
|
|
114
|
+
skill: dir,
|
|
115
|
+
detail: "Skill directory exists but no marketplace.json entry",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// 2. Orphan entries (marketplace entry, no dir)
|
|
120
|
+
for (const name of pluginNames) {
|
|
121
|
+
if (!dirNames.has(name)) {
|
|
122
|
+
issues.push({
|
|
123
|
+
level: "error",
|
|
124
|
+
category: "orphan",
|
|
125
|
+
skill: name,
|
|
126
|
+
detail: "Marketplace entry exists but no skill directory",
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// 3. Companion validation
|
|
131
|
+
issues.push(...validateCompanions(marketplace.plugins));
|
|
132
|
+
// 4. Description drift (SKILL.md frontmatter vs marketplace.json)
|
|
133
|
+
for (const dir of skillDirs) {
|
|
134
|
+
const plugin = pluginMap.get(dir);
|
|
135
|
+
if (!plugin)
|
|
136
|
+
continue;
|
|
137
|
+
const skillMd = join(skillsDir, dir, "SKILL.md");
|
|
138
|
+
if (!existsSync(skillMd))
|
|
139
|
+
continue;
|
|
140
|
+
try {
|
|
141
|
+
const content = readFileSync(skillMd, "utf-8");
|
|
142
|
+
const extracted = extractFrontmatter(content);
|
|
143
|
+
if (!extracted)
|
|
144
|
+
continue;
|
|
145
|
+
const parsed = parseFrontmatter(extracted.raw);
|
|
146
|
+
if (!parsed?.description)
|
|
147
|
+
continue;
|
|
148
|
+
const driftIssue = validateDescriptionSync(dir, parsed.description, plugin.description);
|
|
149
|
+
if (driftIssue)
|
|
150
|
+
issues.push(driftIssue);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* skip unreadable */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// 5. Near-duplicate descriptions across skills
|
|
157
|
+
const descriptions = marketplace.plugins.map((p) => ({ name: p.name, desc: p.description }));
|
|
158
|
+
for (let i = 0; i < descriptions.length; i++) {
|
|
159
|
+
for (let j = i + 1; j < descriptions.length; j++) {
|
|
160
|
+
const a = descriptions[i];
|
|
161
|
+
const b = descriptions[j];
|
|
162
|
+
const sim = jaccardSimilarity(a.desc, b.desc);
|
|
163
|
+
if (sim > 0.85) {
|
|
164
|
+
issues.push({
|
|
165
|
+
level: "warning",
|
|
166
|
+
category: "duplicate-desc",
|
|
167
|
+
skill: a.name,
|
|
168
|
+
detail: `Near-duplicate description with ${b.name} (${Math.round(sim * 100)}% similarity)`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return issues;
|
|
174
|
+
}
|
package/dist/utils/scanner.d.ts
CHANGED
package/dist/utils/scanner.js
CHANGED
|
@@ -89,7 +89,7 @@ const PATTERNS = [
|
|
|
89
89
|
level: "high",
|
|
90
90
|
category: "Prompt injection",
|
|
91
91
|
detail: "System message impersonation",
|
|
92
|
-
regex: /\[system\]
|
|
92
|
+
regex: /\[system\]|<system>|SYSTEM:\s+/,
|
|
93
93
|
},
|
|
94
94
|
// HIGH: Hardcoded secrets
|
|
95
95
|
{
|
|
@@ -136,6 +136,138 @@ const PATTERNS = [
|
|
|
136
136
|
detail: "Dynamic remote instruction loading",
|
|
137
137
|
regex: /(?:curl|wget|fetch)\s+[^\n]*(?:instructions|config|setup)\.(?:md|txt|sh|yaml)/i,
|
|
138
138
|
},
|
|
139
|
+
// =========================================================================
|
|
140
|
+
// v2.4.2 Extended patterns (Snyk ToxicSkills deep coverage)
|
|
141
|
+
// =========================================================================
|
|
142
|
+
// CRITICAL: Additional malicious code patterns
|
|
143
|
+
{
|
|
144
|
+
level: "critical",
|
|
145
|
+
category: "Malicious code",
|
|
146
|
+
detail: "Curl/wget piped to source",
|
|
147
|
+
regex: /(?:curl|wget)\s+[^\n|]*\|\s*source\b/i,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
level: "critical",
|
|
151
|
+
category: "Malicious code",
|
|
152
|
+
detail: "Nested base64 decoding (obfuscation chain)",
|
|
153
|
+
regex: /base64\s+-d[^\n]*\|\s*base64\s+-d/i,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
level: "critical",
|
|
157
|
+
category: "Malicious code",
|
|
158
|
+
detail: "Password-protected 7z or GPG-encrypted archive",
|
|
159
|
+
regex: /(?:7z\s+x\s+-p\S+|gpg\s+--decrypt\s|gpg\s+-d\s)/i,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
level: "critical",
|
|
163
|
+
category: "Suspicious download",
|
|
164
|
+
detail: "GitHub release download from unverified source",
|
|
165
|
+
regex: /github\.com\/[^\s/]+\/[^\s/]+\/releases\/download\//i,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
level: "critical",
|
|
169
|
+
category: "Malicious code",
|
|
170
|
+
detail: "PowerShell encoded command execution",
|
|
171
|
+
regex: /powershell[^\n]*-[Ee](?:nc|ncodedCommand)\s/i,
|
|
172
|
+
},
|
|
173
|
+
// HIGH: Extended credential and prompt injection patterns
|
|
174
|
+
{
|
|
175
|
+
level: "high",
|
|
176
|
+
category: "Credential theft",
|
|
177
|
+
detail: "Instructing to print/echo API keys or secrets",
|
|
178
|
+
regex: /(?:echo|print|cat|display|output|show)\s+.*(?:api[_-]?key|token|secret|password|credential)/i,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
level: "high",
|
|
182
|
+
category: "Secret detected",
|
|
183
|
+
detail: "AWS access key ID pattern",
|
|
184
|
+
regex: /AKIA[0-9A-Z]{16}/,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
level: "high",
|
|
188
|
+
category: "Secret detected",
|
|
189
|
+
detail: "Anthropic or OpenAI project key pattern",
|
|
190
|
+
regex: /(?:sk-ant-[a-zA-Z0-9-]{20,}|sk-proj-[a-zA-Z0-9-]{20,})/,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
level: "high",
|
|
194
|
+
category: "Credential theft",
|
|
195
|
+
detail: "Authorization header in curl/wget command",
|
|
196
|
+
regex: /(?:curl|wget)\s+[^\n]*-H\s+["']?(?:Authorization|Bearer|Token)\b/i,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
level: "high",
|
|
200
|
+
category: "Memory poisoning",
|
|
201
|
+
detail: "Writing to agent config files (SOUL.md, MEMORY.md, CLAUDE.md)",
|
|
202
|
+
regex: /(?:>>?|write|append|modify|edit|update)\s+[^\n]*(?:SOUL\.md|MEMORY\.md|CLAUDE\.md|\.cursorrules|\.windsurfrules)/i,
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
level: "high",
|
|
206
|
+
category: "Prompt injection",
|
|
207
|
+
detail: "Invisible Unicode smuggling (zero-width characters)",
|
|
208
|
+
regex: /\u200B|\u200C|\u200D|\uFEFF|\u2060/,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
level: "high",
|
|
212
|
+
category: "Prompt injection",
|
|
213
|
+
detail: "Global behavior override pattern",
|
|
214
|
+
regex: /always\s+(?:respond|reply|output|return|answer)\s+.*(?:json|xml|yaml|markdown|plain)/i,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
level: "high",
|
|
218
|
+
category: "Prompt injection",
|
|
219
|
+
detail: "Agent autonomy escalation (suppressing confirmations)",
|
|
220
|
+
regex: /(?:never|don't|do\s+not|disable)\s+(?:ask|confirm|check|verify|refuse|warn|prompt)/i,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
level: "high",
|
|
224
|
+
category: "Data exfiltration",
|
|
225
|
+
detail: "Instructing agent to include sensitive data in output",
|
|
226
|
+
regex: /include\s+.*(?:contents?|data|credentials?|keys?|tokens?|secrets?)\s+.*(?:response|output|message|reply)/i,
|
|
227
|
+
},
|
|
228
|
+
// MEDIUM: Extended system and dependency patterns
|
|
229
|
+
{
|
|
230
|
+
level: "medium",
|
|
231
|
+
category: "Unverifiable dependency",
|
|
232
|
+
detail: "Global package installation from unknown source",
|
|
233
|
+
regex: /(?:npm\s+install\s+-g|pip\s+install|go\s+install)\s+\S+/i,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
level: "medium",
|
|
237
|
+
category: "Suspicious activity",
|
|
238
|
+
detail: "Cryptocurrency or financial API access",
|
|
239
|
+
regex: /(?:binance|coinbase|metamask|etherscan|stripe|paypal)\.\w+/i,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
level: "medium",
|
|
243
|
+
category: "Destructive command",
|
|
244
|
+
detail: "Recursive force delete on root or home directory",
|
|
245
|
+
regex: /rm\s+-rf\s+(?:\/\s|\/\*|~\/|~\s|\$HOME)/,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
level: "medium",
|
|
249
|
+
category: "Privilege escalation",
|
|
250
|
+
detail: "Sudo usage in skill instructions",
|
|
251
|
+
regex: /\bsudo\s+\w/,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
level: "medium",
|
|
255
|
+
category: "System modification",
|
|
256
|
+
detail: "Writing to system directories",
|
|
257
|
+
regex: />\s*\/(?:etc|usr|var|opt)\//,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
level: "medium",
|
|
261
|
+
category: "Privilege escalation",
|
|
262
|
+
detail: "Docker privileged mode or capability addition",
|
|
263
|
+
regex: /docker\s+run\s+[^\n]*(?:--privileged|--cap-add)/i,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
level: "medium",
|
|
267
|
+
category: "Suspicious activity",
|
|
268
|
+
detail: "Third-party HTTP request in instructions",
|
|
269
|
+
regex: /(?:fetch\(|axios\.|requests\.get|http\.get|urllib)/,
|
|
270
|
+
},
|
|
139
271
|
];
|
|
140
272
|
// ---------------------------------------------------------------------------
|
|
141
273
|
// Scanner
|
|
@@ -168,7 +300,7 @@ export function scanSkillContent(content) {
|
|
|
168
300
|
const joined = line.slice(0, -1) + " " + (lines[i + 1] ?? "").trim();
|
|
169
301
|
for (const pattern of PATTERNS) {
|
|
170
302
|
if (pattern.regex.test(joined)) {
|
|
171
|
-
const alreadyFound = issues.some(iss => iss.line === i + 1 && iss.category === pattern.category);
|
|
303
|
+
const alreadyFound = issues.some((iss) => iss.line === i + 1 && iss.category === pattern.category);
|
|
172
304
|
if (!alreadyFound) {
|
|
173
305
|
issues.push({
|
|
174
306
|
level: pattern.level,
|
|
@@ -191,7 +323,7 @@ export function scanSkillContent(content) {
|
|
|
191
323
|
* Quick check: does this content have any critical issues?
|
|
192
324
|
*/
|
|
193
325
|
export function hasCriticalIssues(content) {
|
|
194
|
-
return scanSkillContent(content).some(i => i.level === "critical");
|
|
326
|
+
return scanSkillContent(content).some((i) => i.level === "critical");
|
|
195
327
|
}
|
|
196
328
|
/**
|
|
197
329
|
* Format scan results for display.
|
|
@@ -200,17 +332,13 @@ export function formatScanResults(skillName, issues) {
|
|
|
200
332
|
if (issues.length === 0)
|
|
201
333
|
return ` [OK] ${skillName}`;
|
|
202
334
|
const lines = [];
|
|
203
|
-
const critical = issues.filter(i => i.level === "critical").length;
|
|
204
|
-
const high = issues.filter(i => i.level === "high").length;
|
|
205
|
-
const medium = issues.filter(i => i.level === "medium").length;
|
|
335
|
+
const critical = issues.filter((i) => i.level === "critical").length;
|
|
336
|
+
const high = issues.filter((i) => i.level === "high").length;
|
|
206
337
|
const tag = critical > 0 ? "[!!]" : high > 0 ? "[!!]" : "[i]";
|
|
207
338
|
lines.push(` ${tag} ${skillName} (${issues.length} issue${issues.length !== 1 ? "s" : ""})`);
|
|
208
339
|
for (const issue of issues) {
|
|
209
|
-
const icon = issue.level === "critical" ? "CRIT"
|
|
210
|
-
: issue.level === "high" ? "HIGH"
|
|
211
|
-
: "MED";
|
|
340
|
+
const icon = issue.level === "critical" ? "CRIT" : issue.level === "high" ? "HIGH" : "MED";
|
|
212
341
|
lines.push(` [${icon}] ${issue.category}: ${issue.detail} (line ${issue.line})`);
|
|
213
342
|
}
|
|
214
343
|
return lines.join("\n");
|
|
215
344
|
}
|
|
216
|
-
//# sourceMappingURL=scanner.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SkillInfo } from "../types.js";
|
|
2
|
+
import type { ProjectContext } from "./project-context.js";
|
|
3
|
+
export interface RecommendVerdict {
|
|
4
|
+
skill: string;
|
|
5
|
+
verdict: "recommended" | "optional" | "skip" | "conflict";
|
|
6
|
+
score: number;
|
|
7
|
+
reasons: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function scoreSkill(skill: SkillInfo, context: ProjectContext, installedSkills: string[], allSkills: SkillInfo[]): RecommendVerdict;
|
|
10
|
+
export declare function rankSkills(skills: SkillInfo[], context: ProjectContext): RecommendVerdict[];
|