@safetnsr/vet 1.6.0 → 1.7.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/categories.d.ts +2 -0
- package/dist/categories.js +10 -1
- package/dist/checks/deps.js +16 -3
- package/dist/checks/models.js +86 -12
- package/dist/checks/scan.js +3 -2
- package/dist/checks/verify.js +72 -5
- package/dist/detect-language.d.ts +6 -0
- package/dist/detect-language.js +24 -0
- package/package.json +1 -1
package/dist/categories.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { CheckResult, CategoryResult, VetResult } from './types.js';
|
|
2
2
|
export declare function toGrade(score: number): string;
|
|
3
|
+
/** Apply a floor of 20 to non-security checks that have no security-related errors */
|
|
4
|
+
export declare function applyScoreFloor(check: CheckResult): number;
|
|
3
5
|
export declare function buildCategories(checkMap: {
|
|
4
6
|
security: CheckResult[];
|
|
5
7
|
integrity: CheckResult[];
|
package/dist/categories.js
CHANGED
|
@@ -21,11 +21,20 @@ const WEIGHTS = {
|
|
|
21
21
|
debt: 0.25,
|
|
22
22
|
deps: 0.15,
|
|
23
23
|
};
|
|
24
|
+
// ── Scoring floor for non-security checks ────────────────────────────────────
|
|
25
|
+
const SECURITY_CHECKS = new Set(['scan', 'secrets', 'permissions', 'owasp']);
|
|
26
|
+
/** Apply a floor of 20 to non-security checks that have no security-related errors */
|
|
27
|
+
export function applyScoreFloor(check) {
|
|
28
|
+
if (SECURITY_CHECKS.has(check.name))
|
|
29
|
+
return check.score;
|
|
30
|
+
// Non-security check: minimum score is 20
|
|
31
|
+
return Math.max(20, check.score);
|
|
32
|
+
}
|
|
24
33
|
// ── Average scores within a category ────────────────────────────────────────
|
|
25
34
|
function averageScore(checks) {
|
|
26
35
|
if (checks.length === 0)
|
|
27
36
|
return 100;
|
|
28
|
-
const total = checks.reduce((sum, c) => sum + c
|
|
37
|
+
const total = checks.reduce((sum, c) => sum + applyScoreFloor(c), 0);
|
|
29
38
|
return Math.round(total / checks.length);
|
|
30
39
|
}
|
|
31
40
|
// ── Group checks into categories ─────────────────────────────────────────────
|
package/dist/checks/deps.js
CHANGED
|
@@ -83,11 +83,14 @@ export function extractPackageName(specifier) {
|
|
|
83
83
|
// Skip node: builtins
|
|
84
84
|
if (specifier.startsWith('node:'))
|
|
85
85
|
return null;
|
|
86
|
+
// Path aliases: @/ is always a path alias (no npm package starts with @/)
|
|
87
|
+
if (specifier.startsWith('@/'))
|
|
88
|
+
return null;
|
|
86
89
|
// Scoped packages: @scope/name or @scope/name/sub
|
|
87
90
|
if (specifier.startsWith('@')) {
|
|
88
91
|
const parts = specifier.split('/');
|
|
89
92
|
if (parts.length < 2)
|
|
90
|
-
return null;
|
|
93
|
+
return null; // bare @scope with no / is not a valid package
|
|
91
94
|
return `${parts[0]}/${parts[1]}`;
|
|
92
95
|
}
|
|
93
96
|
// Regular package: name or name/sub
|
|
@@ -184,15 +187,25 @@ export async function checkDeps(cwd) {
|
|
|
184
187
|
}
|
|
185
188
|
// ── 2. Typosquat detection ─────────────────────────────────────────────────
|
|
186
189
|
const topSet = new Set(TOP_PACKAGES);
|
|
190
|
+
// Known-legitimate short packages that happen to be close to popular ones
|
|
191
|
+
const TYPOSQUAT_WHITELIST = new Set([
|
|
192
|
+
'ai', 'clsx', 'ws', 'os', 'ms', 'pg', 'ip', 'bn', 'qs', 'co', 'is',
|
|
193
|
+
]);
|
|
187
194
|
for (const pkg of declaredNames) {
|
|
188
195
|
if (topSet.has(pkg))
|
|
189
196
|
continue; // it IS the popular package
|
|
197
|
+
if (pkg.length <= 3)
|
|
198
|
+
continue; // too short, too many false matches
|
|
199
|
+
if (TYPOSQUAT_WHITELIST.has(pkg))
|
|
200
|
+
continue;
|
|
190
201
|
for (const top of TOP_PACKAGES) {
|
|
191
202
|
const dist = levenshtein(pkg, top);
|
|
192
203
|
if (dist >= 1 && dist <= 2) {
|
|
204
|
+
// If the package exists on the registry, it's likely legitimate — downgrade to info
|
|
205
|
+
const existsOnRegistry = registryResults.get(pkg) === true;
|
|
193
206
|
issues.push({
|
|
194
|
-
severity: 'error',
|
|
195
|
-
message: `possible typosquat: "${pkg}" is ${dist} edit${dist > 1 ? 's' : ''} from "${top}"`,
|
|
207
|
+
severity: existsOnRegistry ? 'info' : 'error',
|
|
208
|
+
message: `possible typosquat: "${pkg}" is ${dist} edit${dist > 1 ? 's' : ''} from "${top}"${existsOnRegistry ? ' (exists on npm)' : ''}`,
|
|
196
209
|
file: 'package.json',
|
|
197
210
|
fixable: true,
|
|
198
211
|
fixHint: `did you mean "${top}"?`,
|
package/dist/checks/models.js
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
1
|
+
import { join, basename } from 'node:path';
|
|
2
2
|
import { readFile, walkFiles } from '../util.js';
|
|
3
|
+
// ── AI framework detection ───────────────────────────────────────────────────
|
|
4
|
+
const AI_NAME_KEYWORDS = ['ai', 'llm', 'model', 'openai', 'anthropic', 'langchain', 'provider'];
|
|
5
|
+
const AI_PKG_KEYWORDS = new Set(['ai', 'llm', 'language-model', 'openai', 'anthropic']);
|
|
6
|
+
function isAiFramework(cwd) {
|
|
7
|
+
// Check package.json
|
|
8
|
+
const pkgRaw = readFile(join(cwd, 'package.json'));
|
|
9
|
+
if (pkgRaw) {
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(pkgRaw);
|
|
12
|
+
const name = (pkg.name || '').toLowerCase();
|
|
13
|
+
if (AI_NAME_KEYWORDS.some(k => name.includes(k)))
|
|
14
|
+
return true;
|
|
15
|
+
if (Array.isArray(pkg.keywords) && pkg.keywords.some((k) => AI_PKG_KEYWORDS.has(k.toLowerCase())))
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch { /* skip */ }
|
|
19
|
+
}
|
|
20
|
+
// Check pyproject.toml / setup.py for AI deps
|
|
21
|
+
const pyproject = readFile(join(cwd, 'pyproject.toml'));
|
|
22
|
+
if (pyproject) {
|
|
23
|
+
const aiDeps = ['openai', 'anthropic', 'langchain', 'transformers', 'torch', 'tensorflow'];
|
|
24
|
+
if (aiDeps.some(d => pyproject.includes(d)))
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const setupPy = readFile(join(cwd, 'setup.py'));
|
|
28
|
+
if (setupPy) {
|
|
29
|
+
const aiDeps = ['openai', 'anthropic', 'langchain', 'transformers', 'torch', 'tensorflow'];
|
|
30
|
+
if (aiDeps.some(d => setupPy.includes(d)))
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
// ── Test/example/docs path detection ─────────────────────────────────────────
|
|
36
|
+
const TEST_DOCS_PATTERNS = ['test/', 'tests/', '__tests__/', 'examples/', 'docs/'];
|
|
37
|
+
function isTestOrDocsFile(filePath) {
|
|
38
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
39
|
+
if (TEST_DOCS_PATTERNS.some(p => normalized.includes(p) || normalized.startsWith(p)))
|
|
40
|
+
return true;
|
|
41
|
+
const base = basename(filePath);
|
|
42
|
+
if (/\.(test|spec)\./i.test(base))
|
|
43
|
+
return true;
|
|
44
|
+
if (base.endsWith('.md'))
|
|
45
|
+
return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
3
48
|
// Try to use @safetnsr/model-graveyard if installed (248 models, alias matching, YAML registry)
|
|
4
49
|
async function tryModelGraveyard(cwd) {
|
|
5
50
|
try {
|
|
@@ -8,6 +53,7 @@ async function tryModelGraveyard(cwd) {
|
|
|
8
53
|
return null;
|
|
9
54
|
const report = await mod.scan(cwd);
|
|
10
55
|
const issues = [];
|
|
56
|
+
const aiFramework = isAiFramework(cwd);
|
|
11
57
|
// Files that define deprecated model registries should not be flagged
|
|
12
58
|
const SELF_FILES = ['models.ts', 'models.js', 'model-graveyard', 'model-registry', 'sunset', 'fix/models'];
|
|
13
59
|
for (const match of report.matches) {
|
|
@@ -17,8 +63,10 @@ async function tryModelGraveyard(cwd) {
|
|
|
17
63
|
if (match.file && SELF_FILES.some(s => match.file.toLowerCase().includes(s)))
|
|
18
64
|
continue;
|
|
19
65
|
if (match.model.status === 'deprecated' || match.model.status === 'eol') {
|
|
66
|
+
const inTestDocs = match.file && isTestOrDocsFile(match.file);
|
|
67
|
+
const severity = (aiFramework || inTestDocs) ? 'info' : 'error';
|
|
20
68
|
issues.push({
|
|
21
|
-
severity
|
|
69
|
+
severity,
|
|
22
70
|
message: `${match.model.status} model "${match.raw}" in ${match.file}:${match.line}${match.model.successor ? ` — use "${match.model.successor}"` : ''}`,
|
|
23
71
|
file: match.file,
|
|
24
72
|
line: match.line,
|
|
@@ -27,15 +75,22 @@ async function tryModelGraveyard(cwd) {
|
|
|
27
75
|
});
|
|
28
76
|
}
|
|
29
77
|
}
|
|
30
|
-
const
|
|
78
|
+
const errorCount = issues.filter(i => i.severity === 'error').length;
|
|
79
|
+
const score = aiFramework
|
|
80
|
+
? Math.max(70, 100 - errorCount * 20)
|
|
81
|
+
: Math.max(0, 100 - errorCount * 20);
|
|
82
|
+
let summary = issues.length === 0
|
|
83
|
+
? `${report.filesScanned} files scanned (via model-graveyard) — all current`
|
|
84
|
+
: `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} (via model-graveyard)`;
|
|
85
|
+
if (aiFramework) {
|
|
86
|
+
summary += ' — AI framework detected — model references are expected';
|
|
87
|
+
}
|
|
31
88
|
return {
|
|
32
89
|
name: 'models',
|
|
33
90
|
score: Math.min(100, score),
|
|
34
91
|
maxScore: 100,
|
|
35
92
|
issues,
|
|
36
|
-
summary
|
|
37
|
-
? `${report.filesScanned} files scanned (via model-graveyard) — all current`
|
|
38
|
-
: `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} (via model-graveyard)`,
|
|
93
|
+
summary,
|
|
39
94
|
};
|
|
40
95
|
}
|
|
41
96
|
catch {
|
|
@@ -87,6 +142,7 @@ function builtinModels(cwd, ignore) {
|
|
|
87
142
|
const issues = [];
|
|
88
143
|
const files = walkFiles(cwd, ignore);
|
|
89
144
|
const found = new Map();
|
|
145
|
+
const aiFramework = isAiFramework(cwd);
|
|
90
146
|
for (const f of files) {
|
|
91
147
|
if (!SCAN_EXTS.some(ext => f.endsWith(ext)))
|
|
92
148
|
continue;
|
|
@@ -105,24 +161,42 @@ function builtinModels(cwd, ignore) {
|
|
|
105
161
|
found.set(model, existing);
|
|
106
162
|
}
|
|
107
163
|
}
|
|
108
|
-
for (const [model,
|
|
164
|
+
for (const [model, modelFiles] of found) {
|
|
109
165
|
const info = SUNSET_MODELS[model];
|
|
110
|
-
const fileList =
|
|
166
|
+
const fileList = modelFiles.length <= 2 ? modelFiles.join(', ') : `${modelFiles[0]} +${modelFiles.length - 1} more`;
|
|
167
|
+
// Determine severity: downgrade for AI frameworks or test/docs files
|
|
168
|
+
const allInTestDocs = modelFiles.every(f => isTestOrDocsFile(f));
|
|
169
|
+
const severity = (aiFramework || allInTestDocs) ? 'info' : 'error';
|
|
111
170
|
issues.push({
|
|
112
|
-
severity
|
|
171
|
+
severity,
|
|
113
172
|
message: `deprecated model "${model}" in ${fileList} — use "${info.replacement}"${info.sunset ? ` (sunset ${info.sunset})` : ''}`,
|
|
114
|
-
file:
|
|
173
|
+
file: modelFiles[0],
|
|
115
174
|
fixable: true,
|
|
116
175
|
fixHint: `replace "${model}" with "${info.replacement}"`,
|
|
117
176
|
});
|
|
118
177
|
}
|
|
119
|
-
|
|
178
|
+
let score;
|
|
179
|
+
if (aiFramework) {
|
|
180
|
+
// AI framework: models exist for compatibility, score 70+ base
|
|
181
|
+
const errorCount = issues.filter(i => i.severity === 'error').length;
|
|
182
|
+
score = Math.max(70, 100 - errorCount * 20);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const errorCount = issues.filter(i => i.severity === 'error').length;
|
|
186
|
+
score = Math.max(0, 100 - errorCount * 20);
|
|
187
|
+
}
|
|
188
|
+
let summary = issues.length === 0
|
|
189
|
+
? 'all model references current'
|
|
190
|
+
: `${issues.length} deprecated model${issues.length > 1 ? 's' : ''} found`;
|
|
191
|
+
if (aiFramework) {
|
|
192
|
+
summary += ' — AI framework detected — model references are expected';
|
|
193
|
+
}
|
|
120
194
|
return {
|
|
121
195
|
name: 'models',
|
|
122
196
|
score: Math.min(100, score),
|
|
123
197
|
maxScore: 100,
|
|
124
198
|
issues,
|
|
125
|
-
summary
|
|
199
|
+
summary,
|
|
126
200
|
};
|
|
127
201
|
}
|
|
128
202
|
export async function checkModels(cwd, ignore) {
|
package/dist/checks/scan.js
CHANGED
|
@@ -85,9 +85,10 @@ const CONFIG_TARGETS = [
|
|
|
85
85
|
'.claude', 'CLAUDE.md', 'AGENTS.md',
|
|
86
86
|
'.cursorrules', '.cursor',
|
|
87
87
|
'.github',
|
|
88
|
-
'.
|
|
88
|
+
'copilot-instructions.md',
|
|
89
|
+
'.aider.conf.yml', '.aider',
|
|
89
90
|
'.continue',
|
|
90
|
-
'.mcp',
|
|
91
|
+
'.mcp', 'mcp.json', '.mcp.json',
|
|
91
92
|
'.roomodes', '.roo',
|
|
92
93
|
];
|
|
93
94
|
// ── File helpers (delegated to util.ts) ──────────────────────────────────────
|
package/dist/checks/verify.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, basename } from 'node:path';
|
|
1
|
+
import { join, basename, extname } from 'node:path';
|
|
2
2
|
import { readFileSync, existsSync, statSync } from 'node:fs';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
4
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -10,9 +10,48 @@ function safeExec(cmd, cwd) {
|
|
|
10
10
|
return '';
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
// ── Config/meta file exclusion for thin-file checks ──────────────────────────
|
|
14
|
+
const CONFIG_DOTFILES = new Set([
|
|
15
|
+
'.gitignore', '.gitattributes', '.nvmrc', '.node-version', '.editorconfig',
|
|
16
|
+
'.prettierrc', '.eslintignore', '.npmrc', '.npmignore',
|
|
17
|
+
]);
|
|
18
|
+
const CONFIG_EXTENSIONS = new Set([
|
|
19
|
+
'.yml', '.yaml', '.json', '.toml', '.cfg', '.ini', '.lock', '.svg', '.xml',
|
|
20
|
+
]);
|
|
21
|
+
const CONFIG_DIRS = ['.github/', '.husky/', '.vscode/', '.idea/'];
|
|
22
|
+
const META_FILES = new Set([
|
|
23
|
+
'FUNDING.yaml', 'CODEOWNERS', 'LICENSE',
|
|
24
|
+
]);
|
|
25
|
+
function isConfigOrMetaFile(filePath) {
|
|
26
|
+
const base = basename(filePath);
|
|
27
|
+
const ext = extname(filePath).toLowerCase();
|
|
28
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
29
|
+
// Dotfiles
|
|
30
|
+
if (CONFIG_DOTFILES.has(base))
|
|
31
|
+
return true;
|
|
32
|
+
// Config extensions
|
|
33
|
+
if (CONFIG_EXTENSIONS.has(ext))
|
|
34
|
+
return true;
|
|
35
|
+
// Config directories
|
|
36
|
+
if (CONFIG_DIRS.some(d => normalized.includes(d) || normalized.startsWith(d)))
|
|
37
|
+
return true;
|
|
38
|
+
// Meta files
|
|
39
|
+
if (META_FILES.has(base))
|
|
40
|
+
return true;
|
|
41
|
+
if (base.startsWith('CHANGELOG'))
|
|
42
|
+
return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// ── Code file extensions for test detection ──────────────────────────────────
|
|
46
|
+
const CODE_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']);
|
|
47
|
+
const TEST_FILE_PATTERN = /\.(test|spec)\.(ts|js|tsx|jsx)$/i;
|
|
13
48
|
function isTestFile(filePath) {
|
|
14
49
|
const base = basename(filePath);
|
|
15
|
-
|
|
50
|
+
const ext = extname(filePath).toLowerCase();
|
|
51
|
+
// Only code files can be test files
|
|
52
|
+
if (!CODE_EXTENSIONS.has(ext))
|
|
53
|
+
return false;
|
|
54
|
+
if (TEST_FILE_PATTERN.test(base))
|
|
16
55
|
return true;
|
|
17
56
|
const normalized = filePath.replace(/\\/g, '/');
|
|
18
57
|
// Match __tests__/ anywhere in path (including at root)
|
|
@@ -83,10 +122,26 @@ function getRecentMessages(cwd, since) {
|
|
|
83
122
|
}
|
|
84
123
|
return raw.split('\n').map(l => l.replace(/^[a-f0-9]+\s+/, '').trim()).filter(l => l.length > 0);
|
|
85
124
|
}
|
|
125
|
+
// ── Python project detection ─────────────────────────────────────────────────
|
|
126
|
+
function isPythonProject(cwd) {
|
|
127
|
+
const markers = ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt'];
|
|
128
|
+
return markers.some(m => existsSync(join(cwd, m)));
|
|
129
|
+
}
|
|
130
|
+
function isPythonBoilerplate(filePath) {
|
|
131
|
+
const base = basename(filePath);
|
|
132
|
+
if (base === '__init__.py')
|
|
133
|
+
return true;
|
|
134
|
+
if (filePath.endsWith('.pyi'))
|
|
135
|
+
return true;
|
|
136
|
+
if (filePath.replace(/\\/g, '/').includes('__pycache__/'))
|
|
137
|
+
return true;
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
86
140
|
// ── Main check ───────────────────────────────────────────────────────────────
|
|
87
141
|
export function checkVerify(cwd, since) {
|
|
88
142
|
const issues = [];
|
|
89
143
|
let deductions = 0;
|
|
144
|
+
const python = isPythonProject(cwd);
|
|
90
145
|
// Check if git repo
|
|
91
146
|
const isGit = safeExec('git rev-parse --is-inside-work-tree', cwd).trim();
|
|
92
147
|
if (isGit !== 'true') {
|
|
@@ -165,6 +220,16 @@ export function checkVerify(cwd, since) {
|
|
|
165
220
|
}
|
|
166
221
|
const lineCount = countLines(content);
|
|
167
222
|
// 2. File must have meaningful content (>10 non-empty lines)
|
|
223
|
+
// Skip thin file check for Python boilerplate files
|
|
224
|
+
if (python && isPythonBoilerplate(relPath)) {
|
|
225
|
+
verified++;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// Skip thin file check for config/meta files (they're supposed to be small)
|
|
229
|
+
if (isConfigOrMetaFile(relPath)) {
|
|
230
|
+
verified++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
168
233
|
if (lineCount < 10 && lineCount > 0) {
|
|
169
234
|
issues.push({
|
|
170
235
|
severity: 'warning',
|
|
@@ -207,13 +272,15 @@ export function checkVerify(cwd, since) {
|
|
|
207
272
|
verified++;
|
|
208
273
|
}
|
|
209
274
|
const finalScore = Math.max(0, 100 - deductions);
|
|
275
|
+
const baseSummary = failed === 0
|
|
276
|
+
? `${verified} agent claim${verified !== 1 ? 's' : ''} verified clean`
|
|
277
|
+
: `${failed} claim${failed !== 1 ? 's' : ''} failed verification (${verified} passed)`;
|
|
278
|
+
const summary = python ? `${baseSummary} (python project detected — some checks have reduced scope)` : baseSummary;
|
|
210
279
|
return {
|
|
211
280
|
name: 'verify',
|
|
212
281
|
score: finalScore,
|
|
213
282
|
maxScore: 100,
|
|
214
283
|
issues,
|
|
215
|
-
summary
|
|
216
|
-
? `${verified} agent claim${verified !== 1 ? 's' : ''} verified clean`
|
|
217
|
-
: `${failed} claim${failed !== 1 ? 's' : ''} failed verification (${verified} passed)`,
|
|
284
|
+
summary,
|
|
218
285
|
};
|
|
219
286
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type ProjectLanguage = 'javascript' | 'typescript' | 'python' | 'unknown';
|
|
2
|
+
/**
|
|
3
|
+
* Detect the primary language of a project by checking for marker files.
|
|
4
|
+
* Priority: typescript > javascript > python > unknown
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectProjectLanguage(cwd: string): ProjectLanguage;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Detect the primary language of a project by checking for marker files.
|
|
5
|
+
* Priority: typescript > javascript > python > unknown
|
|
6
|
+
*/
|
|
7
|
+
export function detectProjectLanguage(cwd) {
|
|
8
|
+
// TypeScript markers
|
|
9
|
+
if (existsSync(join(cwd, 'tsconfig.json')))
|
|
10
|
+
return 'typescript';
|
|
11
|
+
// JavaScript/TypeScript (package.json present)
|
|
12
|
+
if (existsSync(join(cwd, 'package.json'))) {
|
|
13
|
+
// Check if any tsconfig variant exists
|
|
14
|
+
const tsConfigs = ['tsconfig.build.json', 'tsconfig.app.json', 'tsconfig.node.json'];
|
|
15
|
+
if (tsConfigs.some(f => existsSync(join(cwd, f))))
|
|
16
|
+
return 'typescript';
|
|
17
|
+
return 'javascript';
|
|
18
|
+
}
|
|
19
|
+
// Python markers
|
|
20
|
+
const pythonMarkers = ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt'];
|
|
21
|
+
if (pythonMarkers.some(f => existsSync(join(cwd, f))))
|
|
22
|
+
return 'python';
|
|
23
|
+
return 'unknown';
|
|
24
|
+
}
|