scip-query 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/IMPROVEMENTS.md +143 -0
- package/PLAN.md +320 -0
- package/README.md +1213 -0
- package/dist/chunk-2QZ23IBN.js +55 -0
- package/dist/chunk-2QZ23IBN.js.map +1 -0
- package/dist/chunk-36OMT7ZJ.js +144 -0
- package/dist/chunk-36OMT7ZJ.js.map +1 -0
- package/dist/chunk-3E2X7RIE.js +101 -0
- package/dist/chunk-3E2X7RIE.js.map +1 -0
- package/dist/chunk-3UOUTZQT.js +45 -0
- package/dist/chunk-3UOUTZQT.js.map +1 -0
- package/dist/chunk-3ZZJVBIO.js +88 -0
- package/dist/chunk-3ZZJVBIO.js.map +1 -0
- package/dist/chunk-4TYLS5XX.js +10 -0
- package/dist/chunk-4TYLS5XX.js.map +1 -0
- package/dist/chunk-5FGUEU7N.js +101 -0
- package/dist/chunk-5FGUEU7N.js.map +1 -0
- package/dist/chunk-5WTJAXY2.js +61 -0
- package/dist/chunk-5WTJAXY2.js.map +1 -0
- package/dist/chunk-6NBLIDF4.js +24 -0
- package/dist/chunk-6NBLIDF4.js.map +1 -0
- package/dist/chunk-6SXADWLW.js +43 -0
- package/dist/chunk-6SXADWLW.js.map +1 -0
- package/dist/chunk-6VJ6Q7IE.js +65 -0
- package/dist/chunk-6VJ6Q7IE.js.map +1 -0
- package/dist/chunk-7OZPA5OO.js +258 -0
- package/dist/chunk-7OZPA5OO.js.map +1 -0
- package/dist/chunk-BEPIEVLR.js +76 -0
- package/dist/chunk-BEPIEVLR.js.map +1 -0
- package/dist/chunk-BFSCMC22.js +42 -0
- package/dist/chunk-BFSCMC22.js.map +1 -0
- package/dist/chunk-BP2ATLK2.js +110 -0
- package/dist/chunk-BP2ATLK2.js.map +1 -0
- package/dist/chunk-CM454WL3.js +114 -0
- package/dist/chunk-CM454WL3.js.map +1 -0
- package/dist/chunk-DCKMSTJ4.js +74 -0
- package/dist/chunk-DCKMSTJ4.js.map +1 -0
- package/dist/chunk-DEZKCZXD.js +40 -0
- package/dist/chunk-DEZKCZXD.js.map +1 -0
- package/dist/chunk-DVWGWHFW.js +99 -0
- package/dist/chunk-DVWGWHFW.js.map +1 -0
- package/dist/chunk-EMDQWNYR.js +102 -0
- package/dist/chunk-EMDQWNYR.js.map +1 -0
- package/dist/chunk-FFSWWE5O.js +33 -0
- package/dist/chunk-FFSWWE5O.js.map +1 -0
- package/dist/chunk-FGXRVW7G.js +73 -0
- package/dist/chunk-FGXRVW7G.js.map +1 -0
- package/dist/chunk-FUHJCHS4.js +158 -0
- package/dist/chunk-FUHJCHS4.js.map +1 -0
- package/dist/chunk-GJFURBEW.js +64 -0
- package/dist/chunk-GJFURBEW.js.map +1 -0
- package/dist/chunk-GTILYBH6.js +102 -0
- package/dist/chunk-GTILYBH6.js.map +1 -0
- package/dist/chunk-JJP7KQND.js +1 -0
- package/dist/chunk-JJP7KQND.js.map +1 -0
- package/dist/chunk-JKP5GH6T.js +213 -0
- package/dist/chunk-JKP5GH6T.js.map +1 -0
- package/dist/chunk-KCBMVQL5.js +38 -0
- package/dist/chunk-KCBMVQL5.js.map +1 -0
- package/dist/chunk-KVSW5KYP.js +78 -0
- package/dist/chunk-KVSW5KYP.js.map +1 -0
- package/dist/chunk-LAWMH22O.js +172 -0
- package/dist/chunk-LAWMH22O.js.map +1 -0
- package/dist/chunk-LB7OS35Q.js +72 -0
- package/dist/chunk-LB7OS35Q.js.map +1 -0
- package/dist/chunk-LUSIFBXO.js +57 -0
- package/dist/chunk-LUSIFBXO.js.map +1 -0
- package/dist/chunk-MBVNHJVN.js +44 -0
- package/dist/chunk-MBVNHJVN.js.map +1 -0
- package/dist/chunk-MGNMHKX3.js +15 -0
- package/dist/chunk-MGNMHKX3.js.map +1 -0
- package/dist/chunk-N5KEREIA.js +41 -0
- package/dist/chunk-N5KEREIA.js.map +1 -0
- package/dist/chunk-NDSQYIWT.js +71 -0
- package/dist/chunk-NDSQYIWT.js.map +1 -0
- package/dist/chunk-NUZ4OMU3.js +28 -0
- package/dist/chunk-NUZ4OMU3.js.map +1 -0
- package/dist/chunk-QOV2R2WT.js +170 -0
- package/dist/chunk-QOV2R2WT.js.map +1 -0
- package/dist/chunk-SEFSL2GF.js +78 -0
- package/dist/chunk-SEFSL2GF.js.map +1 -0
- package/dist/chunk-T6ARFSBZ.js +103 -0
- package/dist/chunk-T6ARFSBZ.js.map +1 -0
- package/dist/chunk-TBP6BICL.js +46 -0
- package/dist/chunk-TBP6BICL.js.map +1 -0
- package/dist/chunk-TDNNOR6D.js +97 -0
- package/dist/chunk-TDNNOR6D.js.map +1 -0
- package/dist/chunk-TSPZOMHC.js +195 -0
- package/dist/chunk-TSPZOMHC.js.map +1 -0
- package/dist/chunk-UNTPVD36.js +55 -0
- package/dist/chunk-UNTPVD36.js.map +1 -0
- package/dist/chunk-VRUJH4BO.js +88 -0
- package/dist/chunk-VRUJH4BO.js.map +1 -0
- package/dist/chunk-VZ7AMAFL.js +76 -0
- package/dist/chunk-VZ7AMAFL.js.map +1 -0
- package/dist/chunk-XFXDXEUN.js +24 -0
- package/dist/chunk-XFXDXEUN.js.map +1 -0
- package/dist/chunk-YZAA4LYG.js +169 -0
- package/dist/chunk-YZAA4LYG.js.map +1 -0
- package/dist/chunk-Z73NYSBZ.js +92 -0
- package/dist/chunk-Z73NYSBZ.js.map +1 -0
- package/dist/chunk-ZJRYBOEE.js +125 -0
- package/dist/chunk-ZJRYBOEE.js.map +1 -0
- package/dist/cli.js +5798 -0
- package/dist/cli.js.map +1 -0
- package/dist/db-BxaevAyc.d.ts +683 -0
- package/dist/index.d.ts +254 -0
- package/dist/index.js +1271 -0
- package/dist/index.js.map +1 -0
- package/dist/postinstall.js +167 -0
- package/dist/postinstall.js.map +1 -0
- package/dist/queries/affected.d.ts +14 -0
- package/dist/queries/affected.js +9 -0
- package/dist/queries/affected.js.map +1 -0
- package/dist/queries/bottlenecks.d.ts +18 -0
- package/dist/queries/bottlenecks.js +8 -0
- package/dist/queries/bottlenecks.js.map +1 -0
- package/dist/queries/by-kind.d.ts +20 -0
- package/dist/queries/by-kind.js +10 -0
- package/dist/queries/by-kind.js.map +1 -0
- package/dist/queries/call-graph.d.ts +13 -0
- package/dist/queries/call-graph.js +9 -0
- package/dist/queries/call-graph.js.map +1 -0
- package/dist/queries/change-surface.d.ts +10 -0
- package/dist/queries/change-surface.js +9 -0
- package/dist/queries/change-surface.js.map +1 -0
- package/dist/queries/clean-signature.d.ts +9 -0
- package/dist/queries/clean-signature.js +7 -0
- package/dist/queries/clean-signature.js.map +1 -0
- package/dist/queries/code.d.ts +17 -0
- package/dist/queries/code.js +9 -0
- package/dist/queries/code.js.map +1 -0
- package/dist/queries/complexity-hotspots.d.ts +19 -0
- package/dist/queries/complexity-hotspots.js +9 -0
- package/dist/queries/complexity-hotspots.js.map +1 -0
- package/dist/queries/complexity.d.ts +13 -0
- package/dist/queries/complexity.js +9 -0
- package/dist/queries/complexity.js.map +1 -0
- package/dist/queries/convergence.d.ts +11 -0
- package/dist/queries/convergence.js +9 -0
- package/dist/queries/convergence.js.map +1 -0
- package/dist/queries/coupling.d.ts +17 -0
- package/dist/queries/coupling.js +9 -0
- package/dist/queries/coupling.js.map +1 -0
- package/dist/queries/cycles.d.ts +16 -0
- package/dist/queries/cycles.js +8 -0
- package/dist/queries/cycles.js.map +1 -0
- package/dist/queries/dataflow.d.ts +19 -0
- package/dist/queries/dataflow.js +9 -0
- package/dist/queries/dataflow.js.map +1 -0
- package/dist/queries/dead.d.ts +10 -0
- package/dist/queries/dead.js +9 -0
- package/dist/queries/dead.js.map +1 -0
- package/dist/queries/deep-chains.d.ts +16 -0
- package/dist/queries/deep-chains.js +8 -0
- package/dist/queries/deep-chains.js.map +1 -0
- package/dist/queries/deps.d.ts +9 -0
- package/dist/queries/deps.js +9 -0
- package/dist/queries/deps.js.map +1 -0
- package/dist/queries/diff-impact.d.ts +13 -0
- package/dist/queries/diff-impact.js +9 -0
- package/dist/queries/diff-impact.js.map +1 -0
- package/dist/queries/doc-coverage.d.ts +14 -0
- package/dist/queries/doc-coverage.js +8 -0
- package/dist/queries/doc-coverage.js.map +1 -0
- package/dist/queries/drift.d.ts +25 -0
- package/dist/queries/drift.js +8 -0
- package/dist/queries/drift.js.map +1 -0
- package/dist/queries/extract-candidates.d.ts +25 -0
- package/dist/queries/extract-candidates.js +9 -0
- package/dist/queries/extract-candidates.js.map +1 -0
- package/dist/queries/fan.d.ts +29 -0
- package/dist/queries/fan.js +14 -0
- package/dist/queries/fan.js.map +1 -0
- package/dist/queries/files.d.ts +6 -0
- package/dist/queries/files.js +7 -0
- package/dist/queries/files.js.map +1 -0
- package/dist/queries/health.d.ts +18 -0
- package/dist/queries/health.js +21 -0
- package/dist/queries/health.js.map +1 -0
- package/dist/queries/hierarchy.d.ts +13 -0
- package/dist/queries/hierarchy.js +8 -0
- package/dist/queries/hierarchy.js.map +1 -0
- package/dist/queries/hotspots.d.ts +13 -0
- package/dist/queries/hotspots.js +8 -0
- package/dist/queries/hotspots.js.map +1 -0
- package/dist/queries/imports.d.ts +19 -0
- package/dist/queries/imports.js +12 -0
- package/dist/queries/imports.js.map +1 -0
- package/dist/queries/index.d.ts +47 -0
- package/dist/queries/index.js +207 -0
- package/dist/queries/index.js.map +1 -0
- package/dist/queries/isolated.d.ts +14 -0
- package/dist/queries/isolated.js +9 -0
- package/dist/queries/isolated.js.map +1 -0
- package/dist/queries/members.d.ts +10 -0
- package/dist/queries/members.js +8 -0
- package/dist/queries/members.js.map +1 -0
- package/dist/queries/methods.d.ts +6 -0
- package/dist/queries/methods.js +8 -0
- package/dist/queries/methods.js.map +1 -0
- package/dist/queries/outline.d.ts +10 -0
- package/dist/queries/outline.js +8 -0
- package/dist/queries/outline.js.map +1 -0
- package/dist/queries/passthrough-candidates.d.ts +18 -0
- package/dist/queries/passthrough-candidates.js +9 -0
- package/dist/queries/passthrough-candidates.js.map +1 -0
- package/dist/queries/redundant-reexports.d.ts +22 -0
- package/dist/queries/redundant-reexports.js +8 -0
- package/dist/queries/redundant-reexports.js.map +1 -0
- package/dist/queries/refs.d.ts +6 -0
- package/dist/queries/refs.js +7 -0
- package/dist/queries/refs.js.map +1 -0
- package/dist/queries/similar-chains.d.ts +29 -0
- package/dist/queries/similar-chains.js +8 -0
- package/dist/queries/similar-chains.js.map +1 -0
- package/dist/queries/similar-files.d.ts +19 -0
- package/dist/queries/similar-files.js +8 -0
- package/dist/queries/similar-files.js.map +1 -0
- package/dist/queries/similar-signatures.d.ts +21 -0
- package/dist/queries/similar-signatures.js +8 -0
- package/dist/queries/similar-signatures.js.map +1 -0
- package/dist/queries/similar.d.ts +34 -0
- package/dist/queries/similar.js +11 -0
- package/dist/queries/similar.js.map +1 -0
- package/dist/queries/slice.d.ts +21 -0
- package/dist/queries/slice.js +9 -0
- package/dist/queries/slice.js.map +1 -0
- package/dist/queries/stale-abstractions.d.ts +18 -0
- package/dist/queries/stale-abstractions.js +9 -0
- package/dist/queries/stale-abstractions.js.map +1 -0
- package/dist/queries/stats.d.ts +6 -0
- package/dist/queries/stats.js +7 -0
- package/dist/queries/stats.js.map +1 -0
- package/dist/queries/surface.d.ts +7 -0
- package/dist/queries/surface.js +8 -0
- package/dist/queries/surface.js.map +1 -0
- package/dist/queries/symbols.d.ts +6 -0
- package/dist/queries/symbols.js +9 -0
- package/dist/queries/symbols.js.map +1 -0
- package/dist/queries/system.d.ts +7 -0
- package/dist/queries/system.js +9 -0
- package/dist/queries/system.js.map +1 -0
- package/dist/queries/test-coverage.d.ts +22 -0
- package/dist/queries/test-coverage.js +11 -0
- package/dist/queries/test-coverage.js.map +1 -0
- package/dist/queries/trace.d.ts +6 -0
- package/dist/queries/trace.js +8 -0
- package/dist/queries/trace.js.map +1 -0
- package/dist/queries/wrapper-candidates.d.ts +17 -0
- package/dist/queries/wrapper-candidates.js +9 -0
- package/dist/queries/wrapper-candidates.js.map +1 -0
- package/dist/reindex-worker.js +368 -0
- package/dist/reindex-worker.js.map +1 -0
- package/docs/AGENT_GUIDE.md +359 -0
- package/package.json +70 -0
- package/reports/debloat/2026-04-10-scip-query-self-audit.md +161 -0
- package/skills/concrete-plan/SKILL.md +318 -0
- package/skills/scip-debloat/SKILL.md +413 -0
- package/skills/scip-explore/SKILL.md +235 -0
- package/skills/scip-verify/SKILL.md +323 -0
- package/src/cli.ts +1480 -0
- package/src/config.ts +117 -0
- package/src/db.ts +127 -0
- package/src/gitignore-filter.ts +143 -0
- package/src/index.ts +11 -0
- package/src/postinstall.ts +8 -0
- package/src/queries/affected.ts +86 -0
- package/src/queries/bottlenecks.ts +67 -0
- package/src/queries/by-kind.ts +204 -0
- package/src/queries/call-graph.ts +66 -0
- package/src/queries/change-surface.ts +110 -0
- package/src/queries/clean-signature.ts +22 -0
- package/src/queries/code.ts +101 -0
- package/src/queries/complexity-hotspots.ts +119 -0
- package/src/queries/complexity.ts +152 -0
- package/src/queries/convergence.ts +82 -0
- package/src/queries/coupling.ts +99 -0
- package/src/queries/cycles.ts +78 -0
- package/src/queries/dataflow.ts +128 -0
- package/src/queries/dead.ts +122 -0
- package/src/queries/deep-chains.ts +59 -0
- package/src/queries/deps.ts +46 -0
- package/src/queries/diff-impact.ts +204 -0
- package/src/queries/doc-coverage.ts +86 -0
- package/src/queries/drift.ts +224 -0
- package/src/queries/extract-candidates.ts +167 -0
- package/src/queries/fan.ts +148 -0
- package/src/queries/files.ts +16 -0
- package/src/queries/health.ts +324 -0
- package/src/queries/hierarchy.ts +49 -0
- package/src/queries/hotspots.ts +53 -0
- package/src/queries/imports.ts +95 -0
- package/src/queries/index.ts +45 -0
- package/src/queries/isolated.ts +67 -0
- package/src/queries/members.ts +54 -0
- package/src/queries/methods.ts +27 -0
- package/src/queries/outline.ts +52 -0
- package/src/queries/passthrough-candidates.ts +94 -0
- package/src/queries/redundant-reexports.ts +170 -0
- package/src/queries/refs.ts +27 -0
- package/src/queries/similar-chains.ts +314 -0
- package/src/queries/similar-files.ts +140 -0
- package/src/queries/similar-signatures.ts +151 -0
- package/src/queries/similar.ts +305 -0
- package/src/queries/slice.ts +154 -0
- package/src/queries/stale-abstractions.ts +82 -0
- package/src/queries/stats.ts +22 -0
- package/src/queries/surface.ts +34 -0
- package/src/queries/symbols.ts +39 -0
- package/src/queries/system.ts +86 -0
- package/src/queries/test-coverage.ts +106 -0
- package/src/queries/trace.ts +55 -0
- package/src/queries/wrapper-candidates.ts +112 -0
- package/src/query-support.ts +226 -0
- package/src/reindex/detect.ts +58 -0
- package/src/reindex/index.ts +153 -0
- package/src/reindex/indexers.ts +220 -0
- package/src/reindex/install.ts +125 -0
- package/src/reindex-worker.ts +35 -0
- package/src/setup.ts +202 -0
- package/src/symbol-parser.ts +278 -0
- package/src/types.ts +654 -0
- package/src/watch.ts +274 -0
- package/tests/gitignore-filter.test.ts +48 -0
- package/tests/queries.test.ts +300 -0
- package/tests/symbol-parser.test.ts +157 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +40 -0
- package/vitest.config.ts +7 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,1480 @@
|
|
|
1
|
+
import { program } from 'commander';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { ScipDatabase } from './db.js';
|
|
5
|
+
import { createGitignoreFilter } from './gitignore-filter.js';
|
|
6
|
+
import { loadProjectConfig, resolveIndexPaths, initProjectConfig } from './config.js';
|
|
7
|
+
import { reindex, detectLanguages } from './reindex/index.js';
|
|
8
|
+
import { Watcher } from './watch.js';
|
|
9
|
+
import * as queries from './queries/index.js';
|
|
10
|
+
import type { ScipQueryConfig, DeadOptions, WatcherStatus } from './types.js';
|
|
11
|
+
import { installSkills, isScipInstalled, printScipInstallInstructions } from './setup.js';
|
|
12
|
+
|
|
13
|
+
// ── Helpers ────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
function resolveProjectRoot(): string {
|
|
16
|
+
return process.env['SCIP_QUERY_PROJECT_ROOT'] ?? process.cwd();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function openDb(): ScipDatabase {
|
|
20
|
+
const projectRoot = resolveProjectRoot();
|
|
21
|
+
const config = loadProjectConfig(projectRoot);
|
|
22
|
+
const paths = resolveIndexPaths(projectRoot, config);
|
|
23
|
+
|
|
24
|
+
// Also check legacy location (project root) for backwards compat
|
|
25
|
+
const dbPath = process.env['SCIP_QUERY_INDEX_DB']
|
|
26
|
+
?? (existsSync(paths.dbPath) ? paths.dbPath : join(projectRoot, 'index.db'));
|
|
27
|
+
|
|
28
|
+
if (!existsSync(dbPath)) {
|
|
29
|
+
console.error(`error: No index.db found. Run: scip-query reindex`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const dbConfig: ScipQueryConfig = {
|
|
34
|
+
dbPath,
|
|
35
|
+
indexPath: process.env['SCIP_QUERY_INDEX_SCIP'] ?? paths.indexPath,
|
|
36
|
+
projectRoot,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const filter = createGitignoreFilter(projectRoot);
|
|
40
|
+
return new ScipDatabase(dbConfig, filter);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function withDb(run: (db: ScipDatabase) => void): void {
|
|
44
|
+
const db = openDb();
|
|
45
|
+
try {
|
|
46
|
+
run(db);
|
|
47
|
+
} finally {
|
|
48
|
+
db.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function runQuery<T>(
|
|
53
|
+
query: (db: ScipDatabase) => T,
|
|
54
|
+
render: (result: T) => void,
|
|
55
|
+
): void {
|
|
56
|
+
withDb((db) => {
|
|
57
|
+
render(query(db));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── CLI Definition ─────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
program
|
|
64
|
+
.name('scip-query')
|
|
65
|
+
.description('Language-agnostic code intelligence CLI powered by SCIP indexes')
|
|
66
|
+
.version('0.1.0');
|
|
67
|
+
|
|
68
|
+
// reindex
|
|
69
|
+
program
|
|
70
|
+
.command('reindex')
|
|
71
|
+
.description('Index the codebase and convert to SQLite')
|
|
72
|
+
.option('-l, --language <lang>', 'Index only this language (can be repeated)', collect, [])
|
|
73
|
+
.option('--pnpm-workspaces', 'Enable pnpm workspace support (TypeScript)')
|
|
74
|
+
.action(async (opts) => {
|
|
75
|
+
const projectRoot = resolveProjectRoot();
|
|
76
|
+
try {
|
|
77
|
+
const result = await reindex({
|
|
78
|
+
projectRoot,
|
|
79
|
+
languages: opts.language.length > 0 ? opts.language : undefined,
|
|
80
|
+
pnpmWorkspaces: opts.pnpmWorkspaces,
|
|
81
|
+
});
|
|
82
|
+
console.log(`Indexed ${result.languages.join(', ')} in ${(result.durationMs / 1000).toFixed(1)}s`);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.error(`error: ${err instanceof Error ? err.message : err}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// stats
|
|
90
|
+
program
|
|
91
|
+
.command('stats')
|
|
92
|
+
.description('Show index statistics')
|
|
93
|
+
.action(() => {
|
|
94
|
+
runQuery(
|
|
95
|
+
(db) => queries.stats(db),
|
|
96
|
+
(s) => {
|
|
97
|
+
console.log(`Documents: ${s.documents}`);
|
|
98
|
+
console.log(`Symbols: ${s.symbols}`);
|
|
99
|
+
console.log(`Definitions: ${s.definitions}`);
|
|
100
|
+
console.log(`References: ${s.references}`);
|
|
101
|
+
console.log(`Index size: ${formatBytes(s.indexSizeBytes)}`);
|
|
102
|
+
if (s.lastBuilt) {
|
|
103
|
+
console.log(`Last built: ${s.lastBuilt.toISOString().replace('T', ' ').slice(0, 19)}`);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// files
|
|
110
|
+
program
|
|
111
|
+
.command('files <pattern>')
|
|
112
|
+
.description('Find files matching a pattern')
|
|
113
|
+
.action((pattern) => {
|
|
114
|
+
runQuery(
|
|
115
|
+
(db) => queries.files(db, pattern),
|
|
116
|
+
(results) => {
|
|
117
|
+
for (const r of results) console.log(r.relativePath);
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// symbols
|
|
123
|
+
program
|
|
124
|
+
.command('symbols <file>')
|
|
125
|
+
.description('List symbols defined in a file (with line ranges + signatures)')
|
|
126
|
+
.action((file) => {
|
|
127
|
+
runQuery(
|
|
128
|
+
(db) => queries.symbols(db, file),
|
|
129
|
+
(results) => {
|
|
130
|
+
for (const r of results) {
|
|
131
|
+
const sig = r.signature ? ` — ${r.signature}` : '';
|
|
132
|
+
console.log(` ${r.startLine}-${r.endLine} ${r.shortName}${sig}`);
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// methods
|
|
139
|
+
program
|
|
140
|
+
.command('methods <className>')
|
|
141
|
+
.description('List methods of a class (with line ranges)')
|
|
142
|
+
.action((className) => {
|
|
143
|
+
runQuery(
|
|
144
|
+
(db) => queries.methods(db, className),
|
|
145
|
+
(results) => {
|
|
146
|
+
for (const r of results) {
|
|
147
|
+
console.log(` ${r.startLine}-${r.endLine} ${r.name}`);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// refs
|
|
154
|
+
program
|
|
155
|
+
.command('refs <symbol>')
|
|
156
|
+
.description('Find all files referencing a symbol')
|
|
157
|
+
.action((symbol) => {
|
|
158
|
+
runQuery(
|
|
159
|
+
(db) => queries.refs(db, symbol),
|
|
160
|
+
(results) => {
|
|
161
|
+
let prevFile = '';
|
|
162
|
+
for (const r of results) {
|
|
163
|
+
if (r.relativePath !== prevFile) {
|
|
164
|
+
if (prevFile) console.log('');
|
|
165
|
+
console.log(r.relativePath);
|
|
166
|
+
prevFile = r.relativePath;
|
|
167
|
+
}
|
|
168
|
+
console.log(` line ${r.line}`);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// trace
|
|
175
|
+
program
|
|
176
|
+
.command('trace <symbol>')
|
|
177
|
+
.description('Trace a symbol: definition + all references')
|
|
178
|
+
.action((symbol) => {
|
|
179
|
+
runQuery(
|
|
180
|
+
(db) => queries.trace(db, symbol),
|
|
181
|
+
(result) => {
|
|
182
|
+
console.log('═══ DEFINITION ═══');
|
|
183
|
+
for (const d of result.definitions) {
|
|
184
|
+
const sig = d.signature ? ` — ${d.signature}` : '';
|
|
185
|
+
console.log(` ${d.relativePath}:${d.startLine}-${d.endLine}${sig}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log('\n═══ REFERENCED BY ═══');
|
|
189
|
+
for (const ref of result.referencedBy) {
|
|
190
|
+
console.log(` ${ref}`);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// deps
|
|
197
|
+
program
|
|
198
|
+
.command('deps <file>')
|
|
199
|
+
.description('Files this file depends on (internal)')
|
|
200
|
+
.action((file) => {
|
|
201
|
+
runQuery(
|
|
202
|
+
(db) => queries.deps(db, file),
|
|
203
|
+
(results) => {
|
|
204
|
+
for (const r of results) console.log(r.relativePath);
|
|
205
|
+
},
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// rdeps
|
|
210
|
+
program
|
|
211
|
+
.command('rdeps <file>')
|
|
212
|
+
.description('Files that depend on this file/module')
|
|
213
|
+
.action((file) => {
|
|
214
|
+
runQuery(
|
|
215
|
+
(db) => queries.rdeps(db, file),
|
|
216
|
+
(results) => {
|
|
217
|
+
for (const r of results) console.log(r.relativePath);
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// system
|
|
223
|
+
program
|
|
224
|
+
.command('system <module>')
|
|
225
|
+
.description('Full module map: files, symbols, deps in/out')
|
|
226
|
+
.action((module) => {
|
|
227
|
+
runQuery(
|
|
228
|
+
(db) => queries.system(db, module),
|
|
229
|
+
(result) => {
|
|
230
|
+
console.log('═══ FILES ═══');
|
|
231
|
+
for (const f of result.files) console.log(f);
|
|
232
|
+
|
|
233
|
+
console.log('\n═══ EXPORTED SYMBOLS ═══');
|
|
234
|
+
for (const s of result.symbols) {
|
|
235
|
+
console.log(` ${s.startLine}-${s.endLine} ${s.shortName}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log('\n═══ DEPENDS ON (internal) ═══');
|
|
239
|
+
for (const d of result.dependsOn) console.log(` ${d}`);
|
|
240
|
+
|
|
241
|
+
console.log('\n═══ DEPENDED ON BY ═══');
|
|
242
|
+
for (const d of result.dependedOnBy) console.log(` ${d}`);
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// surface
|
|
248
|
+
program
|
|
249
|
+
.command('surface <module>')
|
|
250
|
+
.description('What symbols consumers actually use from this module')
|
|
251
|
+
.action((module) => {
|
|
252
|
+
runQuery(
|
|
253
|
+
(db) => queries.surface(db, module),
|
|
254
|
+
(results) => {
|
|
255
|
+
for (const r of results) {
|
|
256
|
+
console.log(` ${r.consumer} → ${r.shortName}`);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// dead
|
|
263
|
+
program
|
|
264
|
+
.command('dead [scope]')
|
|
265
|
+
.description('Find dead code and file-internal symbols (no cross-file consumers)')
|
|
266
|
+
.option('--min-loc <n>', 'Only show symbols >= N lines', parseIntSafe, 1)
|
|
267
|
+
.option('--include-tests', 'Include test files')
|
|
268
|
+
.option('--skip-barrels', 'Ignore refs from barrel re-export files')
|
|
269
|
+
.option('--include-members', 'Include class members')
|
|
270
|
+
.action((scope, opts) => {
|
|
271
|
+
withDb((db) => {
|
|
272
|
+
const deadOpts: DeadOptions = {
|
|
273
|
+
scope: scope || undefined,
|
|
274
|
+
minLoc: opts.minLoc,
|
|
275
|
+
includeTests: opts.includeTests,
|
|
276
|
+
skipBarrels: opts.skipBarrels,
|
|
277
|
+
includeMembers: opts.includeMembers,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const result = queries.dead(db, deadOpts);
|
|
281
|
+
|
|
282
|
+
if (result.symbols.length === 0) {
|
|
283
|
+
console.log('No dead code found.');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let prevFile = '';
|
|
288
|
+
for (const s of result.symbols) {
|
|
289
|
+
if (s.relativePath !== prevFile) {
|
|
290
|
+
if (prevFile) console.log('');
|
|
291
|
+
console.log(s.relativePath);
|
|
292
|
+
prevFile = s.relativePath;
|
|
293
|
+
}
|
|
294
|
+
const tag = s.kind === 'dead-code' ? '[dead code]' : '[file-internal only]';
|
|
295
|
+
console.log(` ${s.startLine}-${s.endLine} (${s.loc} LOC) ${s.shortName} ${tag}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log('\n───────────────────────────');
|
|
299
|
+
console.log(
|
|
300
|
+
`Total: ${result.totalCount} symbols (${result.deadCodeCount} dead code, ` +
|
|
301
|
+
`${result.fileInternalCount} file-internal), ${result.totalLoc} LOC`,
|
|
302
|
+
);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// hotspots
|
|
307
|
+
program
|
|
308
|
+
.command('hotspots')
|
|
309
|
+
.description('Most-referenced symbols in the codebase (choke points)')
|
|
310
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 30)
|
|
311
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
312
|
+
.action((opts) => {
|
|
313
|
+
runQuery(
|
|
314
|
+
(db) => queries.hotspots(db, { limit: opts.limit, scope: opts.scope }),
|
|
315
|
+
(results) => {
|
|
316
|
+
console.log(' refs files symbol');
|
|
317
|
+
console.log(' ──── ───── ──────');
|
|
318
|
+
for (const r of results) {
|
|
319
|
+
console.log(` ${String(r.refCount).padStart(4)} ${String(r.fileCount).padStart(5)} ${r.shortName}`);
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// imports
|
|
326
|
+
program
|
|
327
|
+
.command('imports <file>')
|
|
328
|
+
.description('What symbols does this file import?')
|
|
329
|
+
.action((file) => {
|
|
330
|
+
runQuery(
|
|
331
|
+
(db) => queries.imports(db, file),
|
|
332
|
+
(results) => {
|
|
333
|
+
if (results.length === 0) {
|
|
334
|
+
console.log('No imports found (indexer may not emit role=2 for this language).');
|
|
335
|
+
}
|
|
336
|
+
for (const r of results) {
|
|
337
|
+
console.log(` ${r.shortName} ← ${r.fromFile}`);
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// imported-by
|
|
344
|
+
program
|
|
345
|
+
.command('imported-by <symbol>')
|
|
346
|
+
.description('Which files import this symbol?')
|
|
347
|
+
.action((symbol) => {
|
|
348
|
+
runQuery(
|
|
349
|
+
(db) => queries.importedBy(db, symbol),
|
|
350
|
+
(results) => {
|
|
351
|
+
for (const r of results) {
|
|
352
|
+
console.log(` ${r.fromFile}`);
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// unused-imports
|
|
359
|
+
program
|
|
360
|
+
.command('unused-imports <file>')
|
|
361
|
+
.description('Find imports not referenced in the same file')
|
|
362
|
+
.action((file) => {
|
|
363
|
+
runQuery(
|
|
364
|
+
(db) => queries.unusedImports(db, file),
|
|
365
|
+
(results) => {
|
|
366
|
+
if (results.length === 0) {
|
|
367
|
+
console.log('No unused imports found.');
|
|
368
|
+
} else {
|
|
369
|
+
for (const r of results) {
|
|
370
|
+
console.log(` ${r.shortName} in ${r.importedIn}`);
|
|
371
|
+
}
|
|
372
|
+
console.log(`\n${results.length} unused import(s)`);
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// outline
|
|
379
|
+
program
|
|
380
|
+
.command('outline <file>')
|
|
381
|
+
.description('Tree view of symbols in a file (using nesting hierarchy)')
|
|
382
|
+
.action((file) => {
|
|
383
|
+
runQuery(
|
|
384
|
+
(db) => queries.outline(db, file),
|
|
385
|
+
(roots) => {
|
|
386
|
+
function printTree(nodes: typeof roots, indent: number): void {
|
|
387
|
+
for (const n of nodes) {
|
|
388
|
+
const prefix = ' '.repeat(indent);
|
|
389
|
+
console.log(`${prefix}${n.startLine}-${n.endLine} ${n.shortName}`);
|
|
390
|
+
printTree(n.children, indent + 1);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
printTree(roots, 0);
|
|
394
|
+
},
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// members
|
|
399
|
+
program
|
|
400
|
+
.command('members <symbol>')
|
|
401
|
+
.description('All children of a symbol (methods, fields, nested types)')
|
|
402
|
+
.action((symbol) => {
|
|
403
|
+
runQuery(
|
|
404
|
+
(db) => queries.members(db, symbol),
|
|
405
|
+
(results) => {
|
|
406
|
+
for (const r of results) {
|
|
407
|
+
console.log(` ${r.startLine}-${r.endLine} [${r.kind}] ${r.shortName}`);
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// fan-in
|
|
414
|
+
program
|
|
415
|
+
.command('fan-in [symbol]')
|
|
416
|
+
.description('How many files reference a symbol (or top fan-in across codebase)')
|
|
417
|
+
.option('-n, --limit <n>', 'Number of results for top mode', parseIntSafe, 30)
|
|
418
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
419
|
+
.action((symbol, opts) => {
|
|
420
|
+
withDb((db) => {
|
|
421
|
+
if (symbol) {
|
|
422
|
+
const results = queries.fanIn(db, symbol);
|
|
423
|
+
for (const r of results) {
|
|
424
|
+
console.log(` ${String(r.count).padStart(4)} files ${r.name}`);
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
const results = queries.topFanIn(db, { limit: opts.limit, scope: opts.scope });
|
|
428
|
+
console.log(' files symbol');
|
|
429
|
+
console.log(' ───── ──────');
|
|
430
|
+
for (const r of results) {
|
|
431
|
+
console.log(` ${String(r.count).padStart(5)} ${r.name}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// fan-out
|
|
438
|
+
program
|
|
439
|
+
.command('fan-out [file]')
|
|
440
|
+
.description('How many external symbols a file uses (or top fan-out across codebase)')
|
|
441
|
+
.option('-n, --limit <n>', 'Number of results for top mode', parseIntSafe, 30)
|
|
442
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
443
|
+
.action((file, opts) => {
|
|
444
|
+
withDb((db) => {
|
|
445
|
+
if (file) {
|
|
446
|
+
const results = queries.fanOut(db, file);
|
|
447
|
+
for (const r of results) {
|
|
448
|
+
console.log(` ${String(r.count).padStart(4)} symbols ${r.name}`);
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
const results = queries.topFanOut(db, { limit: opts.limit, scope: opts.scope });
|
|
452
|
+
console.log(' symbols file');
|
|
453
|
+
console.log(' ─────── ────');
|
|
454
|
+
for (const r of results) {
|
|
455
|
+
console.log(` ${String(r.count).padStart(7)} ${r.name}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// coupling
|
|
462
|
+
program
|
|
463
|
+
.command('coupling [file1] [file2]')
|
|
464
|
+
.description('Coupling between two files, or top coupled pairs in codebase')
|
|
465
|
+
.option('-n, --limit <n>', 'Number of results for top mode', parseIntSafe, 20)
|
|
466
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
467
|
+
.action((file1, file2, opts) => {
|
|
468
|
+
withDb((db) => {
|
|
469
|
+
if (file1 && file2) {
|
|
470
|
+
const result = queries.coupling(db, file1, file2);
|
|
471
|
+
console.log(`${result.file1} ↔ ${result.file2}: ${result.sharedSymbols} shared symbols`);
|
|
472
|
+
} else {
|
|
473
|
+
const results = queries.topCoupling(db, { limit: opts.limit, scope: opts.scope });
|
|
474
|
+
console.log(' shared file1 → file2');
|
|
475
|
+
console.log(' ────── ─────────────');
|
|
476
|
+
for (const r of results) {
|
|
477
|
+
console.log(` ${String(r.sharedSymbols).padStart(6)} ${r.file1} → ${r.file2}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// cycles
|
|
484
|
+
program
|
|
485
|
+
.command('cycles')
|
|
486
|
+
.description('Detect circular dependency chains between files')
|
|
487
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
488
|
+
.option('--max-depth <n>', 'Maximum cycle depth', parseIntSafe, 10)
|
|
489
|
+
.action((opts) => {
|
|
490
|
+
runQuery(
|
|
491
|
+
(db) => queries.cycles(db, { scope: opts.scope, maxDepth: opts.maxDepth }),
|
|
492
|
+
(results) => {
|
|
493
|
+
if (results.length === 0) {
|
|
494
|
+
console.log('No circular dependencies found.');
|
|
495
|
+
} else {
|
|
496
|
+
for (let i = 0; i < results.length; i++) {
|
|
497
|
+
console.log(`\nCycle ${i + 1} (${results[i]!.path.length - 1} files):`);
|
|
498
|
+
for (let j = 0; j < results[i]!.path.length; j++) {
|
|
499
|
+
const arrow = j < results[i]!.path.length - 1 ? ' →' : ' (cycle)';
|
|
500
|
+
console.log(` ${results[i]!.path[j]}${arrow}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
console.log(`\n${results.length} cycle(s) found.`);
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// bottlenecks
|
|
510
|
+
program
|
|
511
|
+
.command('bottlenecks')
|
|
512
|
+
.description('Find coupling hubs: high fan-in AND high fan-out')
|
|
513
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 20)
|
|
514
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
515
|
+
.option('--min-fan-in <n>', 'Minimum fan-in', parseIntSafe, 2)
|
|
516
|
+
.option('--min-fan-out <n>', 'Minimum fan-out', parseIntSafe, 2)
|
|
517
|
+
.action((opts) => {
|
|
518
|
+
runQuery(
|
|
519
|
+
(db) => queries.bottlenecks(db, {
|
|
520
|
+
limit: opts.limit,
|
|
521
|
+
scope: opts.scope,
|
|
522
|
+
minFanIn: opts.minFanIn,
|
|
523
|
+
minFanOut: opts.minFanOut,
|
|
524
|
+
}),
|
|
525
|
+
(results) => {
|
|
526
|
+
if (results.length === 0) {
|
|
527
|
+
console.log('No bottlenecks found.');
|
|
528
|
+
} else {
|
|
529
|
+
console.log(' score fan-in fan-out symbol');
|
|
530
|
+
console.log(' ───── ────── ─────── ──────');
|
|
531
|
+
for (const r of results) {
|
|
532
|
+
console.log(` ${String(r.score).padStart(5)} ${String(r.fanIn).padStart(6)} ${String(r.fanOut).padStart(7)} ${r.shortName}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// isolated
|
|
540
|
+
program
|
|
541
|
+
.command('isolated')
|
|
542
|
+
.description('Find completely orphaned symbols (no references at all)')
|
|
543
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
544
|
+
.option('--min-loc <n>', 'Minimum lines of code', parseIntSafe, 3)
|
|
545
|
+
.action((opts) => {
|
|
546
|
+
runQuery(
|
|
547
|
+
(db) => queries.isolated(db, { scope: opts.scope, minLoc: opts.minLoc }),
|
|
548
|
+
(results) => {
|
|
549
|
+
if (results.length === 0) {
|
|
550
|
+
console.log('No isolated symbols found.');
|
|
551
|
+
} else {
|
|
552
|
+
let prevFile = '';
|
|
553
|
+
for (const r of results) {
|
|
554
|
+
if (r.relativePath !== prevFile) {
|
|
555
|
+
if (prevFile) console.log('');
|
|
556
|
+
console.log(r.relativePath);
|
|
557
|
+
prevFile = r.relativePath;
|
|
558
|
+
}
|
|
559
|
+
console.log(` ${r.startLine}-${r.endLine} (${r.loc} LOC) ${r.shortName}`);
|
|
560
|
+
}
|
|
561
|
+
console.log(`\n${results.length} isolated symbol(s)`);
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// by-kind
|
|
568
|
+
program
|
|
569
|
+
.command('by-kind <kind>')
|
|
570
|
+
.description('Find symbols by SCIP kind (class, interface, enum, function, etc.)')
|
|
571
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
572
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 100)
|
|
573
|
+
.action((kind, opts) => {
|
|
574
|
+
runQuery(
|
|
575
|
+
(db) => queries.byKind(db, kind, { scope: opts.scope, limit: opts.limit }),
|
|
576
|
+
(results) => {
|
|
577
|
+
if (results.length === 0) {
|
|
578
|
+
console.log(`No symbols found for kind "${kind}". Use "kind-counts" to see available kinds.`);
|
|
579
|
+
} else {
|
|
580
|
+
for (const r of results) {
|
|
581
|
+
console.log(` ${r.relativePath}:${r.startLine}-${r.endLine} [${r.kindName}] ${r.shortName}`);
|
|
582
|
+
}
|
|
583
|
+
console.log(`\n${results.length} symbol(s)`);
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// kind-counts
|
|
590
|
+
program
|
|
591
|
+
.command('kind-counts')
|
|
592
|
+
.description('Histogram of symbol kinds in the codebase')
|
|
593
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
594
|
+
.action((opts) => {
|
|
595
|
+
runQuery(
|
|
596
|
+
(db) => queries.kindCounts(db, { scope: opts.scope }),
|
|
597
|
+
(results) => {
|
|
598
|
+
console.log(' count kind');
|
|
599
|
+
console.log(' ───── ────');
|
|
600
|
+
for (const r of results) {
|
|
601
|
+
console.log(` ${String(r.count).padStart(5)} ${r.kindName} (${r.kind})`);
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// test-coverage
|
|
608
|
+
program
|
|
609
|
+
.command('test-coverage [symbol]')
|
|
610
|
+
.description('Check if symbols are referenced by test files')
|
|
611
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
612
|
+
.option('--min-loc <n>', 'Minimum LOC for summary mode', parseIntSafe, 3)
|
|
613
|
+
.action((symbol, opts) => {
|
|
614
|
+
withDb((db) => {
|
|
615
|
+
if (symbol) {
|
|
616
|
+
const results = queries.testCoverage(db, symbol);
|
|
617
|
+
for (const r of results) {
|
|
618
|
+
const status = r.covered ? 'covered' : 'NOT COVERED';
|
|
619
|
+
console.log(` [${status}] ${r.shortName} (${r.definedIn})`);
|
|
620
|
+
for (const tf of r.testFiles) {
|
|
621
|
+
console.log(` ← ${tf}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
const summary = queries.testCoverageSummary(db, { scope: opts.scope, minLoc: opts.minLoc });
|
|
626
|
+
console.log(`Test coverage: ${summary.percent}%`);
|
|
627
|
+
console.log(` Total symbols: ${summary.total}`);
|
|
628
|
+
console.log(` Covered: ${summary.covered}`);
|
|
629
|
+
console.log(` Not covered: ${summary.uncovered}`);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// doc-coverage
|
|
635
|
+
program
|
|
636
|
+
.command('doc-coverage')
|
|
637
|
+
.description('Check documentation coverage across symbols')
|
|
638
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
639
|
+
.option('--min-loc <n>', 'Minimum LOC to consider', parseIntSafe, 3)
|
|
640
|
+
.option('-n, --limit <n>', 'Max undocumented symbols to show', parseIntSafe, 50)
|
|
641
|
+
.action((opts) => {
|
|
642
|
+
runQuery(
|
|
643
|
+
(db) => queries.docCoverage(db, {
|
|
644
|
+
scope: opts.scope,
|
|
645
|
+
minLoc: opts.minLoc,
|
|
646
|
+
limit: opts.limit,
|
|
647
|
+
}),
|
|
648
|
+
(result) => {
|
|
649
|
+
console.log(`Documentation coverage: ${result.coveragePercent}%`);
|
|
650
|
+
console.log(` Total symbols: ${result.totalSymbols}`);
|
|
651
|
+
console.log(` Documented: ${result.documented}`);
|
|
652
|
+
console.log(` Undocumented: ${result.undocumented}`);
|
|
653
|
+
if (result.undocumentedSymbols.length > 0) {
|
|
654
|
+
console.log('\nUndocumented:');
|
|
655
|
+
for (const s of result.undocumentedSymbols) {
|
|
656
|
+
console.log(` ${s.relativePath}:${s.startLine} ${s.shortName}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// deep-chains
|
|
664
|
+
program
|
|
665
|
+
.command('deep-chains')
|
|
666
|
+
.description('Find the longest transitive dependency chains')
|
|
667
|
+
.option('-n, --limit <n>', 'Number of chains to show', parseIntSafe, 10)
|
|
668
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
669
|
+
.option('--min-depth <n>', 'Minimum chain depth', parseIntSafe, 3)
|
|
670
|
+
.action((opts) => {
|
|
671
|
+
runQuery(
|
|
672
|
+
(db) => queries.deepChains(db, {
|
|
673
|
+
limit: opts.limit,
|
|
674
|
+
scope: opts.scope,
|
|
675
|
+
minDepth: opts.minDepth,
|
|
676
|
+
}),
|
|
677
|
+
(results) => {
|
|
678
|
+
if (results.length === 0) {
|
|
679
|
+
console.log('No deep chains found.');
|
|
680
|
+
} else {
|
|
681
|
+
for (let i = 0; i < results.length; i++) {
|
|
682
|
+
console.log(`\nChain ${i + 1} (depth ${results[i]!.depth}):`);
|
|
683
|
+
for (const file of results[i]!.chain) {
|
|
684
|
+
console.log(` → ${file}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// hierarchy
|
|
693
|
+
program
|
|
694
|
+
.command('hierarchy <symbol>')
|
|
695
|
+
.description('Show a symbol\'s ancestry chain (method → class → module)')
|
|
696
|
+
.action((symbol) => {
|
|
697
|
+
runQuery(
|
|
698
|
+
(db) => queries.hierarchy(db, symbol),
|
|
699
|
+
(chain) => {
|
|
700
|
+
if (chain.length === 0) {
|
|
701
|
+
console.log('Symbol not found.');
|
|
702
|
+
} else {
|
|
703
|
+
for (const node of chain) {
|
|
704
|
+
const indent = ' '.repeat(node.depth);
|
|
705
|
+
console.log(`${indent}${node.shortName}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// call-graph
|
|
713
|
+
program
|
|
714
|
+
.command('call-graph <symbol>')
|
|
715
|
+
.description('Show incoming callers and outgoing callees for a symbol')
|
|
716
|
+
.action((symbol) => {
|
|
717
|
+
runQuery(
|
|
718
|
+
(db) => queries.callGraph(db, symbol),
|
|
719
|
+
(result) => {
|
|
720
|
+
if (!result) {
|
|
721
|
+
console.log('Symbol not found.');
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
console.log(`Symbol: ${result.shortName}\n`);
|
|
725
|
+
console.log(`═══ CALLERS (${result.callers.length}) ═══`);
|
|
726
|
+
for (const c of result.callers) {
|
|
727
|
+
console.log(` ${c.file} ${c.shortName}`);
|
|
728
|
+
}
|
|
729
|
+
console.log(`\n═══ CALLEES (${result.callees.length}) ═══`);
|
|
730
|
+
for (const c of result.callees) {
|
|
731
|
+
console.log(` ${c.file} ${c.shortName}`);
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// similar
|
|
738
|
+
program
|
|
739
|
+
.command('similar [symbol]')
|
|
740
|
+
.description('Find functions with similar callee fingerprints (consolidation candidates)')
|
|
741
|
+
.option('--min-similarity <n>', 'Minimum Jaccard similarity (0-1)', parseFloat, 0.4)
|
|
742
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 20)
|
|
743
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
744
|
+
.option('--min-callees <n>', 'Minimum callees to consider', parseIntSafe, 4)
|
|
745
|
+
.action((symbol, opts) => {
|
|
746
|
+
withDb((db) => {
|
|
747
|
+
if (symbol) {
|
|
748
|
+
const results = queries.similar(db, symbol, {
|
|
749
|
+
minSimilarity: opts.minSimilarity,
|
|
750
|
+
limit: opts.limit,
|
|
751
|
+
});
|
|
752
|
+
if (results.length === 0) {
|
|
753
|
+
console.log('No similar symbols found.');
|
|
754
|
+
} else {
|
|
755
|
+
for (const r of results) {
|
|
756
|
+
console.log(`\n${Math.round(r.similarity * 100)}% similar:`);
|
|
757
|
+
console.log(` A: ${r.shortNameA} (${r.fileA})`);
|
|
758
|
+
console.log(` B: ${r.shortNameB} (${r.fileB})`);
|
|
759
|
+
console.log(` Shared callees: ${r.sharedCallees.join(', ')}`);
|
|
760
|
+
if (r.uniqueToA.length) console.log(` Only in A: ${r.uniqueToA.join(', ')}`);
|
|
761
|
+
if (r.uniqueToB.length) console.log(` Only in B: ${r.uniqueToB.join(', ')}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
} else {
|
|
765
|
+
const results = queries.similarAll(db, {
|
|
766
|
+
minSimilarity: opts.minSimilarity,
|
|
767
|
+
limit: opts.limit,
|
|
768
|
+
scope: opts.scope,
|
|
769
|
+
minCallees: opts.minCallees,
|
|
770
|
+
});
|
|
771
|
+
if (results.length === 0) {
|
|
772
|
+
console.log('No similar symbol pairs found.');
|
|
773
|
+
} else {
|
|
774
|
+
for (const r of results) {
|
|
775
|
+
console.log(`\n${Math.round(r.similarity * 100)}% similar:`);
|
|
776
|
+
console.log(` A: ${r.shortNameA} (${r.fileA})`);
|
|
777
|
+
console.log(` B: ${r.shortNameB} (${r.fileB})`);
|
|
778
|
+
console.log(` Shared: ${r.sharedCallees.join(', ')}`);
|
|
779
|
+
}
|
|
780
|
+
console.log(`\n${results.length} similar pair(s) found.`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// similar-files
|
|
787
|
+
program
|
|
788
|
+
.command('similar-files [file]')
|
|
789
|
+
.description('Find files with similar dependency profiles')
|
|
790
|
+
.option('--min-similarity <n>', 'Minimum Jaccard similarity (0-1)', parseFloat, 0.5)
|
|
791
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 20)
|
|
792
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
793
|
+
.option('--min-deps <n>', 'Minimum dependencies to consider', parseIntSafe, 3)
|
|
794
|
+
.action((file, opts) => {
|
|
795
|
+
runQuery(
|
|
796
|
+
(db) => queries.similarFiles(db, {
|
|
797
|
+
minSimilarity: opts.minSimilarity,
|
|
798
|
+
limit: opts.limit,
|
|
799
|
+
scope: opts.scope,
|
|
800
|
+
minDeps: opts.minDeps,
|
|
801
|
+
filePattern: file,
|
|
802
|
+
}),
|
|
803
|
+
(results) => {
|
|
804
|
+
if (results.length === 0) {
|
|
805
|
+
console.log('No similar file pairs found.');
|
|
806
|
+
} else {
|
|
807
|
+
for (const r of results) {
|
|
808
|
+
console.log(`\n${Math.round(r.similarity * 100)}% similar:`);
|
|
809
|
+
console.log(` ${r.fileA}`);
|
|
810
|
+
console.log(` ${r.fileB}`);
|
|
811
|
+
console.log(` Shared deps (${r.sharedDeps.length}): ${r.sharedDeps.join(', ')}`);
|
|
812
|
+
if (r.uniqueToA.length) console.log(` Only in first: ${r.uniqueToA.join(', ')}`);
|
|
813
|
+
if (r.uniqueToB.length) console.log(` Only in second: ${r.uniqueToB.join(', ')}`);
|
|
814
|
+
}
|
|
815
|
+
console.log(`\n${results.length} similar pair(s) found.`);
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
// similar-chains
|
|
822
|
+
program
|
|
823
|
+
.command('similar-chains')
|
|
824
|
+
.description('Find end-to-end dependency flows that diverge at few points')
|
|
825
|
+
.option('--min-similarity <n>', 'Minimum chain similarity (0-1)', parseFloat, 0.5)
|
|
826
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 15)
|
|
827
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
828
|
+
.option('--min-length <n>', 'Minimum chain length', parseIntSafe, 3)
|
|
829
|
+
.option('--max-length <n>', 'Maximum chain length', parseIntSafe, 8)
|
|
830
|
+
.action((opts) => {
|
|
831
|
+
runQuery(
|
|
832
|
+
(db) => queries.similarChains(db, {
|
|
833
|
+
minSimilarity: opts.minSimilarity,
|
|
834
|
+
limit: opts.limit,
|
|
835
|
+
scope: opts.scope,
|
|
836
|
+
minChainLength: opts.minLength,
|
|
837
|
+
maxChainLength: opts.maxLength,
|
|
838
|
+
}),
|
|
839
|
+
(results) => {
|
|
840
|
+
if (results.length === 0) {
|
|
841
|
+
console.log('No similar chains found.');
|
|
842
|
+
} else {
|
|
843
|
+
for (let i = 0; i < results.length; i++) {
|
|
844
|
+
const r = results[i]!;
|
|
845
|
+
console.log(`\n── Chain pair ${i + 1} (${Math.round(r.similarity * 100)}% similar, ${r.divergencePoints.length} divergence point(s)) ──`);
|
|
846
|
+
console.log(` Chain A: ${r.chainA.join(' → ')}`);
|
|
847
|
+
console.log(` Chain B: ${r.chainB.join(' → ')}`);
|
|
848
|
+
if (r.commonPrefix.length) console.log(` Common prefix: ${r.commonPrefix.join(' → ')}`);
|
|
849
|
+
if (r.commonSuffix.length) console.log(` Common suffix: ${r.commonSuffix.join(' → ')}`);
|
|
850
|
+
console.log(' Divergence points (consolidation targets):');
|
|
851
|
+
for (const d of r.divergencePoints) {
|
|
852
|
+
console.log(` [${d.index}] ${d.nodeA} ↔ ${d.nodeB}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
console.log(`\n${results.length} similar chain pair(s) found.`);
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// extract-candidates
|
|
862
|
+
program
|
|
863
|
+
.command('extract-candidates')
|
|
864
|
+
.description('Find functions with natural extraction seams (isolated callee clusters)')
|
|
865
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
866
|
+
.option('--min-loc <n>', 'Minimum function LOC', parseIntSafe, 10)
|
|
867
|
+
.option('--min-callees <n>', 'Minimum callees to analyze', parseIntSafe, 6)
|
|
868
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 20)
|
|
869
|
+
.action((opts) => {
|
|
870
|
+
runQuery(
|
|
871
|
+
(db) => queries.extractCandidates(db, {
|
|
872
|
+
scope: opts.scope,
|
|
873
|
+
minLoc: opts.minLoc,
|
|
874
|
+
minCallees: opts.minCallees,
|
|
875
|
+
limit: opts.limit,
|
|
876
|
+
}),
|
|
877
|
+
(results) => {
|
|
878
|
+
if (results.length === 0) {
|
|
879
|
+
console.log('No extraction candidates found.');
|
|
880
|
+
} else {
|
|
881
|
+
for (const r of results) {
|
|
882
|
+
console.log(`\n${r.relativePath}:${r.startLine}-${r.endLine} ${r.shortName} (${r.loc} LOC, ${r.totalCallees} callees)`);
|
|
883
|
+
for (let i = 0; i < r.clusters.length; i++) {
|
|
884
|
+
const c = r.clusters[i]!;
|
|
885
|
+
console.log(` Cluster ${i + 1} (${Math.round(c.isolation * 100)}% isolated, ${c.callees.length} callees):`);
|
|
886
|
+
for (const callee of c.callees) {
|
|
887
|
+
console.log(` ${callee}`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
console.log(`\n${results.length} extraction candidate(s) found.`);
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
// affected
|
|
898
|
+
program
|
|
899
|
+
.command('affected <symbol>')
|
|
900
|
+
.description('Transitive closure of symbols that could break if this symbol changes')
|
|
901
|
+
.option('--max-depth <n>', 'Maximum traversal depth', parseIntSafe, 5)
|
|
902
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
903
|
+
.action((symbol, opts) => {
|
|
904
|
+
const db = openDb();
|
|
905
|
+
const results = queries.affected(db, symbol, { maxDepth: opts.maxDepth, scope: opts.scope });
|
|
906
|
+
if (results.length === 0) {
|
|
907
|
+
console.log('No affected symbols found.');
|
|
908
|
+
} else {
|
|
909
|
+
let prevDepth = -1;
|
|
910
|
+
for (const r of results) {
|
|
911
|
+
if (r.depth !== prevDepth) {
|
|
912
|
+
console.log(`\n ── Depth ${r.depth} ──`);
|
|
913
|
+
prevDepth = r.depth;
|
|
914
|
+
}
|
|
915
|
+
console.log(` ${r.file} ${r.shortName}`);
|
|
916
|
+
}
|
|
917
|
+
console.log(`\n${results.length} affected symbol(s) across ${new Set(results.map((r) => r.file)).size} files.`);
|
|
918
|
+
}
|
|
919
|
+
db.close();
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// change-surface
|
|
923
|
+
program
|
|
924
|
+
.command('change-surface <file>')
|
|
925
|
+
.description('Pre-change briefing: exports, consumers, test coverage, risk')
|
|
926
|
+
.action((file) => {
|
|
927
|
+
const db = openDb();
|
|
928
|
+
const result = queries.changeSurface(db, file);
|
|
929
|
+
if (!result) {
|
|
930
|
+
console.log('File not found in index.');
|
|
931
|
+
db.close();
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
console.log(`File: ${result.file}`);
|
|
935
|
+
console.log(`Test coverage: ${result.testCoveragePercent}% | External consumers: ${result.totalExternalConsumers}\n`);
|
|
936
|
+
for (const s of result.symbols) {
|
|
937
|
+
const risk = s.riskLevel === 'high' ? ' *** HIGH RISK ***' : s.riskLevel === 'medium' ? ' * medium risk *' : '';
|
|
938
|
+
const tests = s.testFiles.length > 0 ? ` (${s.testFiles.length} test file(s))` : ' (no tests)';
|
|
939
|
+
console.log(` ${s.startLine}-${s.endLine} ${s.shortName} [${s.externalConsumers} consumers]${tests}${risk}`);
|
|
940
|
+
}
|
|
941
|
+
db.close();
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// diff-impact
|
|
945
|
+
program
|
|
946
|
+
.command('diff-impact')
|
|
947
|
+
.description('Compute affected symbols from current git diff')
|
|
948
|
+
.option('--base <ref>', 'Git ref to diff against (default: HEAD)')
|
|
949
|
+
.action((opts) => {
|
|
950
|
+
const db = openDb();
|
|
951
|
+
const result = queries.diffImpact(db, { base: opts.base });
|
|
952
|
+
console.log(`Changed files: ${result.summary.totalChangedFiles}`);
|
|
953
|
+
console.log(`Changed symbols: ${result.summary.totalChangedSymbols}`);
|
|
954
|
+
console.log(`Affected consumer files: ${result.summary.totalAffectedFiles}`);
|
|
955
|
+
console.log(`Test coverage: ${result.summary.testCoveragePercent}%\n`);
|
|
956
|
+
if (result.changedSymbols.length > 0) {
|
|
957
|
+
console.log('Changed symbols:');
|
|
958
|
+
for (const s of result.changedSymbols) {
|
|
959
|
+
console.log(` ${s.file} ${s.shortName} (fan-in: ${s.fanIn})`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (result.uncoveredSymbols.length > 0) {
|
|
963
|
+
console.log('\nUncovered (no test references):');
|
|
964
|
+
for (const s of result.uncoveredSymbols) {
|
|
965
|
+
console.log(` ${s.file} ${s.shortName}`);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (result.affectedConsumers.length > 0) {
|
|
969
|
+
console.log('\nAffected consumer files:');
|
|
970
|
+
for (const c of result.affectedConsumers) {
|
|
971
|
+
console.log(` ${c.file} (${c.consumedSymbols} symbol(s))`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
db.close();
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
// drift
|
|
978
|
+
program
|
|
979
|
+
.command('drift [module]')
|
|
980
|
+
.description('Detect unused imports, layer violations, and pattern deviations')
|
|
981
|
+
.action((module) => {
|
|
982
|
+
const db = openDb();
|
|
983
|
+
const summary = queries.drift(db, { scope: module });
|
|
984
|
+
if (summary.results.length === 0) {
|
|
985
|
+
console.log('No drift detected.');
|
|
986
|
+
} else {
|
|
987
|
+
const grouped = new Map<string, typeof summary.results>();
|
|
988
|
+
for (const r of summary.results) {
|
|
989
|
+
if (!grouped.has(r.file)) grouped.set(r.file, []);
|
|
990
|
+
grouped.get(r.file)!.push(r);
|
|
991
|
+
}
|
|
992
|
+
for (const [file, items] of grouped) {
|
|
993
|
+
console.log(`\n${file}`);
|
|
994
|
+
for (const r of items) {
|
|
995
|
+
const tag = r.kind === 'unused-import' ? 'UNUSED' : r.kind === 'layer-violation' ? 'LAYER' : 'UNIQUE';
|
|
996
|
+
console.log(` [${tag}] ${r.description}`);
|
|
997
|
+
if (r.detail) console.log(` ${r.detail}`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
console.log(`\n${summary.unusedImports} unused import(s), ${summary.layerViolations} layer violation(s), ${summary.patternDeviations} pattern deviation(s)`);
|
|
1001
|
+
}
|
|
1002
|
+
db.close();
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
// wrapper-candidates
|
|
1006
|
+
program
|
|
1007
|
+
.command('wrapper-candidates')
|
|
1008
|
+
.description('Find symbols only called by one consumer (premature abstractions)')
|
|
1009
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
1010
|
+
.option('--max-loc <n>', 'Maximum LOC for candidates', parseIntSafe, 15)
|
|
1011
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 30)
|
|
1012
|
+
.action((opts) => {
|
|
1013
|
+
const db = openDb();
|
|
1014
|
+
const results = queries.wrapperCandidates(db, { scope: opts.scope, maxLoc: opts.maxLoc, limit: opts.limit });
|
|
1015
|
+
if (results.length === 0) {
|
|
1016
|
+
console.log('No wrapper candidates found.');
|
|
1017
|
+
} else {
|
|
1018
|
+
for (const r of results) {
|
|
1019
|
+
console.log(` ${r.file}:${r.startLine}-${r.endLine} ${r.shortName} (${r.loc} LOC)`);
|
|
1020
|
+
console.log(` Only called by: ${r.singleCallerShort} (fan-in: ${r.callerFanIn})`);
|
|
1021
|
+
}
|
|
1022
|
+
console.log(`\n${results.length} wrapper candidate(s).`);
|
|
1023
|
+
}
|
|
1024
|
+
db.close();
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
// passthrough-candidates
|
|
1028
|
+
program
|
|
1029
|
+
.command('passthrough-candidates')
|
|
1030
|
+
.description('Find functions that just forward to one other function')
|
|
1031
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
1032
|
+
.option('--max-loc <n>', 'Maximum LOC for candidates', parseIntSafe, 15)
|
|
1033
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 30)
|
|
1034
|
+
.action((opts) => {
|
|
1035
|
+
const db = openDb();
|
|
1036
|
+
const results = queries.passthroughCandidates(db, { scope: opts.scope, maxLoc: opts.maxLoc, limit: opts.limit });
|
|
1037
|
+
if (results.length === 0) {
|
|
1038
|
+
console.log('No passthrough candidates found.');
|
|
1039
|
+
} else {
|
|
1040
|
+
for (const r of results) {
|
|
1041
|
+
console.log(` ${r.file}:${r.startLine}-${r.endLine} ${r.shortName} (${r.loc} LOC)`);
|
|
1042
|
+
console.log(` Forwards to: ${r.forwardsToShort} (${r.forwardsToFile})`);
|
|
1043
|
+
}
|
|
1044
|
+
console.log(`\n${results.length} passthrough candidate(s).`);
|
|
1045
|
+
}
|
|
1046
|
+
db.close();
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
// stale-abstractions
|
|
1050
|
+
program
|
|
1051
|
+
.command('stale-abstractions')
|
|
1052
|
+
.description('Find types/interfaces with 0-1 consumers (premature abstractions)')
|
|
1053
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
1054
|
+
.option('--min-loc <n>', 'Minimum LOC', parseIntSafe, 3)
|
|
1055
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 30)
|
|
1056
|
+
.action((opts) => {
|
|
1057
|
+
const db = openDb();
|
|
1058
|
+
const results = queries.staleAbstractions(db, { scope: opts.scope, minLoc: opts.minLoc, limit: opts.limit });
|
|
1059
|
+
if (results.length === 0) {
|
|
1060
|
+
console.log('No stale abstractions found.');
|
|
1061
|
+
} else {
|
|
1062
|
+
for (const r of results) {
|
|
1063
|
+
const label = r.consumers === 0 ? 'unused' : '1 consumer';
|
|
1064
|
+
console.log(` ${r.file}:${r.startLine}-${r.endLine} ${r.shortName} (${r.loc} LOC, ${label})`);
|
|
1065
|
+
}
|
|
1066
|
+
console.log(`\n${results.length} stale abstraction(s).`);
|
|
1067
|
+
}
|
|
1068
|
+
db.close();
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
// complexity-hotspots
|
|
1072
|
+
program
|
|
1073
|
+
.command('complexity-hotspots')
|
|
1074
|
+
.description('Composite complexity score: LOC x fan-in x fan-out')
|
|
1075
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
1076
|
+
.option('--min-loc <n>', 'Minimum LOC', parseIntSafe, 10)
|
|
1077
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 20)
|
|
1078
|
+
.action((opts) => {
|
|
1079
|
+
const db = openDb();
|
|
1080
|
+
const results = queries.complexityHotspots(db, { scope: opts.scope, minLoc: opts.minLoc, limit: opts.limit });
|
|
1081
|
+
if (results.length === 0) {
|
|
1082
|
+
console.log('No complexity hotspots found.');
|
|
1083
|
+
} else {
|
|
1084
|
+
console.log(' score LOC fan-in fan-out callees symbol');
|
|
1085
|
+
console.log(' ───── ──── ────── ─────── ─────── ──────');
|
|
1086
|
+
for (const r of results) {
|
|
1087
|
+
console.log(` ${r.score.toFixed(1).padStart(5)} ${String(r.loc).padStart(4)} ${String(r.fanIn).padStart(6)} ${String(r.fanOut).padStart(7)} ${String(r.calleeCount).padStart(7)} ${r.shortName}`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
db.close();
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
// health
|
|
1094
|
+
program
|
|
1095
|
+
.command('health')
|
|
1096
|
+
.description('Composite codebase health report with prioritized action list')
|
|
1097
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
1098
|
+
.option('--json', 'Output as JSON for programmatic consumption')
|
|
1099
|
+
.action((opts) => {
|
|
1100
|
+
const db = openDb();
|
|
1101
|
+
const report = queries.health(db, { scope: opts.scope });
|
|
1102
|
+
if (opts.json) {
|
|
1103
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1104
|
+
} else {
|
|
1105
|
+
console.log(`\n Codebase Health Score: ${report.score}/100\n`);
|
|
1106
|
+
console.log(` ${report.overview.documents} files | ${report.overview.symbols} symbols | ${formatBytes(report.overview.indexSizeBytes)}\n`);
|
|
1107
|
+
|
|
1108
|
+
console.log(' Findings:');
|
|
1109
|
+
const f = report.findings;
|
|
1110
|
+
if (f.deadSymbols > 0) console.log(` Dead code: ${f.deadSymbols} symbols (${f.deadLoc} LOC)`);
|
|
1111
|
+
if (f.isolatedSymbols > 0) console.log(` Isolated symbols: ${f.isolatedSymbols} (${f.isolatedLoc} LOC)`);
|
|
1112
|
+
if (f.cycles > 0) console.log(` Circular deps: ${f.cycles}`);
|
|
1113
|
+
if (f.similarPairs > 0) console.log(` Similar pairs: ${f.similarPairs}`);
|
|
1114
|
+
if (f.extractionCandidates > 0) console.log(` Extract candidates: ${f.extractionCandidates}`);
|
|
1115
|
+
if (f.wrappers > 0) console.log(` Wrapper functions: ${f.wrappers}`);
|
|
1116
|
+
if (f.passthroughs > 0) console.log(` Passthroughs: ${f.passthroughs}`);
|
|
1117
|
+
if (f.staleTypes > 0) console.log(` Stale abstractions: ${f.staleTypes}`);
|
|
1118
|
+
if (f.driftedFiles > 0) console.log(` Pattern drift: ${f.driftedFiles} files`);
|
|
1119
|
+
if (f.complexityHotspotCount > 0) console.log(` Complexity hotspots: ${f.complexityHotspotCount}`);
|
|
1120
|
+
console.log(` Test coverage: ${f.testCoveragePercent}%`);
|
|
1121
|
+
|
|
1122
|
+
if (report.actions.length > 0) {
|
|
1123
|
+
console.log('\n Prioritized Actions (highest impact + lowest effort first):');
|
|
1124
|
+
for (let i = 0; i < report.actions.length; i++) {
|
|
1125
|
+
const a = report.actions[i]!;
|
|
1126
|
+
const loc = a.locRecoverable > 0 ? ` (~${a.locRecoverable} LOC recoverable)` : '';
|
|
1127
|
+
console.log(` ${i + 1}. [${a.effort} effort / ${a.impact} impact] ${a.description}${loc}`);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
if (report.topComplexity.length > 0) {
|
|
1132
|
+
console.log('\n Top Complexity Hotspots:');
|
|
1133
|
+
for (const c of report.topComplexity) {
|
|
1134
|
+
console.log(` ${c.score.toFixed(1).padStart(6)} ${c.symbol}`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (report.actions.length === 0) {
|
|
1139
|
+
console.log('\n No issues found. Codebase is clean.');
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
db.close();
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// convergence
|
|
1146
|
+
program
|
|
1147
|
+
.command('convergence <symbol1> <symbol2>')
|
|
1148
|
+
.description('Show what a consolidated version of two similar functions would look like')
|
|
1149
|
+
.action((symbol1, symbol2) => {
|
|
1150
|
+
const db = openDb();
|
|
1151
|
+
const result = queries.convergence(db, symbol1, symbol2);
|
|
1152
|
+
if (!result) {
|
|
1153
|
+
console.log('One or both symbols not found.');
|
|
1154
|
+
db.close();
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
console.log(`\n${Math.round(result.similarity * 100)}% callee overlap\n`);
|
|
1158
|
+
console.log(` A: ${result.symbolA.shortName} (${result.symbolA.file}, ${result.symbolA.loc} LOC)`);
|
|
1159
|
+
console.log(` B: ${result.symbolB.shortName} (${result.symbolB.file}, ${result.symbolB.loc} LOC)\n`);
|
|
1160
|
+
console.log(` Shared callees (${result.sharedCallees.length}):`);
|
|
1161
|
+
for (const c of result.sharedCallees) console.log(` ${c}`);
|
|
1162
|
+
if (result.uniqueToA.length > 0) {
|
|
1163
|
+
console.log(`\n Unique to A (${result.uniqueToA.length}):`);
|
|
1164
|
+
for (const c of result.uniqueToA) console.log(` ${c}`);
|
|
1165
|
+
}
|
|
1166
|
+
if (result.uniqueToB.length > 0) {
|
|
1167
|
+
console.log(`\n Unique to B (${result.uniqueToB.length}):`);
|
|
1168
|
+
for (const c of result.uniqueToB) console.log(` ${c}`);
|
|
1169
|
+
}
|
|
1170
|
+
console.log(`\n Strategy: ${result.consolidationStrategy}`);
|
|
1171
|
+
db.close();
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
// code
|
|
1175
|
+
program
|
|
1176
|
+
.command('code <symbol>')
|
|
1177
|
+
.description('Read the source code for a symbol (bounded to its definition range)')
|
|
1178
|
+
.option('-C, --context <n>', 'Extra lines of context above/below', parseIntSafe, 0)
|
|
1179
|
+
.action((symbol, opts) => {
|
|
1180
|
+
const db = openDb();
|
|
1181
|
+
const result = queries.code(db, symbol, { context: opts.context });
|
|
1182
|
+
if (!result) {
|
|
1183
|
+
console.log('Symbol not found or file unreadable.');
|
|
1184
|
+
db.close();
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
console.log(`${result.relativePath}:${result.startLine}-${result.endLine} ${result.shortName} [${result.language ?? 'unknown'}]\n`);
|
|
1188
|
+
const lines = result.source.split('\n');
|
|
1189
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1190
|
+
console.log(` ${String(result.startLine + i).padStart(4)} ${lines[i]}`);
|
|
1191
|
+
}
|
|
1192
|
+
db.close();
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
// complexity
|
|
1196
|
+
program
|
|
1197
|
+
.command('complexity <symbol>')
|
|
1198
|
+
.description('Per-symbol complexity: branches, cyclomatic estimate, fan-in/out, callees')
|
|
1199
|
+
.action((symbol) => {
|
|
1200
|
+
const db = openDb();
|
|
1201
|
+
const result = queries.complexity(db, symbol);
|
|
1202
|
+
if (!result) {
|
|
1203
|
+
console.log('Symbol not found.');
|
|
1204
|
+
db.close();
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
console.log(`${result.relativePath}:${result.startLine}-${result.endLine} ${result.shortName}\n`);
|
|
1208
|
+
console.log(` LOC: ${result.loc}`);
|
|
1209
|
+
console.log(` Branches: ${result.branches}`);
|
|
1210
|
+
console.log(` Cyclomatic estimate: ${result.cyclomaticEstimate}`);
|
|
1211
|
+
console.log(` Callees: ${result.calleeCount}`);
|
|
1212
|
+
console.log(` Fan-in: ${result.fanIn}`);
|
|
1213
|
+
console.log(` Fan-out: ${result.fanOut}`);
|
|
1214
|
+
db.close();
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// dataflow
|
|
1218
|
+
program
|
|
1219
|
+
.command('dataflow <symbol>')
|
|
1220
|
+
.description('Reference-level dataflow: definition sites, usage sites, producers, consumers')
|
|
1221
|
+
.action((symbol) => {
|
|
1222
|
+
const db = openDb();
|
|
1223
|
+
const result = queries.dataflow(db, symbol);
|
|
1224
|
+
if (!result) {
|
|
1225
|
+
console.log('Symbol not found.');
|
|
1226
|
+
db.close();
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
console.log(`${result.shortName} (${result.relativePath})\n`);
|
|
1230
|
+
|
|
1231
|
+
if (result.definitionSites.length > 0) {
|
|
1232
|
+
console.log(' ═══ DEFINED AT ═══');
|
|
1233
|
+
for (const s of result.definitionSites) {
|
|
1234
|
+
console.log(` ${s.file}:${s.line}`);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
if (result.usageSites.length > 0) {
|
|
1239
|
+
console.log('\n ═══ USED AT ═══');
|
|
1240
|
+
for (const s of result.usageSites) {
|
|
1241
|
+
console.log(` ${s.file}:${s.line} in ${s.enclosingShort}`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (result.producers.length > 0) {
|
|
1246
|
+
console.log('\n ═══ PRODUCERS (feeds into this) ═══');
|
|
1247
|
+
for (const p of result.producers) {
|
|
1248
|
+
console.log(` ${p.file} ${p.shortName}`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (result.consumers.length > 0) {
|
|
1253
|
+
console.log('\n ═══ CONSUMERS (this feeds into) ═══');
|
|
1254
|
+
for (const c of result.consumers) {
|
|
1255
|
+
console.log(` ${c.file} ${c.shortName}`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
db.close();
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// slice
|
|
1262
|
+
program
|
|
1263
|
+
.command('slice <symbol>')
|
|
1264
|
+
.description('Reference-level program slice: what affects this (backward) or what this affects (forward)')
|
|
1265
|
+
.option('--forward', 'Forward slice (what does this affect). Default is backward.')
|
|
1266
|
+
.action((symbol, opts) => {
|
|
1267
|
+
const db = openDb();
|
|
1268
|
+
const direction = opts.forward ? 'forward' : 'backward';
|
|
1269
|
+
const result = queries.slice(db, symbol, { direction });
|
|
1270
|
+
if (!result) {
|
|
1271
|
+
console.log('Symbol not found.');
|
|
1272
|
+
db.close();
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
console.log(`${result.direction} slice of ${result.shortName}\n`);
|
|
1276
|
+
if (result.connectedSymbols.length === 0) {
|
|
1277
|
+
console.log(' No connected symbols found.');
|
|
1278
|
+
} else {
|
|
1279
|
+
for (const s of result.connectedSymbols) {
|
|
1280
|
+
console.log(` ${s.file} ${s.shortName}`);
|
|
1281
|
+
console.log(` ${s.relationship}`);
|
|
1282
|
+
}
|
|
1283
|
+
console.log(`\n${result.connectedSymbols.length} connected symbol(s).`);
|
|
1284
|
+
}
|
|
1285
|
+
db.close();
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
// install-skills
|
|
1289
|
+
program
|
|
1290
|
+
.command('install-skills')
|
|
1291
|
+
.description('Install skills (concrete-plan, scip-explore, scip-debloat, scip-verify) into Claude Code and Codex')
|
|
1292
|
+
.action(() => {
|
|
1293
|
+
const result = installSkills();
|
|
1294
|
+
const total = result.installed.length + result.alreadyLinked.length;
|
|
1295
|
+
console.log(`\n${result.installed.length} installed, ${result.alreadyLinked.length} already linked, ${result.skipped.length} skipped.`);
|
|
1296
|
+
if (total > 0) {
|
|
1297
|
+
console.log('Skills will be available in your next Claude Code / Codex session.');
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
// check-deps
|
|
1302
|
+
program
|
|
1303
|
+
.command('check-deps')
|
|
1304
|
+
.description('Check if required dependencies (scip CLI) are installed')
|
|
1305
|
+
.action(() => {
|
|
1306
|
+
if (isScipInstalled()) {
|
|
1307
|
+
console.log('scip CLI: installed');
|
|
1308
|
+
} else {
|
|
1309
|
+
printScipInstallInstructions();
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
// redundant-reexports
|
|
1314
|
+
program
|
|
1315
|
+
.command('redundant-reexports')
|
|
1316
|
+
.description('Find barrel re-exports that nobody imports through')
|
|
1317
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
1318
|
+
.option('-n, --limit <n>', 'Number of results', parseIntSafe, 30)
|
|
1319
|
+
.action((opts) => {
|
|
1320
|
+
const db = openDb();
|
|
1321
|
+
const results = queries.redundantReexports(db, { scope: opts.scope, limit: opts.limit });
|
|
1322
|
+
if (results.length === 0) {
|
|
1323
|
+
console.log('No redundant re-exports found.');
|
|
1324
|
+
} else {
|
|
1325
|
+
let prevBarrel = '';
|
|
1326
|
+
for (const r of results) {
|
|
1327
|
+
if (r.barrelFile !== prevBarrel) {
|
|
1328
|
+
if (prevBarrel) console.log('');
|
|
1329
|
+
console.log(r.barrelFile);
|
|
1330
|
+
prevBarrel = r.barrelFile;
|
|
1331
|
+
}
|
|
1332
|
+
console.log(` ${r.shortName} (from ${r.originalFile})`);
|
|
1333
|
+
console.log(` barrel: ${r.barrelConsumers} consumer(s) | direct: ${r.directConsumers} consumer(s)`);
|
|
1334
|
+
}
|
|
1335
|
+
console.log(`\n${results.length} redundant re-export(s).`);
|
|
1336
|
+
}
|
|
1337
|
+
db.close();
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
// similar-signatures
|
|
1341
|
+
program
|
|
1342
|
+
.command('similar-signatures')
|
|
1343
|
+
.description('Find functions with near-identical type signatures (same shape)')
|
|
1344
|
+
.option('-s, --scope <path>', 'Limit to files matching path')
|
|
1345
|
+
.option('--min-loc <n>', 'Minimum LOC per function', parseIntSafe, 3)
|
|
1346
|
+
.option('-n, --limit <n>', 'Number of groups', parseIntSafe, 20)
|
|
1347
|
+
.action((opts) => {
|
|
1348
|
+
const db = openDb();
|
|
1349
|
+
const groups = queries.similarSignatures(db, { scope: opts.scope, minLoc: opts.minLoc, limit: opts.limit });
|
|
1350
|
+
if (groups.length === 0) {
|
|
1351
|
+
console.log('No same-shape function groups found.');
|
|
1352
|
+
} else {
|
|
1353
|
+
for (const g of groups) {
|
|
1354
|
+
console.log(`\nSignature: ${g.signature} (${g.functions.length} functions)`);
|
|
1355
|
+
for (const f of g.functions) {
|
|
1356
|
+
console.log(` ${f.file}:${f.startLine}-${f.endLine} ${f.shortName} (${f.loc} LOC)`);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
console.log(`\n${groups.length} group(s) found.`);
|
|
1360
|
+
}
|
|
1361
|
+
db.close();
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
// init
|
|
1365
|
+
program
|
|
1366
|
+
.command('init')
|
|
1367
|
+
.description('Create a .scipquery.json config file for this project')
|
|
1368
|
+
.action(() => {
|
|
1369
|
+
const projectRoot = resolveProjectRoot();
|
|
1370
|
+
const languages = detectLanguages(projectRoot);
|
|
1371
|
+
const configPath = initProjectConfig(projectRoot, languages);
|
|
1372
|
+
console.log(`Config written to ${configPath}`);
|
|
1373
|
+
console.log(`Detected languages: ${languages.join(', ') || '(none)'}`);
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
// watch
|
|
1377
|
+
program
|
|
1378
|
+
.command('watch')
|
|
1379
|
+
.description('Watch for file changes and reindex automatically')
|
|
1380
|
+
.option('--debounce <ms>', 'Ms to wait after last change (default: 30000)', parseInt)
|
|
1381
|
+
.option('--cooldown <ms>', 'Min ms between reindexes (default: 60000)', parseInt)
|
|
1382
|
+
.action((opts) => {
|
|
1383
|
+
const projectRoot = resolveProjectRoot();
|
|
1384
|
+
const config = loadProjectConfig(projectRoot);
|
|
1385
|
+
|
|
1386
|
+
// CLI flags override config
|
|
1387
|
+
if (opts.debounce) (config.watch ??= {}).debounceMs = opts.debounce;
|
|
1388
|
+
if (opts.cooldown) (config.watch ??= {}).cooldownMs = opts.cooldown;
|
|
1389
|
+
|
|
1390
|
+
const watcher = new Watcher({
|
|
1391
|
+
projectRoot,
|
|
1392
|
+
config,
|
|
1393
|
+
onStatus: (status) => {
|
|
1394
|
+
process.stdout.write(`\r\x1b[K${formatStatus(status)}`);
|
|
1395
|
+
},
|
|
1396
|
+
onReindexComplete: (durationMs) => {
|
|
1397
|
+
console.log(`\nReindex complete in ${(durationMs / 1000).toFixed(1)}s`);
|
|
1398
|
+
},
|
|
1399
|
+
onError: (err) => {
|
|
1400
|
+
console.error(`\nWatch error: ${err.message}`);
|
|
1401
|
+
},
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
console.log(`Watching ${projectRoot}`);
|
|
1405
|
+
console.log(`Debounce: ${config.watch?.debounceMs ?? 30000}ms | Cooldown: ${config.watch?.cooldownMs ?? 60000}ms`);
|
|
1406
|
+
console.log('Press Ctrl+C to stop.\n');
|
|
1407
|
+
watcher.start();
|
|
1408
|
+
|
|
1409
|
+
process.on('SIGINT', () => {
|
|
1410
|
+
watcher.stop();
|
|
1411
|
+
console.log('\nStopped.');
|
|
1412
|
+
process.exit(0);
|
|
1413
|
+
});
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
// status
|
|
1417
|
+
program
|
|
1418
|
+
.command('status')
|
|
1419
|
+
.description('Show index status for this project')
|
|
1420
|
+
.action(() => {
|
|
1421
|
+
const projectRoot = resolveProjectRoot();
|
|
1422
|
+
const config = loadProjectConfig(projectRoot);
|
|
1423
|
+
const paths = resolveIndexPaths(projectRoot, config);
|
|
1424
|
+
|
|
1425
|
+
console.log(`Project: ${projectRoot}`);
|
|
1426
|
+
console.log(`DB path: ${paths.dbPath}`);
|
|
1427
|
+
console.log(`Exists: ${existsSync(paths.dbPath) ? 'yes' : 'no'}`);
|
|
1428
|
+
|
|
1429
|
+
if (existsSync(paths.dbPath)) {
|
|
1430
|
+
withDb((db) => {
|
|
1431
|
+
const s = queries.stats(db);
|
|
1432
|
+
console.log(`Symbols: ${s.symbols}`);
|
|
1433
|
+
console.log(`Files: ${s.documents}`);
|
|
1434
|
+
console.log(`Size: ${formatBytes(s.indexSizeBytes)}`);
|
|
1435
|
+
if (s.lastBuilt) {
|
|
1436
|
+
const ago = Math.round((Date.now() - s.lastBuilt.getTime()) / 1000);
|
|
1437
|
+
console.log(`Built: ${ago}s ago`);
|
|
1438
|
+
}
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
// ── Parse & Run ────────────────────────────────────────────
|
|
1444
|
+
|
|
1445
|
+
program.parse();
|
|
1446
|
+
|
|
1447
|
+
// ── Utility ────────────────────────────────────────────────
|
|
1448
|
+
|
|
1449
|
+
function collect(value: string, prev: string[]): string[] {
|
|
1450
|
+
return prev.concat([value]);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/** parseInt wrapper safe for commander (which passes default as 2nd arg = radix) */
|
|
1454
|
+
function parseIntSafe(value: string): number {
|
|
1455
|
+
return parseInt(value, 10);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function formatBytes(bytes: number): string {
|
|
1459
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1460
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
1461
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1462
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function formatStatus(status: WatcherStatus): string {
|
|
1466
|
+
switch (status.state) {
|
|
1467
|
+
case 'idle':
|
|
1468
|
+
return 'Watching (idle)';
|
|
1469
|
+
case 'waiting': {
|
|
1470
|
+
const secs = Math.round((status.reindexAt - Date.now()) / 1000);
|
|
1471
|
+
return `${status.changedFiles} file(s) changed, reindexing in ${secs}s...`;
|
|
1472
|
+
}
|
|
1473
|
+
case 'indexing':
|
|
1474
|
+
return `Reindexing... (${Math.round((Date.now() - status.startedAt) / 1000)}s)`;
|
|
1475
|
+
case 'cooldown': {
|
|
1476
|
+
const secs = Math.round((status.until - Date.now()) / 1000);
|
|
1477
|
+
return `Cooldown (${secs}s)${status.dirty ? ' — changes pending' : ''}`;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|