thuban 0.3.1 → 0.3.2
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/cli.js +342 -6
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const fs = require('fs');
|
|
23
|
+
const os = require('os');
|
|
23
24
|
|
|
24
25
|
// Import engine components
|
|
25
26
|
const CodeScanner = require('./packages/scanner/code-scanner.js');
|
|
@@ -93,7 +94,8 @@ function printHelp() {
|
|
|
93
94
|
// ─── SCAN & DETECT ───────────────────────────────────────
|
|
94
95
|
console.log(c('bold', ' SCAN & DETECT'));
|
|
95
96
|
console.log('');
|
|
96
|
-
console.log(` ${c('cyan', 'scan')} [path]
|
|
97
|
+
console.log(` ${c('cyan', 'scan')} [path|url] Full health scan — score, grade, all issues`);
|
|
98
|
+
console.log(` ${c('cyan', 'compare')} [a] [b] Compare two codebases side by side`);
|
|
97
99
|
console.log(` ${c('cyan', 'hallucinate')} [path] Find phantom APIs and invented imports`);
|
|
98
100
|
console.log(` ${c('cyan', 'ghosts')} [path] Dead functions nobody calls`);
|
|
99
101
|
console.log(` ${c('cyan', 'ai-score')} [path] Probability each function is AI-generated`);
|
|
@@ -166,6 +168,9 @@ function printHelp() {
|
|
|
166
168
|
console.log(c('gray', ' # Scan a specific folder, ignore tests'));
|
|
167
169
|
console.log(` ${c('white', 'npx thuban scan ./src --ignore "**/*.test.js"')}`);
|
|
168
170
|
console.log('');
|
|
171
|
+
console.log(c('gray', ' Report a bug or request a feature:'));
|
|
172
|
+
console.log(` ${c('cyan', 'https://github.com/SilverwingsBenefitsGit/thuban/issues')}`);
|
|
173
|
+
console.log('');
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
function parseArgs(args) {
|
|
@@ -204,7 +209,14 @@ function parseArgs(args) {
|
|
|
204
209
|
else if (arg === '--version' || arg === '-v') { opts.command = 'version'; }
|
|
205
210
|
else if (!opts.command) { opts.command = arg; }
|
|
206
211
|
else if (opts.command === 'activate' && !opts.licenseKey) { opts.licenseKey = arg; }
|
|
207
|
-
else if (!noPathCommands.includes(opts.command)) {
|
|
212
|
+
else if (!noPathCommands.includes(opts.command)) {
|
|
213
|
+
// Don't resolve git URLs as local paths
|
|
214
|
+
if (arg.match(/^(https?:\/\/|git@)/)) {
|
|
215
|
+
opts.targetPath = arg;
|
|
216
|
+
} else {
|
|
217
|
+
opts.targetPath = path.resolve(arg);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
208
220
|
i++;
|
|
209
221
|
}
|
|
210
222
|
|
|
@@ -256,7 +268,34 @@ function collectFiles(dir, ignorePatterns = []) {
|
|
|
256
268
|
|
|
257
269
|
async function cmdScan(opts) {
|
|
258
270
|
const startTime = Date.now();
|
|
259
|
-
const
|
|
271
|
+
const gitUrlPattern = /^(https?:\/\/|git@)/;
|
|
272
|
+
const isGitUrl = gitUrlPattern.test(opts.targetPath);
|
|
273
|
+
let targetPath = isGitUrl ? opts.targetPath : path.resolve(opts.targetPath);
|
|
274
|
+
let clonedRepo = null;
|
|
275
|
+
|
|
276
|
+
// ─── Git URL detection — clone and scan ─────────────────
|
|
277
|
+
if (isGitUrl) {
|
|
278
|
+
if (!opts.json) {
|
|
279
|
+
console.log('');
|
|
280
|
+
console.log(c('cyan', ' Cloning repository...'));
|
|
281
|
+
console.log(c('gray', ` ${opts.targetPath}`));
|
|
282
|
+
}
|
|
283
|
+
const tmpDir = path.join(os.tmpdir(), 'thuban-scan-' + Date.now());
|
|
284
|
+
const { execSync } = require('child_process');
|
|
285
|
+
try {
|
|
286
|
+
execSync(`git clone --depth 1 "${opts.targetPath}" "${tmpDir}"`, { stdio: 'pipe', timeout: 60000 });
|
|
287
|
+
targetPath = tmpDir;
|
|
288
|
+
clonedRepo = tmpDir;
|
|
289
|
+
if (!opts.json) {
|
|
290
|
+
console.log(c('cyan', ' Cloned successfully'));
|
|
291
|
+
console.log('');
|
|
292
|
+
}
|
|
293
|
+
} catch (e) {
|
|
294
|
+
console.error(c('red', ` Failed to clone repository: ${e.message}`));
|
|
295
|
+
console.error(c('gray', ' Check the URL is correct and the repo is accessible'));
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
260
299
|
|
|
261
300
|
// ─── License check ───────────────────────────────────────
|
|
262
301
|
const lm = new LicenseManager();
|
|
@@ -966,6 +1005,11 @@ async function cmdReport(opts) {
|
|
|
966
1005
|
if (orphans.length > 5) console.log(` ${c('gray', '→')} Consider removing ${orphans.length} orphan files`);
|
|
967
1006
|
console.log('');
|
|
968
1007
|
}
|
|
1008
|
+
|
|
1009
|
+
// Clean up cloned repo
|
|
1010
|
+
if (clonedRepo) {
|
|
1011
|
+
try { fs.rmSync(clonedRepo, { recursive: true, force: true }); } catch {}
|
|
1012
|
+
}
|
|
969
1013
|
}
|
|
970
1014
|
|
|
971
1015
|
// ─── Tech Debt & Fix Commands ────────────────────────────
|
|
@@ -1948,6 +1992,292 @@ async function cmdBaseline(opts) {
|
|
|
1948
1992
|
}
|
|
1949
1993
|
}
|
|
1950
1994
|
|
|
1995
|
+
// ─── Telemetry (opt-in anonymous usage stats) ───────────────
|
|
1996
|
+
|
|
1997
|
+
function getTelemetryConfigPath() {
|
|
1998
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
1999
|
+
return path.join(home, '.thuban-telemetry.json');
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function isTelemetryEnabled() {
|
|
2003
|
+
try {
|
|
2004
|
+
const config = JSON.parse(fs.readFileSync(getTelemetryConfigPath(), 'utf-8'));
|
|
2005
|
+
return config.enabled === true;
|
|
2006
|
+
} catch { return false; }
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
function sendAnonymousTelemetry(data) {
|
|
2010
|
+
if (!isTelemetryEnabled()) return;
|
|
2011
|
+
try {
|
|
2012
|
+
const payload = JSON.stringify({
|
|
2013
|
+
v: data.version || '0.3.1',
|
|
2014
|
+
cmd: data.command,
|
|
2015
|
+
files: data.fileCount,
|
|
2016
|
+
score: data.score,
|
|
2017
|
+
grade: data.grade,
|
|
2018
|
+
langs: data.languages,
|
|
2019
|
+
issues: data.issueCount,
|
|
2020
|
+
duration: data.duration,
|
|
2021
|
+
os: process.platform,
|
|
2022
|
+
node: process.version,
|
|
2023
|
+
ts: new Date().toISOString()
|
|
2024
|
+
});
|
|
2025
|
+
// Fire and forget — never blocks the CLI
|
|
2026
|
+
const https = require('https');
|
|
2027
|
+
const req = https.request({
|
|
2028
|
+
hostname: 'thuban-telemetry.silverwingsbenefits.workers.dev',
|
|
2029
|
+
path: '/report',
|
|
2030
|
+
method: 'POST',
|
|
2031
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
2032
|
+
timeout: 3000
|
|
2033
|
+
});
|
|
2034
|
+
req.on('error', () => {}); // silently ignore
|
|
2035
|
+
req.write(payload);
|
|
2036
|
+
req.end();
|
|
2037
|
+
} catch { /* never fail the CLI for telemetry */ }
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
async function cmdTelemetry(opts) {
|
|
2041
|
+
const configPath = getTelemetryConfigPath();
|
|
2042
|
+
const args = process.argv.slice(2);
|
|
2043
|
+
|
|
2044
|
+
if (args.includes('--on')) {
|
|
2045
|
+
fs.writeFileSync(configPath, JSON.stringify({ enabled: true, updated: new Date().toISOString() }));
|
|
2046
|
+
console.log('');
|
|
2047
|
+
console.log(c('cyan', ' Telemetry enabled'));
|
|
2048
|
+
console.log(c('gray', ' Anonymous usage stats will be sent (no code, no paths, no PII)'));
|
|
2049
|
+
console.log(c('gray', ' Turn off anytime: npx thuban telemetry --off'));
|
|
2050
|
+
console.log('');
|
|
2051
|
+
} else if (args.includes('--off')) {
|
|
2052
|
+
fs.writeFileSync(configPath, JSON.stringify({ enabled: false, updated: new Date().toISOString() }));
|
|
2053
|
+
console.log('');
|
|
2054
|
+
console.log(c('cyan', ' Telemetry disabled'));
|
|
2055
|
+
console.log(c('gray', ' No data will be collected'));
|
|
2056
|
+
console.log('');
|
|
2057
|
+
} else {
|
|
2058
|
+
const enabled = isTelemetryEnabled();
|
|
2059
|
+
console.log('');
|
|
2060
|
+
console.log(c('bold', ' TELEMETRY STATUS'));
|
|
2061
|
+
console.log('');
|
|
2062
|
+
console.log(` Status: ${enabled ? c('cyan', 'Enabled') : c('gray', 'Disabled (default)')}`);
|
|
2063
|
+
console.log('');
|
|
2064
|
+
console.log(c('gray', ' Thuban telemetry is 100% opt-in and anonymous.'));
|
|
2065
|
+
console.log(c('gray', ' We collect: file count, score, grade, languages, OS, scan duration.'));
|
|
2066
|
+
console.log(c('gray', ' We never collect: code, file names, paths, project names, or PII.'));
|
|
2067
|
+
console.log('');
|
|
2068
|
+
console.log(` Enable: ${c('cyan', 'npx thuban telemetry --on')}`);
|
|
2069
|
+
console.log(` Disable: ${c('cyan', 'npx thuban telemetry --off')}`);
|
|
2070
|
+
console.log('');
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// ─── Internal scan (returns data without printing) ──────────
|
|
2075
|
+
|
|
2076
|
+
async function runScanSilent(targetInput) {
|
|
2077
|
+
const gitUrlPattern = /^(https?:\/\/|git@)/;
|
|
2078
|
+
const isGitUrl = gitUrlPattern.test(targetInput);
|
|
2079
|
+
let targetPath = isGitUrl ? targetInput : path.resolve(targetInput);
|
|
2080
|
+
let clonedRepo = null;
|
|
2081
|
+
let label = targetInput;
|
|
2082
|
+
|
|
2083
|
+
if (isGitUrl) {
|
|
2084
|
+
const tmpDir = path.join(os.tmpdir(), 'thuban-compare-' + Date.now() + '-' + Math.random().toString(36).slice(2,6));
|
|
2085
|
+
const { execSync } = require('child_process');
|
|
2086
|
+
execSync(`git clone --depth 1 "${targetInput}" "${tmpDir}"`, { stdio: 'pipe', timeout: 60000 });
|
|
2087
|
+
targetPath = tmpDir;
|
|
2088
|
+
clonedRepo = tmpDir;
|
|
2089
|
+
// Extract repo name for label
|
|
2090
|
+
const parts = targetInput.replace(/\.git$/, '').split('/');
|
|
2091
|
+
label = parts[parts.length - 1] || targetInput;
|
|
2092
|
+
} else {
|
|
2093
|
+
label = path.basename(path.resolve(targetInput)) || targetInput;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
const startTime = Date.now();
|
|
2097
|
+
const scanner = new CodeScanner();
|
|
2098
|
+
const graph = new DependencyGraph();
|
|
2099
|
+
const hallDetector = new HallucinationDetector({ rootPath: targetPath });
|
|
2100
|
+
|
|
2101
|
+
const files = collectFiles(targetPath, []);
|
|
2102
|
+
const scanResults = {};
|
|
2103
|
+
let annotated = 0;
|
|
2104
|
+
let driftCount = 0;
|
|
2105
|
+
|
|
2106
|
+
for (const file of files) {
|
|
2107
|
+
try {
|
|
2108
|
+
const result = scanner.scanFile(file);
|
|
2109
|
+
const relPath = path.relative(targetPath, file);
|
|
2110
|
+
scanResults[file] = result;
|
|
2111
|
+
graph.addFile(file, targetPath);
|
|
2112
|
+
if (result.motherCode && result.motherCode.hasDNA) annotated++;
|
|
2113
|
+
if (result.drifts && result.drifts.length > 0) driftCount++;
|
|
2114
|
+
} catch (e) { /* skip */ }
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// Run hallucination detection (same as main scan)
|
|
2118
|
+
const hallResults = hallDetector.scan(files, targetPath);
|
|
2119
|
+
|
|
2120
|
+
const elapsed = Date.now() - startTime;
|
|
2121
|
+
let totalIssues = 0, criticalCount = 0, highCount = 0;
|
|
2122
|
+
const issueByCat = {};
|
|
2123
|
+
|
|
2124
|
+
// Count code scanner issues
|
|
2125
|
+
for (const [, r] of Object.entries(scanResults)) {
|
|
2126
|
+
if (r.issues) {
|
|
2127
|
+
totalIssues += r.issues.length;
|
|
2128
|
+
for (const iss of r.issues) {
|
|
2129
|
+
if (iss.severity === 'critical') criticalCount++;
|
|
2130
|
+
if (iss.severity === 'high') highCount++;
|
|
2131
|
+
const cat = iss.category || iss.type || 'other';
|
|
2132
|
+
issueByCat[cat] = (issueByCat[cat] || 0) + 1;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// Count hallucination issues
|
|
2138
|
+
const hallIssueCount = (hallResults.phantomAPIs || []).length
|
|
2139
|
+
+ (hallResults.phantomImports || []).length
|
|
2140
|
+
+ (hallResults.deprecatedAPIs || []).length;
|
|
2141
|
+
totalIssues += hallIssueCount;
|
|
2142
|
+
criticalCount += (hallResults.phantomAPIs || []).length + (hallResults.phantomImports || []).length;
|
|
2143
|
+
highCount += (hallResults.deprecatedAPIs || []).length;
|
|
2144
|
+
if (hallIssueCount > 0) issueByCat['hallucination'] = hallIssueCount;
|
|
2145
|
+
|
|
2146
|
+
const orphans = graph.getOrphanFiles();
|
|
2147
|
+
const circular = graph.circularDeps || [];
|
|
2148
|
+
const coverage = files.length > 0 ? Math.round((annotated / files.length) * 100) : 0;
|
|
2149
|
+
|
|
2150
|
+
const score = Math.max(0, Math.round(
|
|
2151
|
+
100 - (criticalCount * 15) - (highCount * 5) - (circular.length * 10)
|
|
2152
|
+
- (orphans.length * 1) - ((100 - coverage) * 0.1) - (driftCount * 3)
|
|
2153
|
+
));
|
|
2154
|
+
|
|
2155
|
+
const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
|
|
2156
|
+
|
|
2157
|
+
if (clonedRepo) {
|
|
2158
|
+
try { fs.rmSync(clonedRepo, { recursive: true, force: true }); } catch {}
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
return {
|
|
2162
|
+
label,
|
|
2163
|
+
files: files.length,
|
|
2164
|
+
score,
|
|
2165
|
+
grade,
|
|
2166
|
+
elapsed,
|
|
2167
|
+
issues: { total: totalIssues, critical: criticalCount, high: highCount },
|
|
2168
|
+
categories: issueByCat,
|
|
2169
|
+
dependencies: { circular: circular.length, orphans: orphans.length },
|
|
2170
|
+
motherCode: { annotated, coverage, drifted: driftCount }
|
|
2171
|
+
};
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
// ─── Compare Command ────────────────────────────────────────
|
|
2175
|
+
|
|
2176
|
+
async function cmdCompare(opts) {
|
|
2177
|
+
const args = process.argv.slice(2);
|
|
2178
|
+
// Find the two paths after 'compare'
|
|
2179
|
+
const paths = args.filter(a => a !== 'compare' && !a.startsWith('--'));
|
|
2180
|
+
|
|
2181
|
+
if (paths.length < 2) {
|
|
2182
|
+
console.log('');
|
|
2183
|
+
console.log(c('red', ' Compare requires two paths or URLs'));
|
|
2184
|
+
console.log('');
|
|
2185
|
+
console.log(c('gray', ' Usage:'));
|
|
2186
|
+
console.log(` ${c('cyan', 'npx thuban compare ./project-a ./project-b')}`);
|
|
2187
|
+
console.log(` ${c('cyan', 'npx thuban compare . https://github.com/user/repo')}`);
|
|
2188
|
+
console.log(` ${c('cyan', 'npx thuban compare https://github.com/a/repo https://github.com/b/repo')}`);
|
|
2189
|
+
console.log('');
|
|
2190
|
+
process.exit(1);
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
console.log('');
|
|
2194
|
+
console.log(c('bold', ' ╔══════════════════════════════════════╗'));
|
|
2195
|
+
console.log(c('bold', ' ║ THUBAN — Codebase Comparison ║'));
|
|
2196
|
+
console.log(c('bold', ' ╚══════════════════════════════════════╝'));
|
|
2197
|
+
console.log('');
|
|
2198
|
+
|
|
2199
|
+
console.log(c('cyan', ` Scanning: ${paths[0]}`));
|
|
2200
|
+
const resultA = await runScanSilent(paths[0]);
|
|
2201
|
+
console.log(c('cyan', ` Scanning: ${paths[1]}`));
|
|
2202
|
+
const resultB = await runScanSilent(paths[1]);
|
|
2203
|
+
|
|
2204
|
+
console.log('');
|
|
2205
|
+
|
|
2206
|
+
// ─── Side by side comparison ────────────────────────────
|
|
2207
|
+
const pad = (str, len) => String(str).padEnd(len);
|
|
2208
|
+
const padL = (str, len) => String(str).padStart(len);
|
|
2209
|
+
const colW = 20;
|
|
2210
|
+
const labelA = resultA.label.length > colW ? resultA.label.slice(0, colW - 2) + '..' : resultA.label;
|
|
2211
|
+
const labelB = resultB.label.length > colW ? resultB.label.slice(0, colW - 2) + '..' : resultB.label;
|
|
2212
|
+
|
|
2213
|
+
const scoreColorA = resultA.score >= 80 ? 'green' : resultA.score >= 60 ? 'yellow' : 'red';
|
|
2214
|
+
const scoreColorB = resultB.score >= 80 ? 'green' : resultB.score >= 60 ? 'yellow' : 'red';
|
|
2215
|
+
|
|
2216
|
+
const arrow = (a, b) => {
|
|
2217
|
+
if (a === b) return c('gray', ' =');
|
|
2218
|
+
return a > b ? c('green', ' ↑') : c('red', ' ↓');
|
|
2219
|
+
};
|
|
2220
|
+
|
|
2221
|
+
const better = (a, b, lowerIsBetter) => {
|
|
2222
|
+
if (a === b) return c('gray', ' draw');
|
|
2223
|
+
if (lowerIsBetter) return a < b ? c('green', ' ← winner') : c('red', '');
|
|
2224
|
+
return a > b ? c('green', ' ← winner') : c('red', '');
|
|
2225
|
+
};
|
|
2226
|
+
|
|
2227
|
+
console.log(c('bold', ' ┌──────────────────────┬──────────────────────┬──────────────────────┐'));
|
|
2228
|
+
console.log(c('bold', ` │ ${pad('METRIC', colW)} │ ${pad(labelA, colW)} │ ${pad(labelB, colW)} │`));
|
|
2229
|
+
console.log(c('bold', ' ├──────────────────────┼──────────────────────┼──────────────────────┤'));
|
|
2230
|
+
|
|
2231
|
+
const row = (label, valA, valB) => {
|
|
2232
|
+
console.log(` │ ${c('gray', pad(label, colW))} │ ${pad(valA, colW)} │ ${pad(valB, colW)} │`);
|
|
2233
|
+
};
|
|
2234
|
+
|
|
2235
|
+
row('Health Score', `${c(scoreColorA, resultA.score + '/100')}`, `${c(scoreColorB, resultB.score + '/100')}`);
|
|
2236
|
+
row('Grade', `${c(scoreColorA, resultA.grade)}`, `${c(scoreColorB, resultB.grade)}`);
|
|
2237
|
+
row('Files Scanned', String(resultA.files), String(resultB.files));
|
|
2238
|
+
row('Total Issues', String(resultA.issues.total), String(resultB.issues.total));
|
|
2239
|
+
row('Critical Issues', String(resultA.issues.critical), String(resultB.issues.critical));
|
|
2240
|
+
row('High Issues', String(resultA.issues.high), String(resultB.issues.high));
|
|
2241
|
+
row('Circular Deps', String(resultA.dependencies.circular), String(resultB.dependencies.circular));
|
|
2242
|
+
row('Orphan Files', String(resultA.dependencies.orphans), String(resultB.dependencies.orphans));
|
|
2243
|
+
row('Mother Code DNA', `${resultA.motherCode.coverage}%`, `${resultB.motherCode.coverage}%`);
|
|
2244
|
+
row('Drift Issues', String(resultA.motherCode.drifted), String(resultB.motherCode.drifted));
|
|
2245
|
+
row('Scan Time', `${resultA.elapsed}ms`, `${resultB.elapsed}ms`);
|
|
2246
|
+
|
|
2247
|
+
console.log(c('bold', ' └──────────────────────┴──────────────────────┴──────────────────────┘'));
|
|
2248
|
+
console.log('');
|
|
2249
|
+
|
|
2250
|
+
// ─── Verdict ────────────────────────────────────────────
|
|
2251
|
+
const diff = resultA.score - resultB.score;
|
|
2252
|
+
if (diff === 0) {
|
|
2253
|
+
console.log(c('cyan', ` VERDICT: Both codebases score identically (${resultA.score}/100)`));
|
|
2254
|
+
} else {
|
|
2255
|
+
const winner = diff > 0 ? resultA : resultB;
|
|
2256
|
+
const loser = diff > 0 ? resultB : resultA;
|
|
2257
|
+
console.log(c('cyan', ` VERDICT: ${winner.label} is healthier`));
|
|
2258
|
+
console.log(c('gray', ` ${winner.label} scores ${winner.score}/100 (${winner.grade}) vs ${loser.label} at ${loser.score}/100 (${loser.grade})`));
|
|
2259
|
+
console.log(c('gray', ` ${Math.abs(diff)} point difference — ${Math.abs(diff) > 20 ? 'significant gap' : Math.abs(diff) > 10 ? 'moderate gap' : 'close race'}`));
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
// ─── Issue breakdown comparison ─────────────────────────
|
|
2263
|
+
const allCats = new Set([...Object.keys(resultA.categories), ...Object.keys(resultB.categories)]);
|
|
2264
|
+
if (allCats.size > 0) {
|
|
2265
|
+
console.log('');
|
|
2266
|
+
console.log(c('bold', ' ISSUE BREAKDOWN'));
|
|
2267
|
+
for (const cat of allCats) {
|
|
2268
|
+
const countA = resultA.categories[cat] || 0;
|
|
2269
|
+
const countB = resultB.categories[cat] || 0;
|
|
2270
|
+
const catLabel = cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
2271
|
+
console.log(c('gray', ` ${pad(catLabel, 22)} ${padL(countA, 5)} vs ${padL(countB, 5)} ${countA < countB ? c('green', `← ${labelA}`) : countA > countB ? c('green', `→ ${labelB}`) : c('gray', 'tied')}`));
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
console.log('');
|
|
2276
|
+
console.log(c('gray', ` Full scan: npx thuban scan ${paths[0]}`));
|
|
2277
|
+
console.log(c('gray', ` Full scan: npx thuban scan ${paths[1]}`));
|
|
2278
|
+
console.log('');
|
|
2279
|
+
}
|
|
2280
|
+
|
|
1951
2281
|
async function main() {
|
|
1952
2282
|
const args = process.argv.slice(2);
|
|
1953
2283
|
const opts = parseArgs(args);
|
|
@@ -1964,9 +2294,9 @@ async function main() {
|
|
|
1964
2294
|
}
|
|
1965
2295
|
|
|
1966
2296
|
// Verify target path exists
|
|
1967
|
-
const noPathCommands2 = ['activate', 'status', 'upgrade'];
|
|
2297
|
+
const noPathCommands2 = ['activate', 'status', 'upgrade', 'telemetry', 'compare'];
|
|
1968
2298
|
|
|
1969
|
-
if (!noPathCommands2.includes(opts.command) && !fs.existsSync(opts.targetPath)) {
|
|
2299
|
+
if (!noPathCommands2.includes(opts.command) && !opts.targetPath.match(/^(https?:\/\/|git@)/) && !fs.existsSync(opts.targetPath)) {
|
|
1970
2300
|
console.error(c('red', ` Error: Path not found: ${opts.targetPath}`));
|
|
1971
2301
|
process.exit(1);
|
|
1972
2302
|
}
|
|
@@ -2049,12 +2379,18 @@ async function main() {
|
|
|
2049
2379
|
case 'baseline':
|
|
2050
2380
|
await cmdBaseline(opts);
|
|
2051
2381
|
break;
|
|
2382
|
+
case 'telemetry':
|
|
2383
|
+
await cmdTelemetry(opts);
|
|
2384
|
+
break;
|
|
2385
|
+
case 'compare':
|
|
2386
|
+
await cmdCompare(opts);
|
|
2387
|
+
break;
|
|
2052
2388
|
default:
|
|
2053
2389
|
console.error('');
|
|
2054
2390
|
console.error(c('red', ` Unknown command: ${opts.command}`));
|
|
2055
2391
|
console.error('');
|
|
2056
2392
|
// Suggest closest match
|
|
2057
|
-
const allCmds = ['scan','deps','drift','inject','health','report','debt','fix','hallucinate','watch','dashboard','diff','gate','cost','ghosts','ai-score','clones','passport','executive','baseline','activate','status','upgrade'];
|
|
2393
|
+
const allCmds = ['scan','compare','deps','drift','inject','health','report','debt','fix','hallucinate','watch','dashboard','diff','gate','cost','ghosts','ai-score','clones','passport','executive','baseline','activate','status','upgrade','telemetry'];
|
|
2058
2394
|
const suggestions = allCmds.filter(cmd => cmd.startsWith(opts.command.slice(0,2)) || cmd.includes(opts.command));
|
|
2059
2395
|
if (suggestions.length > 0) {
|
|
2060
2396
|
console.error(c('gray', ` Did you mean: `) + c('cyan', suggestions.join(', ')) + c('gray', '?'));
|
package/package.json
CHANGED