corpus-cli 0.1.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/commands/check.d.ts +1 -0
- package/dist/commands/check.js +163 -0
- package/dist/commands/init-graph.d.ts +7 -0
- package/dist/commands/init-graph.js +270 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +211 -0
- package/dist/commands/report.d.ts +1 -0
- package/dist/commands/report.js +93 -0
- package/dist/commands/scan.d.ts +1 -0
- package/dist/commands/scan.js +481 -0
- package/dist/commands/verify.d.ts +1 -0
- package/dist/commands/verify.js +334 -0
- package/dist/commands/watch.d.ts +1 -0
- package/dist/commands/watch.js +380 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +87 -0
- package/dist/utils/colors.d.ts +6 -0
- package/dist/utils/colors.js +6 -0
- package/dist/utils/config.d.ts +3 -0
- package/dist/utils/config.js +39 -0
- package/dist/utils/table.d.ts +2 -0
- package/dist/utils/table.js +24 -0
- package/package.json +28 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { watch, readFileSync, writeFileSync, statSync, readdirSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { green, amber, red, dim, bold, cyan } from '../utils/colors.js';
|
|
4
|
+
import { detectSecrets } from '@corpus/core';
|
|
5
|
+
import { checkCodeSafety } from '@corpus/core';
|
|
6
|
+
import { checkForCVEs } from '@corpus/core';
|
|
7
|
+
import { checkDependencies } from '@corpus/core';
|
|
8
|
+
import { checkFile } from '@corpus/core';
|
|
9
|
+
import { shouldSuppress } from '@corpus/core';
|
|
10
|
+
const IGNORE_DIRS = new Set([
|
|
11
|
+
'node_modules', '.git', 'dist', '.next', '__pycache__', '.venv',
|
|
12
|
+
'venv', '.cache', '.turbo', 'coverage', '.nyc_output', '.claude',
|
|
13
|
+
'.swarm', '.claude-flow', '.insforge', '.corpus',
|
|
14
|
+
]);
|
|
15
|
+
const SCAN_EXTENSIONS = new Set([
|
|
16
|
+
'.ts', '.tsx', '.js', '.jsx', '.py', '.json', '.yaml', '.yml',
|
|
17
|
+
'.env', '.toml', '.sh', '.sql', '.tf', '.hcl', '.md',
|
|
18
|
+
]);
|
|
19
|
+
const stats = {
|
|
20
|
+
filesScanned: 0,
|
|
21
|
+
issuesFound: 0,
|
|
22
|
+
criticalCount: 0,
|
|
23
|
+
warningCount: 0,
|
|
24
|
+
passCount: 0,
|
|
25
|
+
startTime: Date.now(),
|
|
26
|
+
lastScanTime: '-',
|
|
27
|
+
recentEvents: [],
|
|
28
|
+
cvesDetected: 0,
|
|
29
|
+
depsChecked: 0,
|
|
30
|
+
contractViolations: 0,
|
|
31
|
+
autoHealed: 0,
|
|
32
|
+
};
|
|
33
|
+
function deepScan(content, filepath, projectRoot) {
|
|
34
|
+
const results = [];
|
|
35
|
+
// Core secret detection
|
|
36
|
+
try {
|
|
37
|
+
const secrets = detectSecrets(content, filepath);
|
|
38
|
+
for (const s of secrets) {
|
|
39
|
+
results.push({ severity: s.severity === 'CRITICAL' ? 'CRIT' : 'WARN', message: `${s.type}: ${s.redacted}`, type: s.type });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
// Code safety
|
|
44
|
+
try {
|
|
45
|
+
const safety = checkCodeSafety(content, filepath);
|
|
46
|
+
for (const s of safety) {
|
|
47
|
+
// Check pattern intelligence for suppression
|
|
48
|
+
const suppression = shouldSuppress(projectRoot, s.rule, filepath);
|
|
49
|
+
if (suppression.suppress)
|
|
50
|
+
continue;
|
|
51
|
+
results.push({ severity: s.severity === 'CRITICAL' ? 'CRIT' : s.severity === 'WARNING' ? 'WARN' : 'INFO', message: s.message, type: s.rule });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
// CVE pattern detection
|
|
56
|
+
try {
|
|
57
|
+
const cves = checkForCVEs(content, filepath);
|
|
58
|
+
for (const c of cves) {
|
|
59
|
+
results.push({ severity: 'CRIT', message: `${c.cveId}: ${c.name}`, type: `CVE:${c.cveId}`, cveId: c.cveId });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch { }
|
|
63
|
+
// Graph contract verification (if graph exists)
|
|
64
|
+
try {
|
|
65
|
+
const graphResult = checkFile(projectRoot, filepath, content);
|
|
66
|
+
if (graphResult.verdict === 'VIOLATES') {
|
|
67
|
+
for (const v of graphResult.violations) {
|
|
68
|
+
results.push({ severity: v.severity === 'CRITICAL' ? 'CRIT' : 'WARN', message: `Contract: ${v.message}`, type: `contract:${v.type}` });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch { }
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
// Async dependency check (runs separately due to network)
|
|
76
|
+
async function checkDeps(content, filepath, projectRoot) {
|
|
77
|
+
const results = [];
|
|
78
|
+
try {
|
|
79
|
+
const findings = await checkDependencies(content, filepath, { projectRoot });
|
|
80
|
+
for (const f of findings) {
|
|
81
|
+
results.push({
|
|
82
|
+
severity: f.severity === 'CRITICAL' ? 'CRIT' : 'WARN',
|
|
83
|
+
message: `Dep: ${f.package} (${f.reason})${f.similarPackages?.length ? ' → did you mean ' + f.similarPackages[0] + '?' : ''}`,
|
|
84
|
+
type: `dep:${f.reason}`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
function isScannable(filepath) {
|
|
92
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
93
|
+
return SCAN_EXTENSIONS.has(ext) || filepath.includes('.env');
|
|
94
|
+
}
|
|
95
|
+
// ── Disk persistence for web dashboard ───────────────────────────────────────
|
|
96
|
+
const DASHBOARD_FILE = '/tmp/corpus-dashboard.json';
|
|
97
|
+
let eventsFilePath = '';
|
|
98
|
+
function ensureCorpusDir(projectRoot) {
|
|
99
|
+
const corpusDir = path.join(projectRoot, '.corpus');
|
|
100
|
+
if (!existsSync(corpusDir))
|
|
101
|
+
mkdirSync(corpusDir, { recursive: true });
|
|
102
|
+
eventsFilePath = path.join(corpusDir, 'events.json');
|
|
103
|
+
}
|
|
104
|
+
function writeDashboardState(dir) {
|
|
105
|
+
const passRate = stats.filesScanned > 0
|
|
106
|
+
? Math.round((stats.passCount / stats.filesScanned) * 100)
|
|
107
|
+
: 100;
|
|
108
|
+
const dashboard = {
|
|
109
|
+
score: passRate,
|
|
110
|
+
filesScanned: stats.filesScanned,
|
|
111
|
+
issues: {
|
|
112
|
+
critical: stats.criticalCount,
|
|
113
|
+
warning: stats.warningCount,
|
|
114
|
+
info: 0,
|
|
115
|
+
},
|
|
116
|
+
events: stats.recentEvents.map(e => ({
|
|
117
|
+
time: e.time,
|
|
118
|
+
file: e.file,
|
|
119
|
+
severity: e.severity,
|
|
120
|
+
message: e.message,
|
|
121
|
+
})),
|
|
122
|
+
uptime: uptime(),
|
|
123
|
+
lastUpdate: formatTime(),
|
|
124
|
+
status: 'active',
|
|
125
|
+
cvesDetected: stats.cvesDetected,
|
|
126
|
+
contractViolations: stats.contractViolations,
|
|
127
|
+
depsChecked: stats.depsChecked,
|
|
128
|
+
autoHealed: stats.autoHealed,
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
writeFileSync(DASHBOARD_FILE, JSON.stringify(dashboard));
|
|
132
|
+
}
|
|
133
|
+
catch { /* /tmp write failed */ }
|
|
134
|
+
}
|
|
135
|
+
function writeEventToDisk(event) {
|
|
136
|
+
if (!eventsFilePath)
|
|
137
|
+
return;
|
|
138
|
+
try {
|
|
139
|
+
let events = [];
|
|
140
|
+
if (existsSync(eventsFilePath)) {
|
|
141
|
+
events = JSON.parse(readFileSync(eventsFilePath, 'utf-8'));
|
|
142
|
+
}
|
|
143
|
+
events.push({
|
|
144
|
+
...event,
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
});
|
|
147
|
+
// Keep last 200 events
|
|
148
|
+
if (events.length > 200)
|
|
149
|
+
events = events.slice(-200);
|
|
150
|
+
writeFileSync(eventsFilePath, JSON.stringify(events));
|
|
151
|
+
}
|
|
152
|
+
catch { /* write failed */ }
|
|
153
|
+
}
|
|
154
|
+
function formatTime() {
|
|
155
|
+
const now = new Date();
|
|
156
|
+
return `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
157
|
+
}
|
|
158
|
+
function uptime() {
|
|
159
|
+
const ms = Date.now() - stats.startTime;
|
|
160
|
+
const s = Math.floor(ms / 1000);
|
|
161
|
+
if (s < 60)
|
|
162
|
+
return `${s}s`;
|
|
163
|
+
const m = Math.floor(s / 60);
|
|
164
|
+
if (m < 60)
|
|
165
|
+
return `${m}m ${s % 60}s`;
|
|
166
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
167
|
+
}
|
|
168
|
+
// ── Dashboard rendering ─────────────────────────────────────────────────────
|
|
169
|
+
function renderDashboard(dir) {
|
|
170
|
+
const passRate = stats.filesScanned > 0
|
|
171
|
+
? Math.round((stats.passCount / stats.filesScanned) * 100)
|
|
172
|
+
: 100;
|
|
173
|
+
const passColor = passRate >= 95 ? green : passRate >= 80 ? amber : red;
|
|
174
|
+
process.stdout.write('\x1b[2J\x1b[H'); // Clear screen
|
|
175
|
+
process.stdout.write('\n');
|
|
176
|
+
process.stdout.write(bold(` CORPUS WATCH`) + dim(` ${dir}\n`));
|
|
177
|
+
process.stdout.write(' ' + '\u2550'.repeat(60) + '\n\n');
|
|
178
|
+
// Stats row
|
|
179
|
+
process.stdout.write(` ${bold('Pass rate')} ${passColor(`${passRate}%`)} `);
|
|
180
|
+
process.stdout.write(`${bold('Scanned')} ${cyan(String(stats.filesScanned))} `);
|
|
181
|
+
process.stdout.write(`${bold('Issues')} ${stats.issuesFound > 0 ? red(String(stats.issuesFound)) : green('0')} `);
|
|
182
|
+
process.stdout.write(`${bold('Uptime')} ${dim(uptime())}\n`);
|
|
183
|
+
process.stdout.write('\n');
|
|
184
|
+
// Breakdown
|
|
185
|
+
if (stats.criticalCount > 0 || stats.warningCount > 0) {
|
|
186
|
+
process.stdout.write(` ${red(`\u2716 ${stats.criticalCount} critical`)} ${amber(`\u26A0 ${stats.warningCount} warning`)} ${green(`\u2714 ${stats.passCount} clean`)}\n\n`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
process.stdout.write(` ${green(`\u2714 ${stats.passCount} clean`)} ${dim('No issues found')}\n\n`);
|
|
190
|
+
}
|
|
191
|
+
// Intelligence stats
|
|
192
|
+
if (stats.cvesDetected > 0 || stats.contractViolations > 0 || stats.depsChecked > 0) {
|
|
193
|
+
process.stdout.write(` ${red(`CVEs: ${stats.cvesDetected}`)} ${amber(`Contracts: ${stats.contractViolations}`)} ${cyan(`Deps checked: ${stats.depsChecked}`)} ${green(`Auto-healed: ${stats.autoHealed}`)}\n\n`);
|
|
194
|
+
}
|
|
195
|
+
// Recent events — prioritize findings over PASS
|
|
196
|
+
process.stdout.write(dim(' Recent activity:\n'));
|
|
197
|
+
if (stats.recentEvents.length === 0) {
|
|
198
|
+
process.stdout.write(dim(' Waiting for file changes...\n'));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Show findings first, then recent clean files
|
|
202
|
+
const findings = stats.recentEvents.filter(e => e.severity !== 'PASS');
|
|
203
|
+
const clean = stats.recentEvents.filter(e => e.severity === 'PASS');
|
|
204
|
+
const display = [...findings.slice(-10), ...clean.slice(-2)].slice(-12);
|
|
205
|
+
for (const event of display) {
|
|
206
|
+
const sev = event.severity === 'CRIT' ? red('CRIT') :
|
|
207
|
+
event.severity === 'WARN' ? amber('WARN') :
|
|
208
|
+
event.severity === 'INFO' ? dim('INFO') :
|
|
209
|
+
green('PASS');
|
|
210
|
+
process.stdout.write(` ${dim(event.time)} ${sev} ${event.file.padEnd(38)} ${dim(event.message)}\n`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
process.stdout.write('\n' + dim(' Press Ctrl+C to stop\n'));
|
|
214
|
+
}
|
|
215
|
+
// ── Initial scan ────────────────────────────────────────────────────────────
|
|
216
|
+
function initialScan(dir) {
|
|
217
|
+
function walkAndScan(d) {
|
|
218
|
+
try {
|
|
219
|
+
for (const entry of readdirSync(d)) {
|
|
220
|
+
if (IGNORE_DIRS.has(entry))
|
|
221
|
+
continue;
|
|
222
|
+
const full = path.join(d, entry);
|
|
223
|
+
try {
|
|
224
|
+
const s = statSync(full);
|
|
225
|
+
if (s.isDirectory())
|
|
226
|
+
walkAndScan(full);
|
|
227
|
+
else if (s.isFile() && isScannable(full)) {
|
|
228
|
+
const content = readFileSync(full, 'utf-8');
|
|
229
|
+
const findings = deepScan(content, full, dir);
|
|
230
|
+
stats.filesScanned++;
|
|
231
|
+
const cveCount = findings.filter(f => f.type.startsWith('CVE:')).length;
|
|
232
|
+
const contractCount = findings.filter(f => f.type.startsWith('contract:')).length;
|
|
233
|
+
stats.cvesDetected += cveCount;
|
|
234
|
+
stats.contractViolations += contractCount;
|
|
235
|
+
if (findings.length === 0) {
|
|
236
|
+
stats.passCount++;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
stats.issuesFound += findings.length;
|
|
240
|
+
const hasCrit = findings.some((f) => f.severity === 'CRIT');
|
|
241
|
+
if (hasCrit)
|
|
242
|
+
stats.criticalCount++;
|
|
243
|
+
else
|
|
244
|
+
stats.warningCount++;
|
|
245
|
+
stats.recentEvents.push({
|
|
246
|
+
time: formatTime(),
|
|
247
|
+
file: path.relative(dir, full),
|
|
248
|
+
severity: hasCrit ? 'CRIT' : 'WARN',
|
|
249
|
+
message: findings[0].message,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch { /* skip */ }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch { /* skip */ }
|
|
258
|
+
}
|
|
259
|
+
walkAndScan(dir);
|
|
260
|
+
}
|
|
261
|
+
// ── Main ────────────────────────────────────────────────────────────────────
|
|
262
|
+
export async function runWatch() {
|
|
263
|
+
const targetDir = process.argv[3] || '.';
|
|
264
|
+
const resolvedDir = path.resolve(targetDir);
|
|
265
|
+
// Ensure .corpus dir exists for event writing
|
|
266
|
+
ensureCorpusDir(resolvedDir);
|
|
267
|
+
// Initial scan
|
|
268
|
+
process.stdout.write(dim('\n Running initial scan...\n'));
|
|
269
|
+
initialScan(resolvedDir);
|
|
270
|
+
// Write initial dashboard state
|
|
271
|
+
writeDashboardState(resolvedDir);
|
|
272
|
+
// Render dashboard
|
|
273
|
+
renderDashboard(resolvedDir);
|
|
274
|
+
const debounce = new Map();
|
|
275
|
+
function handleFileChange(filepath) {
|
|
276
|
+
if (!isScannable(filepath))
|
|
277
|
+
return;
|
|
278
|
+
const existing = debounce.get(filepath);
|
|
279
|
+
if (existing)
|
|
280
|
+
clearTimeout(existing);
|
|
281
|
+
debounce.set(filepath, setTimeout(() => {
|
|
282
|
+
debounce.delete(filepath);
|
|
283
|
+
try {
|
|
284
|
+
const s = statSync(filepath);
|
|
285
|
+
if (!s.isFile())
|
|
286
|
+
return;
|
|
287
|
+
const content = readFileSync(filepath, 'utf-8');
|
|
288
|
+
const findings = deepScan(content, filepath, resolvedDir);
|
|
289
|
+
const relPath = path.relative(resolvedDir, filepath);
|
|
290
|
+
const time = formatTime();
|
|
291
|
+
stats.filesScanned++;
|
|
292
|
+
stats.lastScanTime = time;
|
|
293
|
+
const cveCount = findings.filter(f => f.type.startsWith('CVE:')).length;
|
|
294
|
+
const contractCount = findings.filter(f => f.type.startsWith('contract:')).length;
|
|
295
|
+
stats.cvesDetected += cveCount;
|
|
296
|
+
stats.contractViolations += contractCount;
|
|
297
|
+
if (findings.length === 0) {
|
|
298
|
+
stats.passCount++;
|
|
299
|
+
stats.recentEvents.push({ time, file: relPath, severity: 'PASS', message: 'Clean' });
|
|
300
|
+
writeEventToDisk({ type: 'verified', file: relPath, verdict: 'VERIFIED', details: 'All contracts satisfied' });
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
stats.issuesFound += findings.length;
|
|
304
|
+
const hasCrit = findings.some((f) => f.severity === 'CRIT');
|
|
305
|
+
if (hasCrit)
|
|
306
|
+
stats.criticalCount++;
|
|
307
|
+
else
|
|
308
|
+
stats.warningCount++;
|
|
309
|
+
for (const f of findings) {
|
|
310
|
+
stats.recentEvents.push({ time, file: relPath, severity: f.severity, message: f.message });
|
|
311
|
+
writeEventToDisk({
|
|
312
|
+
type: f.severity === 'CRIT' ? 'violation' : 'scan',
|
|
313
|
+
file: relPath,
|
|
314
|
+
verdict: f.severity === 'CRIT' ? 'VIOLATION' : 'WARNING',
|
|
315
|
+
details: f.message,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Async: check dependencies (non-blocking)
|
|
320
|
+
checkDeps(content, filepath, resolvedDir).then(depResults => {
|
|
321
|
+
if (depResults.length > 0) {
|
|
322
|
+
stats.depsChecked++;
|
|
323
|
+
for (const r of depResults) {
|
|
324
|
+
stats.issuesFound++;
|
|
325
|
+
if (r.severity === 'CRIT')
|
|
326
|
+
stats.criticalCount++;
|
|
327
|
+
else
|
|
328
|
+
stats.warningCount++;
|
|
329
|
+
stats.recentEvents.push({ time: formatTime(), file: relPath, severity: r.severity, message: r.message });
|
|
330
|
+
}
|
|
331
|
+
renderDashboard(resolvedDir);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
// Keep only last 50 events
|
|
335
|
+
if (stats.recentEvents.length > 50) {
|
|
336
|
+
stats.recentEvents = stats.recentEvents.slice(-50);
|
|
337
|
+
}
|
|
338
|
+
renderDashboard(resolvedDir);
|
|
339
|
+
}
|
|
340
|
+
catch { /* file deleted */ }
|
|
341
|
+
}, 300));
|
|
342
|
+
}
|
|
343
|
+
// Watch
|
|
344
|
+
try {
|
|
345
|
+
watch(resolvedDir, { recursive: true }, (_event, filename) => {
|
|
346
|
+
if (!filename)
|
|
347
|
+
return;
|
|
348
|
+
const fullPath = path.join(resolvedDir, filename);
|
|
349
|
+
const parts = filename.split(path.sep);
|
|
350
|
+
if (parts.some((p) => IGNORE_DIRS.has(p)))
|
|
351
|
+
return;
|
|
352
|
+
handleFileChange(fullPath);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
process.stderr.write(red(` Could not watch ${resolvedDir}\n`));
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
// Refresh dashboard every 5s (uptime counter)
|
|
360
|
+
setInterval(() => renderDashboard(resolvedDir), 5000);
|
|
361
|
+
await new Promise(() => {
|
|
362
|
+
process.on('SIGINT', () => {
|
|
363
|
+
process.stdout.write('\n\n');
|
|
364
|
+
process.stdout.write(bold(' CORPUS WATCH SESSION SUMMARY\n'));
|
|
365
|
+
process.stdout.write(' ' + '\u2550'.repeat(40) + '\n');
|
|
366
|
+
process.stdout.write(` Files scanned: ${stats.filesScanned}\n`);
|
|
367
|
+
process.stdout.write(` Issues found: ${stats.issuesFound}\n`);
|
|
368
|
+
process.stdout.write(` Critical: ${stats.criticalCount}\n`);
|
|
369
|
+
process.stdout.write(` Warnings: ${stats.warningCount}\n`);
|
|
370
|
+
process.stdout.write(` Clean: ${stats.passCount}\n`);
|
|
371
|
+
process.stdout.write(` CVEs detected: ${stats.cvesDetected}\n`);
|
|
372
|
+
process.stdout.write(` Deps checked: ${stats.depsChecked}\n`);
|
|
373
|
+
process.stdout.write(` Violations: ${stats.contractViolations}\n`);
|
|
374
|
+
process.stdout.write(` Auto-healed: ${stats.autoHealed}\n`);
|
|
375
|
+
process.stdout.write(` Duration: ${uptime()}\n`);
|
|
376
|
+
process.stdout.write('\n');
|
|
377
|
+
process.exit(0);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
const command = process.argv[2];
|
|
4
|
+
async function main() {
|
|
5
|
+
switch (command) {
|
|
6
|
+
case 'init': {
|
|
7
|
+
const { runInit } = await import('./commands/init.js');
|
|
8
|
+
await runInit();
|
|
9
|
+
// After init completes, also build the codebase graph
|
|
10
|
+
const { initGraph } = await import('./commands/init-graph.js');
|
|
11
|
+
await initGraph(process.argv.slice(3));
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
case 'graph': {
|
|
15
|
+
const { initGraph } = await import('./commands/init-graph.js');
|
|
16
|
+
await initGraph(process.argv.slice(3));
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
case 'verify': {
|
|
20
|
+
const { runVerify } = await import('./commands/verify.js');
|
|
21
|
+
await runVerify();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case 'scan': {
|
|
25
|
+
const { runScan } = await import('./commands/scan.js');
|
|
26
|
+
await runScan();
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case 'watch': {
|
|
30
|
+
const { runWatch } = await import('./commands/watch.js');
|
|
31
|
+
await runWatch();
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case 'check': {
|
|
35
|
+
const { runCheck } = await import('./commands/check.js');
|
|
36
|
+
await runCheck();
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
case 'report': {
|
|
40
|
+
const { runReport } = await import('./commands/report.js');
|
|
41
|
+
await runReport();
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
process.stdout.write(`
|
|
46
|
+
corpus - Runtime safety for AI agents and AI-generated code
|
|
47
|
+
|
|
48
|
+
Trust Verification:
|
|
49
|
+
verify Compute trust score (0-100) per file with line-by-line findings
|
|
50
|
+
|
|
51
|
+
Security Scanning:
|
|
52
|
+
scan Scan files for secrets, PII, injection patterns, unsafe code
|
|
53
|
+
watch Real-time file watcher with live security scanning
|
|
54
|
+
|
|
55
|
+
Policy Management:
|
|
56
|
+
init Initialize Corpus in your project (+ pre-commit hooks + graph)
|
|
57
|
+
check Validate all policy files
|
|
58
|
+
report View your agent's behavioral report
|
|
59
|
+
|
|
60
|
+
Graph & Analysis:
|
|
61
|
+
graph Build / rebuild the codebase graph
|
|
62
|
+
|
|
63
|
+
Usage:
|
|
64
|
+
corpus verify Trust score for your codebase
|
|
65
|
+
corpus verify --json Machine-readable trust report
|
|
66
|
+
corpus scan Scan current directory
|
|
67
|
+
corpus scan --staged Scan staged git changes (pre-commit)
|
|
68
|
+
corpus scan --json Machine-readable output for CI
|
|
69
|
+
corpus watch Watch files and scan on every save
|
|
70
|
+
corpus watch src/ Watch specific directory
|
|
71
|
+
corpus init Set up Corpus in your project
|
|
72
|
+
corpus graph Build the codebase graph
|
|
73
|
+
corpus check Validate policy files
|
|
74
|
+
|
|
75
|
+
MCP Server (for AI coding tools):
|
|
76
|
+
npx corpus-mcp Start the MCP server for Claude Code / Cursor
|
|
77
|
+
|
|
78
|
+
`);
|
|
79
|
+
if (command && command !== '--help' && command !== '-h') {
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
main().catch((e) => {
|
|
85
|
+
process.stderr.write(`Error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const green: (s: string) => string;
|
|
2
|
+
export declare const amber: (s: string) => string;
|
|
3
|
+
export declare const red: (s: string) => string;
|
|
4
|
+
export declare const dim: (s: string) => string;
|
|
5
|
+
export declare const bold: (s: string) => string;
|
|
6
|
+
export declare const cyan: (s: string) => string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
2
|
+
export const amber = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
3
|
+
export const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
4
|
+
export const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
5
|
+
export const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
6
|
+
export const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
export function readPolicyFile(path = './corpus.policy.yaml') {
|
|
3
|
+
if (!existsSync(path))
|
|
4
|
+
return null;
|
|
5
|
+
const raw = readFileSync(path, 'utf-8');
|
|
6
|
+
// Simple YAML key extraction for CLI (no dependency).
|
|
7
|
+
// NOTE: This parser only handles top-level scalar key:value pairs.
|
|
8
|
+
// Nested objects, arrays, multi-line strings, and anchors are NOT supported.
|
|
9
|
+
// This is sufficient for reading `agent` and `version` fields used by report.ts,
|
|
10
|
+
// but should be replaced with a proper YAML parser if deeper parsing is needed.
|
|
11
|
+
const lines = raw.split('\n');
|
|
12
|
+
const result = {};
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
const match = line.match(/^(\w+):\s*(.+)/);
|
|
15
|
+
if (match) {
|
|
16
|
+
result[match[1]] = match[2].replace(/^["']|["']$/g, '');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
export function readEnvFile(path = './.env.corpus') {
|
|
22
|
+
if (!existsSync(path))
|
|
23
|
+
return {};
|
|
24
|
+
const raw = readFileSync(path, 'utf-8');
|
|
25
|
+
const result = {};
|
|
26
|
+
for (const line of raw.split('\n')) {
|
|
27
|
+
const match = line.match(/^([A-Z_]+)=(.+)/);
|
|
28
|
+
if (match) {
|
|
29
|
+
result[match[1]] = match[2];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
export function writeEnvFile(path, vars) {
|
|
35
|
+
const content = Object.entries(vars)
|
|
36
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
37
|
+
.join('\n') + '\n';
|
|
38
|
+
writeFileSync(path, content);
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function renderTable(rows) {
|
|
2
|
+
if (rows.length === 0)
|
|
3
|
+
return '';
|
|
4
|
+
const colWidths = [];
|
|
5
|
+
for (const row of rows) {
|
|
6
|
+
for (let i = 0; i < row.length; i++) {
|
|
7
|
+
const stripped = row[i].replace(/\x1b\[[0-9;]*m/g, '');
|
|
8
|
+
colWidths[i] = Math.max(colWidths[i] ?? 0, stripped.length);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return rows
|
|
12
|
+
.map((row) => row
|
|
13
|
+
.map((cell, i) => {
|
|
14
|
+
const stripped = cell.replace(/\x1b\[[0-9;]*m/g, '');
|
|
15
|
+
const padding = colWidths[i] - stripped.length;
|
|
16
|
+
return cell + ' '.repeat(Math.max(0, padding));
|
|
17
|
+
})
|
|
18
|
+
.join(' '))
|
|
19
|
+
.join('\n');
|
|
20
|
+
}
|
|
21
|
+
export function progressBar(value, width = 20) {
|
|
22
|
+
const filled = Math.round((value / 100) * width);
|
|
23
|
+
return '\u2588'.repeat(filled) + '\u2591'.repeat(width - filled);
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "corpus-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The immune system for AI-generated code — scan, watch, and auto-heal vibe-coded software",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"corpus": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"corpus-core": "0.1.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.11.0",
|
|
20
|
+
"typescript": "^5.3.0"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["ai", "security", "scanner", "trust-score", "secrets", "vibe-coding", "claude", "cursor", "copilot", "mcp", "jac"],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/fluentflier/corpus"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT"
|
|
28
|
+
}
|