claudex-setup 1.16.0 → 1.16.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/README.md +56 -23
- package/bin/cli.js +92 -5
- package/content/launch-posts.md +159 -92
- package/package.json +2 -2
- package/src/activity.js +195 -1
- package/src/analyze.js +11 -6
- package/src/audit.js +49 -14
- package/src/context.js +106 -0
- package/src/deep-review.js +95 -68
- package/src/domain-packs.js +13 -4
- package/src/index.js +4 -0
- package/src/mcp-packs.js +16 -0
- package/src/secret-patterns.js +30 -0
- package/src/techniques.js +4 -2
- package/src/watch.js +170 -42
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudex-setup",
|
|
3
|
-
"version": "1.16.
|
|
4
|
-
"description": "Score your repo's Claude Code setup against
|
|
3
|
+
"version": "1.16.1",
|
|
4
|
+
"description": "Score your repo's Claude Code setup against 85 checks. See gaps, apply fixes selectively with rollback, govern hooks and permissions, and benchmark impact — without breaking existing config.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claudex-setup": "bin/cli.js"
|
package/src/activity.js
CHANGED
|
@@ -21,10 +21,12 @@ function ensureArtifactDirs(dir) {
|
|
|
21
21
|
const activityDir = path.join(root, 'activity');
|
|
22
22
|
const rollbackDir = path.join(root, 'rollbacks');
|
|
23
23
|
const snapshotDir = path.join(root, 'snapshots');
|
|
24
|
+
const outcomesDir = path.join(root, 'outcomes');
|
|
24
25
|
fs.mkdirSync(activityDir, { recursive: true });
|
|
25
26
|
fs.mkdirSync(rollbackDir, { recursive: true });
|
|
26
27
|
fs.mkdirSync(snapshotDir, { recursive: true });
|
|
27
|
-
|
|
28
|
+
fs.mkdirSync(outcomesDir, { recursive: true });
|
|
29
|
+
return { root, activityDir, rollbackDir, snapshotDir, outcomesDir };
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
function writeJson(filePath, payload) {
|
|
@@ -322,6 +324,192 @@ function exportTrendReport(dir) {
|
|
|
322
324
|
return lines.join('\n');
|
|
323
325
|
}
|
|
324
326
|
|
|
327
|
+
function readOutcomeIndex(dir) {
|
|
328
|
+
const indexPath = path.join(dir, '.claude', 'claudex-setup', 'outcomes', 'index.json');
|
|
329
|
+
if (!fs.existsSync(indexPath)) return [];
|
|
330
|
+
try {
|
|
331
|
+
const entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
332
|
+
return Array.isArray(entries) ? entries : [];
|
|
333
|
+
} catch {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function updateOutcomeIndex(outcomesDir, record) {
|
|
339
|
+
const indexPath = path.join(outcomesDir, 'index.json');
|
|
340
|
+
let entries = [];
|
|
341
|
+
|
|
342
|
+
if (fs.existsSync(indexPath)) {
|
|
343
|
+
try {
|
|
344
|
+
entries = JSON.parse(fs.readFileSync(indexPath, 'utf8'));
|
|
345
|
+
if (!Array.isArray(entries)) entries = [];
|
|
346
|
+
} catch {
|
|
347
|
+
entries = [];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
entries.push(record);
|
|
352
|
+
const MAX_INDEX_ENTRIES = 500;
|
|
353
|
+
if (entries.length > MAX_INDEX_ENTRIES) {
|
|
354
|
+
entries = entries.slice(entries.length - MAX_INDEX_ENTRIES);
|
|
355
|
+
}
|
|
356
|
+
fs.writeFileSync(indexPath, JSON.stringify(entries, null, 2), 'utf8');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function normalizeOutcomeStatus(value) {
|
|
360
|
+
const normalized = `${value || ''}`.trim().toLowerCase();
|
|
361
|
+
if (!['accepted', 'rejected', 'deferred'].includes(normalized)) {
|
|
362
|
+
throw new Error('feedback status must be one of: accepted, rejected, deferred');
|
|
363
|
+
}
|
|
364
|
+
return normalized;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function normalizeOutcomeEffect(value) {
|
|
368
|
+
const normalized = `${value || ''}`.trim().toLowerCase();
|
|
369
|
+
if (!['positive', 'neutral', 'negative'].includes(normalized)) {
|
|
370
|
+
throw new Error('feedback effect must be one of: positive, neutral, negative');
|
|
371
|
+
}
|
|
372
|
+
return normalized;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function recordRecommendationOutcome(dir, payload) {
|
|
376
|
+
const key = `${payload.key || ''}`.trim();
|
|
377
|
+
if (!key) {
|
|
378
|
+
throw new Error('feedback requires a recommendation key');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const status = normalizeOutcomeStatus(payload.status);
|
|
382
|
+
const effect = normalizeOutcomeEffect(payload.effect || 'neutral');
|
|
383
|
+
const scoreDelta = Number.isFinite(payload.scoreDelta) ? payload.scoreDelta : (
|
|
384
|
+
payload.scoreDelta === null || payload.scoreDelta === undefined || payload.scoreDelta === ''
|
|
385
|
+
? null
|
|
386
|
+
: Number(payload.scoreDelta)
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
if (scoreDelta !== null && !Number.isFinite(scoreDelta)) {
|
|
390
|
+
throw new Error('feedback scoreDelta must be a number when provided');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const id = timestampId();
|
|
394
|
+
const { outcomesDir } = ensureArtifactDirs(dir);
|
|
395
|
+
const filePath = path.join(outcomesDir, `${id}.json`);
|
|
396
|
+
const record = {
|
|
397
|
+
id,
|
|
398
|
+
createdAt: new Date().toISOString(),
|
|
399
|
+
key,
|
|
400
|
+
status,
|
|
401
|
+
effect,
|
|
402
|
+
source: `${payload.source || 'manual-cli'}`.trim() || 'manual-cli',
|
|
403
|
+
notes: `${payload.notes || ''}`.trim(),
|
|
404
|
+
scoreDelta,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
writeJson(filePath, record);
|
|
408
|
+
updateOutcomeIndex(outcomesDir, {
|
|
409
|
+
...record,
|
|
410
|
+
relativePath: path.relative(dir, filePath),
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
id,
|
|
415
|
+
filePath,
|
|
416
|
+
relativePath: path.relative(dir, filePath),
|
|
417
|
+
record,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function summarizeOutcomeEntries(entries = []) {
|
|
422
|
+
const byKey = {};
|
|
423
|
+
|
|
424
|
+
for (const entry of entries) {
|
|
425
|
+
if (!entry || !entry.key) continue;
|
|
426
|
+
const bucket = byKey[entry.key] || {
|
|
427
|
+
key: entry.key,
|
|
428
|
+
total: 0,
|
|
429
|
+
accepted: 0,
|
|
430
|
+
rejected: 0,
|
|
431
|
+
deferred: 0,
|
|
432
|
+
positive: 0,
|
|
433
|
+
neutral: 0,
|
|
434
|
+
negative: 0,
|
|
435
|
+
scoreDeltaTotal: 0,
|
|
436
|
+
scoreDeltaCount: 0,
|
|
437
|
+
latestAt: null,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
bucket.total += 1;
|
|
441
|
+
if (bucket[entry.status] !== undefined) bucket[entry.status] += 1;
|
|
442
|
+
if (bucket[entry.effect] !== undefined) bucket[entry.effect] += 1;
|
|
443
|
+
if (Number.isFinite(entry.scoreDelta)) {
|
|
444
|
+
bucket.scoreDeltaTotal += entry.scoreDelta;
|
|
445
|
+
bucket.scoreDeltaCount += 1;
|
|
446
|
+
}
|
|
447
|
+
if (!bucket.latestAt || new Date(entry.createdAt) > new Date(bucket.latestAt)) {
|
|
448
|
+
bucket.latestAt = entry.createdAt;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
byKey[entry.key] = bucket;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (const bucket of Object.values(byKey)) {
|
|
455
|
+
bucket.avgScoreDelta = bucket.scoreDeltaCount > 0
|
|
456
|
+
? Number((bucket.scoreDeltaTotal / bucket.scoreDeltaCount).toFixed(2))
|
|
457
|
+
: null;
|
|
458
|
+
bucket.evidenceClass = bucket.total > 0 ? 'measured' : 'estimated';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
totalEntries: entries.length,
|
|
463
|
+
byKey,
|
|
464
|
+
keys: Object.keys(byKey).sort(),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function getRecommendationOutcomeSummary(dir) {
|
|
469
|
+
return summarizeOutcomeEntries(readOutcomeIndex(dir));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function getRecommendationAdjustment(summaryByKey, key) {
|
|
473
|
+
const bucket = summaryByKey && summaryByKey[key];
|
|
474
|
+
if (!bucket) return 0;
|
|
475
|
+
|
|
476
|
+
let adjustment = 0;
|
|
477
|
+
adjustment += bucket.accepted * 2;
|
|
478
|
+
adjustment += bucket.positive * 3;
|
|
479
|
+
adjustment -= bucket.rejected * 3;
|
|
480
|
+
adjustment -= bucket.negative * 4;
|
|
481
|
+
|
|
482
|
+
if (Number.isFinite(bucket.avgScoreDelta)) {
|
|
483
|
+
if (bucket.avgScoreDelta > 0) adjustment += Math.min(4, Math.round(bucket.avgScoreDelta / 4));
|
|
484
|
+
if (bucket.avgScoreDelta < 0) adjustment -= Math.min(4, Math.round(Math.abs(bucket.avgScoreDelta) / 4));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (adjustment > 8) return 8;
|
|
488
|
+
if (adjustment < -8) return -8;
|
|
489
|
+
return adjustment;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function formatRecommendationOutcomeSummary(dir) {
|
|
493
|
+
const summary = getRecommendationOutcomeSummary(dir);
|
|
494
|
+
if (summary.totalEntries === 0) {
|
|
495
|
+
return 'No recommendation outcomes recorded yet. Use `npx claudex-setup feedback --key permissionDeny --status accepted --effect positive` after a real run.';
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const lines = [
|
|
499
|
+
'Recommendation outcome summary:',
|
|
500
|
+
'',
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
for (const key of summary.keys) {
|
|
504
|
+
const bucket = summary.byKey[key];
|
|
505
|
+
const avg = Number.isFinite(bucket.avgScoreDelta) ? ` | avg score delta ${bucket.avgScoreDelta >= 0 ? '+' : ''}${bucket.avgScoreDelta}` : '';
|
|
506
|
+
const adjustment = getRecommendationAdjustment(summary.byKey, key);
|
|
507
|
+
lines.push(` ${key}: total ${bucket.total} | accepted ${bucket.accepted} | rejected ${bucket.rejected} | deferred ${bucket.deferred} | positive ${bucket.positive} | negative ${bucket.negative}${avg} | ranking ${adjustment >= 0 ? '+' : ''}${adjustment}`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return lines.join('\n');
|
|
511
|
+
}
|
|
512
|
+
|
|
325
513
|
module.exports = {
|
|
326
514
|
ensureArtifactDirs,
|
|
327
515
|
writeActivityArtifact,
|
|
@@ -332,4 +520,10 @@ module.exports = {
|
|
|
332
520
|
compareLatest,
|
|
333
521
|
formatHistory,
|
|
334
522
|
exportTrendReport,
|
|
523
|
+
readOutcomeIndex,
|
|
524
|
+
recordRecommendationOutcome,
|
|
525
|
+
summarizeOutcomeEntries,
|
|
526
|
+
getRecommendationOutcomeSummary,
|
|
527
|
+
getRecommendationAdjustment,
|
|
528
|
+
formatRecommendationOutcomeSummary,
|
|
335
529
|
};
|
package/src/analyze.js
CHANGED
|
@@ -244,12 +244,15 @@ function toGaps(results) {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
function toRecommendations(auditResult) {
|
|
247
|
-
const failed = auditResult.results
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
247
|
+
const failed = auditResult.results.filter(r => r.passed === false);
|
|
248
|
+
const topActionOrder = new Map((auditResult.topNextActions || []).map((item, index) => [item.key, index]));
|
|
249
|
+
failed.sort((a, b) => {
|
|
250
|
+
const rankedA = topActionOrder.has(a.key) ? topActionOrder.get(a.key) : Number.MAX_SAFE_INTEGER;
|
|
251
|
+
const rankedB = topActionOrder.has(b.key) ? topActionOrder.get(b.key) : Number.MAX_SAFE_INTEGER;
|
|
252
|
+
if (rankedA !== rankedB) return rankedA - rankedB;
|
|
253
|
+
const order = { critical: 3, high: 2, medium: 1, low: 0 };
|
|
254
|
+
return (order[b.impact] || 0) - (order[a.impact] || 0);
|
|
255
|
+
});
|
|
253
256
|
|
|
254
257
|
return failed.slice(0, 10).map((r, index) => ({
|
|
255
258
|
priority: index + 1,
|
|
@@ -259,6 +262,8 @@ function toRecommendations(auditResult) {
|
|
|
259
262
|
module: moduleFromCategory(r.category),
|
|
260
263
|
risk: riskFromImpact(r.impact),
|
|
261
264
|
why: r.fix,
|
|
265
|
+
evidenceClass: (auditResult.topNextActions || []).find(item => item.key === r.key)?.evidenceClass || 'estimated',
|
|
266
|
+
rankingAdjustment: (auditResult.topNextActions || []).find(item => item.key === r.key)?.rankingAdjustment || 0,
|
|
262
267
|
}));
|
|
263
268
|
}
|
|
264
269
|
|
package/src/audit.js
CHANGED
|
@@ -6,6 +6,7 @@ const { TECHNIQUES, STACKS } = require('./techniques');
|
|
|
6
6
|
const { ProjectContext } = require('./context');
|
|
7
7
|
const { getBadgeMarkdown } = require('./badge');
|
|
8
8
|
const { sendInsights, getLocalInsights } = require('./insights');
|
|
9
|
+
const { getRecommendationOutcomeSummary, getRecommendationAdjustment } = require('./activity');
|
|
9
10
|
|
|
10
11
|
const COLORS = {
|
|
11
12
|
reset: '\x1b[0m',
|
|
@@ -98,18 +99,35 @@ function getQuickWins(failed) {
|
|
|
98
99
|
.slice(0, 3);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function
|
|
102
|
+
function getRecommendationPriorityScore(item, outcomeSummaryByKey = {}) {
|
|
103
|
+
const impactScore = (IMPACT_ORDER[item.impact] ?? 0) * 100;
|
|
104
|
+
const feedbackAdjustment = getRecommendationAdjustment(outcomeSummaryByKey, item.key);
|
|
105
|
+
const brevityPenalty = Math.min((item.fix || '').length, 240) / 20;
|
|
106
|
+
return impactScore + (feedbackAdjustment * 10) - brevityPenalty;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildTopNextActions(failed, limit = 5, outcomeSummaryByKey = {}) {
|
|
102
110
|
const pool = getPrioritizedFailed(failed);
|
|
103
111
|
|
|
104
112
|
return [...pool]
|
|
105
113
|
.sort((a, b) => {
|
|
106
|
-
|
|
107
|
-
const impactB = IMPACT_ORDER[b.impact] ?? 0;
|
|
108
|
-
if (impactA !== impactB) return impactB - impactA;
|
|
109
|
-
return (a.fix || '').length - (b.fix || '').length;
|
|
114
|
+
return getRecommendationPriorityScore(b, outcomeSummaryByKey) - getRecommendationPriorityScore(a, outcomeSummaryByKey);
|
|
110
115
|
})
|
|
111
116
|
.slice(0, limit)
|
|
112
|
-
.map(({ key, name, impact, fix, category }) =>
|
|
117
|
+
.map(({ key, name, impact, fix, category }) => {
|
|
118
|
+
const feedback = outcomeSummaryByKey[key] || null;
|
|
119
|
+
const rankingAdjustment = getRecommendationAdjustment(outcomeSummaryByKey, key);
|
|
120
|
+
const signals = [
|
|
121
|
+
`failed-check:${key}`,
|
|
122
|
+
`impact:${impact}`,
|
|
123
|
+
`category:${category}`,
|
|
124
|
+
];
|
|
125
|
+
if (feedback) {
|
|
126
|
+
signals.push(`feedback:${feedback.total}`);
|
|
127
|
+
signals.push(`ranking-adjustment:${rankingAdjustment >= 0 ? '+' : ''}${rankingAdjustment}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return ({
|
|
113
131
|
key,
|
|
114
132
|
name,
|
|
115
133
|
impact,
|
|
@@ -119,12 +137,20 @@ function buildTopNextActions(failed, limit = 5) {
|
|
|
119
137
|
why: ACTION_RATIONALES[key] || fix,
|
|
120
138
|
risk: riskFromImpact(impact),
|
|
121
139
|
confidence: confidenceFromImpact(impact),
|
|
122
|
-
signals
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
140
|
+
signals,
|
|
141
|
+
evidenceClass: feedback ? 'measured' : 'estimated',
|
|
142
|
+
rankingAdjustment,
|
|
143
|
+
feedback: feedback ? {
|
|
144
|
+
total: feedback.total,
|
|
145
|
+
accepted: feedback.accepted,
|
|
146
|
+
rejected: feedback.rejected,
|
|
147
|
+
deferred: feedback.deferred,
|
|
148
|
+
positive: feedback.positive,
|
|
149
|
+
negative: feedback.negative,
|
|
150
|
+
avgScoreDelta: feedback.avgScoreDelta,
|
|
151
|
+
} : null,
|
|
152
|
+
});
|
|
153
|
+
});
|
|
128
154
|
}
|
|
129
155
|
|
|
130
156
|
function inferSuggestedNextCommand(result) {
|
|
@@ -194,6 +220,7 @@ async function audit(options) {
|
|
|
194
220
|
const ctx = new ProjectContext(options.dir);
|
|
195
221
|
const stacks = ctx.detectStacks(STACKS);
|
|
196
222
|
const results = [];
|
|
223
|
+
const outcomeSummary = getRecommendationOutcomeSummary(options.dir);
|
|
197
224
|
|
|
198
225
|
// Run all technique checks
|
|
199
226
|
for (const [key, technique] of Object.entries(TECHNIQUES)) {
|
|
@@ -235,7 +262,7 @@ async function audit(options) {
|
|
|
235
262
|
const organicEarned = organicPassed.reduce((sum, r) => sum + (weights[r.impact] || 5), 0);
|
|
236
263
|
const organicScore = maxScore > 0 ? Math.round((organicEarned / maxScore) * 100) : 0;
|
|
237
264
|
const quickWins = getQuickWins(failed);
|
|
238
|
-
const topNextActions = buildTopNextActions(failed, 5);
|
|
265
|
+
const topNextActions = buildTopNextActions(failed, 5, outcomeSummary.byKey);
|
|
239
266
|
const result = {
|
|
240
267
|
score,
|
|
241
268
|
organicScore,
|
|
@@ -248,6 +275,10 @@ async function audit(options) {
|
|
|
248
275
|
results,
|
|
249
276
|
quickWins: quickWins.map(({ key, name, impact, fix, category }) => ({ key, name, impact, category, fix })),
|
|
250
277
|
topNextActions,
|
|
278
|
+
recommendationOutcomes: {
|
|
279
|
+
totalEntries: outcomeSummary.totalEntries,
|
|
280
|
+
keysTracked: outcomeSummary.keys,
|
|
281
|
+
},
|
|
251
282
|
};
|
|
252
283
|
result.suggestedNextCommand = inferSuggestedNextCommand(result);
|
|
253
284
|
result.liteSummary = {
|
|
@@ -344,6 +375,10 @@ async function audit(options) {
|
|
|
344
375
|
console.log(colorize(` Why: ${item.why}`, 'dim'));
|
|
345
376
|
console.log(colorize(` Trace: ${item.signals.join(' | ')}`, 'dim'));
|
|
346
377
|
console.log(colorize(` Risk: ${item.risk} | Confidence: ${item.confidence}`, 'dim'));
|
|
378
|
+
if (item.feedback) {
|
|
379
|
+
const avgDelta = Number.isFinite(item.feedback.avgScoreDelta) ? ` | Avg score delta: ${item.feedback.avgScoreDelta >= 0 ? '+' : ''}${item.feedback.avgScoreDelta}` : '';
|
|
380
|
+
console.log(colorize(` Feedback: accepted ${item.feedback.accepted}, rejected ${item.feedback.rejected}, positive ${item.feedback.positive}, negative ${item.feedback.negative}${avgDelta}`, 'dim'));
|
|
381
|
+
}
|
|
347
382
|
console.log(colorize(` Fix: ${item.fix}`, 'dim'));
|
|
348
383
|
}
|
|
349
384
|
console.log('');
|
|
@@ -382,4 +417,4 @@ async function audit(options) {
|
|
|
382
417
|
return result;
|
|
383
418
|
}
|
|
384
419
|
|
|
385
|
-
module.exports = { audit };
|
|
420
|
+
module.exports = { audit, buildTopNextActions };
|
package/src/context.js
CHANGED
|
@@ -14,6 +14,7 @@ class ProjectContext {
|
|
|
14
14
|
this.dir = dir;
|
|
15
15
|
this.files = [];
|
|
16
16
|
this._cache = {};
|
|
17
|
+
this._dependencyCache = null;
|
|
17
18
|
this._scan();
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -107,6 +108,52 @@ class ProjectContext {
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
projectDependencies() {
|
|
112
|
+
if (this._dependencyCache) return this._dependencyCache;
|
|
113
|
+
|
|
114
|
+
const deps = {};
|
|
115
|
+
const addDependency = (name, source) => {
|
|
116
|
+
if (!name) return;
|
|
117
|
+
const normalized = `${name}`.trim().toLowerCase().replace(/\[.*\]$/, '');
|
|
118
|
+
if (!normalized || normalized === 'python') return;
|
|
119
|
+
if (!deps[normalized]) {
|
|
120
|
+
deps[normalized] = source || true;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const pkg = this.jsonFile('package.json') || {};
|
|
125
|
+
for (const source of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
126
|
+
for (const name of Object.keys(pkg[source] || {})) {
|
|
127
|
+
addDependency(name, 'package.json');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const pyproject = this.fileContent('pyproject.toml') || '';
|
|
132
|
+
for (const name of extractPyprojectDependencies(pyproject)) {
|
|
133
|
+
addDependency(name, 'pyproject.toml');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const requirementFiles = [
|
|
137
|
+
'requirements.txt',
|
|
138
|
+
'requirements-dev.txt',
|
|
139
|
+
'requirements-dev.in',
|
|
140
|
+
'requirements-prod.txt',
|
|
141
|
+
'requirements/base.txt',
|
|
142
|
+
'requirements/dev.txt',
|
|
143
|
+
'requirements/test.txt',
|
|
144
|
+
];
|
|
145
|
+
for (const filePath of requirementFiles) {
|
|
146
|
+
const content = this.fileContent(filePath);
|
|
147
|
+
if (!content) continue;
|
|
148
|
+
for (const name of extractRequirementsDependencies(content)) {
|
|
149
|
+
addDependency(name, filePath);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this._dependencyCache = deps;
|
|
154
|
+
return deps;
|
|
155
|
+
}
|
|
156
|
+
|
|
110
157
|
detectStacks(STACKS) {
|
|
111
158
|
const detected = [];
|
|
112
159
|
for (const [key, stack] of Object.entries(STACKS)) {
|
|
@@ -132,4 +179,63 @@ class ProjectContext {
|
|
|
132
179
|
}
|
|
133
180
|
}
|
|
134
181
|
|
|
182
|
+
function extractPyprojectDependencies(content) {
|
|
183
|
+
if (!content) return [];
|
|
184
|
+
|
|
185
|
+
const deps = new Set();
|
|
186
|
+
const add = (value) => {
|
|
187
|
+
if (!value) return;
|
|
188
|
+
deps.add(value.trim().toLowerCase().replace(/\[.*\]$/, ''));
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const extractSection = (sectionName) => {
|
|
192
|
+
const escaped = sectionName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
193
|
+
const pattern = new RegExp(`\\[${escaped}\\]([\\s\\S]*?)(?:\\n\\s*\\[|$)`);
|
|
194
|
+
const match = content.match(pattern);
|
|
195
|
+
return match ? match[1] : '';
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const poetryDeps = extractSection('tool.poetry.dependencies');
|
|
199
|
+
for (const match of poetryDeps.matchAll(/^\s*([A-Za-z0-9_.-]+)\s*=/gm)) {
|
|
200
|
+
add(match[1]);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const projectDeps = extractSection('project');
|
|
204
|
+
const projectDepsArrayMatch = projectDeps.match(/dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
205
|
+
if (projectDepsArrayMatch) {
|
|
206
|
+
for (const item of projectDepsArrayMatch[1].matchAll(/["']([^"']+)["']/g)) {
|
|
207
|
+
const name = item[1].split(/[<>=!~ ]/)[0];
|
|
208
|
+
add(name);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const optionalDepsSection = extractSection('project.optional-dependencies');
|
|
213
|
+
for (const item of optionalDepsSection.matchAll(/["']([^"']+)["']/g)) {
|
|
214
|
+
const name = item[1].split(/[<>=!~ ]/)[0];
|
|
215
|
+
add(name);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const dependencyGroupsSection = extractSection('dependency-groups');
|
|
219
|
+
for (const item of dependencyGroupsSection.matchAll(/["']([^"']+)["']/g)) {
|
|
220
|
+
const name = item[1].split(/[<>=!~ ]/)[0];
|
|
221
|
+
add(name);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return [...deps].filter(Boolean);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function extractRequirementsDependencies(content) {
|
|
228
|
+
if (!content) return [];
|
|
229
|
+
|
|
230
|
+
const deps = new Set();
|
|
231
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
232
|
+
const line = rawLine.replace(/#.*$/, '').trim();
|
|
233
|
+
if (!line || line.startsWith('-')) continue;
|
|
234
|
+
const match = line.match(/^([A-Za-z0-9_.-]+)/);
|
|
235
|
+
if (!match) continue;
|
|
236
|
+
deps.add(match[1].toLowerCase().replace(/\[.*\]$/, ''));
|
|
237
|
+
}
|
|
238
|
+
return [...deps];
|
|
239
|
+
}
|
|
240
|
+
|
|
135
241
|
module.exports = { ProjectContext };
|