ts-knowledge-graph 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -8
- package/contribs/{web_visualisation → webview}/README.md +7 -7
- package/contribs/webview/web/css/style.css +310 -0
- package/contribs/{web_visualisation → webview}/web/index.html +40 -5
- package/contribs/{web_visualisation → webview}/web/js/app.js +378 -39
- package/contribs/{web_visualisation/web/data → webview/web/js_autogenerated}/kind_descriptions.js +2 -1
- package/contribs/{web_visualisation → webview}/web/types/app_globals.d.ts +11 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/cluster/cluster_weights.d.ts +20 -0
- package/dist/cluster/cluster_weights.d.ts.map +1 -0
- package/dist/cluster/cluster_weights.js +32 -0
- package/dist/cluster/cluster_weights.js.map +1 -0
- package/dist/cluster/community_detector.d.ts +61 -0
- package/dist/cluster/community_detector.d.ts.map +1 -0
- package/dist/cluster/community_detector.js +120 -0
- package/dist/cluster/community_detector.js.map +1 -0
- package/dist/cluster/community_labeler.d.ts +84 -0
- package/dist/cluster/community_labeler.d.ts.map +1 -0
- package/dist/cluster/community_labeler.js +194 -0
- package/dist/cluster/community_labeler.js.map +1 -0
- package/dist/cluster/graph_clusterer.d.ts +47 -0
- package/dist/cluster/graph_clusterer.d.ts.map +1 -0
- package/dist/cluster/graph_clusterer.js +126 -0
- package/dist/cluster/graph_clusterer.js.map +1 -0
- package/dist/commands/benchmark_command.d.ts.map +1 -1
- package/dist/commands/benchmark_command.js +13 -10
- package/dist/commands/benchmark_command.js.map +1 -1
- package/dist/commands/blast_radius_command.d.ts.map +1 -1
- package/dist/commands/blast_radius_command.js +6 -5
- package/dist/commands/blast_radius_command.js.map +1 -1
- package/dist/commands/cluster_command.d.ts +7 -0
- package/dist/commands/cluster_command.d.ts.map +1 -0
- package/dist/commands/cluster_command.js +55 -0
- package/dist/commands/cluster_command.js.map +1 -0
- package/dist/commands/command_helpers.d.ts +9 -4
- package/dist/commands/command_helpers.d.ts.map +1 -1
- package/dist/commands/command_helpers.js +13 -8
- package/dist/commands/command_helpers.js.map +1 -1
- package/dist/commands/cost_command.d.ts.map +1 -1
- package/dist/commands/cost_command.js +25 -8
- package/dist/commands/cost_command.js.map +1 -1
- package/dist/commands/enrich_command.d.ts.map +1 -1
- package/dist/commands/enrich_command.js +7 -5
- package/dist/commands/enrich_command.js.map +1 -1
- package/dist/commands/extract_command.d.ts.map +1 -1
- package/dist/commands/extract_command.js +12 -6
- package/dist/commands/extract_command.js.map +1 -1
- package/dist/commands/hotspots_command.d.ts.map +1 -1
- package/dist/commands/hotspots_command.js +6 -5
- package/dist/commands/hotspots_command.js.map +1 -1
- package/dist/commands/install_command.d.ts +15 -5
- package/dist/commands/install_command.d.ts.map +1 -1
- package/dist/commands/install_command.js +61 -23
- package/dist/commands/install_command.js.map +1 -1
- package/dist/commands/load_command.d.ts.map +1 -1
- package/dist/commands/load_command.js +18 -13
- package/dist/commands/load_command.js.map +1 -1
- package/dist/commands/neighbors_command.d.ts.map +1 -1
- package/dist/commands/neighbors_command.js +6 -5
- package/dist/commands/neighbors_command.js.map +1 -1
- package/dist/commands/references_command.d.ts.map +1 -1
- package/dist/commands/references_command.js +6 -5
- package/dist/commands/references_command.js.map +1 -1
- package/dist/commands/report_command.d.ts +16 -0
- package/dist/commands/report_command.d.ts.map +1 -0
- package/dist/commands/report_command.js +115 -0
- package/dist/commands/report_command.js.map +1 -0
- package/dist/commands/webview_command.d.ts +36 -0
- package/dist/commands/webview_command.d.ts.map +1 -0
- package/dist/commands/webview_command.js +186 -0
- package/dist/commands/webview_command.js.map +1 -0
- package/dist/enrich/cpu_profile.d.ts +33 -0
- package/dist/enrich/cpu_profile.d.ts.map +1 -1
- package/dist/enrich/cpu_profile.js +88 -0
- package/dist/enrich/cpu_profile.js.map +1 -1
- package/dist/enrich/runtime_enricher.d.ts +8 -0
- package/dist/enrich/runtime_enricher.d.ts.map +1 -1
- package/dist/enrich/runtime_enricher.js +18 -0
- package/dist/enrich/runtime_enricher.js.map +1 -1
- package/dist/enrich/runtime_join.d.ts +25 -1
- package/dist/enrich/runtime_join.d.ts.map +1 -1
- package/dist/enrich/runtime_join.js +43 -0
- package/dist/enrich/runtime_join.js.map +1 -1
- package/dist/extract/git_source.d.ts +23 -0
- package/dist/extract/git_source.d.ts.map +1 -0
- package/dist/extract/git_source.js +75 -0
- package/dist/extract/git_source.js.map +1 -0
- package/dist/query/graph_query.d.ts +36 -1
- package/dist/query/graph_query.d.ts.map +1 -1
- package/dist/query/graph_query.js +69 -6
- package/dist/query/graph_query.js.map +1 -1
- package/dist/report/graph_report.d.ts +51 -0
- package/dist/report/graph_report.d.ts.map +1 -0
- package/dist/report/graph_report.js +312 -0
- package/dist/report/graph_report.js.map +1 -0
- package/dist/report/pdf_renderer.d.ts +22 -0
- package/dist/report/pdf_renderer.d.ts.map +1 -0
- package/dist/report/pdf_renderer.js +54 -0
- package/dist/report/pdf_renderer.js.map +1 -0
- package/dist/report/report_data.d.ts +128 -0
- package/dist/report/report_data.d.ts.map +1 -0
- package/dist/report/report_data.js +191 -0
- package/dist/report/report_data.js.map +1 -0
- package/dist/schema/edge.d.ts +5 -5
- package/dist/schema/edge.d.ts.map +1 -1
- package/dist/schema/edge.js +3 -0
- package/dist/schema/edge.js.map +1 -1
- package/dist/schema/source_manifest.d.ts +30 -0
- package/dist/schema/source_manifest.d.ts.map +1 -0
- package/dist/schema/source_manifest.js +21 -0
- package/dist/schema/source_manifest.js.map +1 -0
- package/dist/store/jsonl_reader.d.ts +4 -0
- package/dist/store/jsonl_reader.d.ts.map +1 -1
- package/dist/store/jsonl_reader.js +13 -1
- package/dist/store/jsonl_reader.js.map +1 -1
- package/dist/store/jsonl_store.d.ts +2 -1
- package/dist/store/jsonl_store.d.ts.map +1 -1
- package/dist/store/jsonl_store.js +4 -1
- package/dist/store/jsonl_store.js.map +1 -1
- package/dist/store/kuzu_store.d.ts +13 -0
- package/dist/store/kuzu_store.d.ts.map +1 -1
- package/dist/store/kuzu_store.js +29 -0
- package/dist/store/kuzu_store.js.map +1 -1
- package/dist/store/output_folder.d.ts +43 -0
- package/dist/store/output_folder.d.ts.map +1 -0
- package/dist/store/output_folder.js +61 -0
- package/dist/store/output_folder.js.map +1 -0
- package/dotclaude_folder/commands/code-graph-interview.md +123 -0
- package/dotclaude_folder/commands/code-graph-optimize.md +65 -0
- package/dotclaude_folder/skills/code-graph-query/SKILL.md +4 -4
- package/package.json +72 -62
- package/contribs/web_visualisation/web/css/style.css +0 -219
- package/contribs/web_visualisation/web/tsconfig.json +0 -18
- /package/contribs/{web_visualisation/web/data → webview/web/js_autogenerated}/.gitignore +0 -0
|
@@ -37,10 +37,11 @@ const EDGE_COLORS = {
|
|
|
37
37
|
READS_CONFIG: '#65a30d',
|
|
38
38
|
CALLS_EXTERNAL: '#e11d48',
|
|
39
39
|
HANDLES: '#0ea5e9',
|
|
40
|
+
CALLS_RUNTIME: '#be123c',
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
/* One-line descriptions per node/edge kind, generated from src/schema into
|
|
43
|
-
|
|
44
|
+
js_autogenerated/kind_descriptions.js. Absent (empty) when that file has not been built. */
|
|
44
45
|
const KIND_DESCRIPTIONS = window.KIND_DESCRIPTIONS ?? { nodes: {}, edges: {} };
|
|
45
46
|
|
|
46
47
|
/* Heat ramp for runtime self-time: cool slate → yellow → red ("red = hot"). */
|
|
@@ -50,11 +51,11 @@ const HEAT_STOPS = [
|
|
|
50
51
|
{ at: 1, color: [220, 38, 38] },
|
|
51
52
|
];
|
|
52
53
|
|
|
53
|
-
/* Un-measured nodes render at a neutral baseline, distinct from a cheap-but-measured node. */
|
|
54
|
-
const RUNTIME_UNMEASURED_COLOR = '#243044';
|
|
55
|
-
const RUNTIME_UNMEASURED_BORDER = '#475569';
|
|
56
54
|
const HOTSPOTS_LIMIT = 12;
|
|
57
55
|
|
|
56
|
+
/* Persisted theme override ('light' | 'dark'); absent means follow the OS. */
|
|
57
|
+
const THEME_STORAGE_KEY = 'ktg.theme';
|
|
58
|
+
|
|
58
59
|
/** @type {AppState} */
|
|
59
60
|
const state = {
|
|
60
61
|
nodes: [],
|
|
@@ -62,13 +63,22 @@ const state = {
|
|
|
62
63
|
cy: undefined,
|
|
63
64
|
hiddenNodeKinds: new Set(),
|
|
64
65
|
hiddenEdgeKinds: new Set(),
|
|
66
|
+
hiddenCommunities: new Set(),
|
|
65
67
|
hideIsolated: false,
|
|
66
68
|
onlyMeasured: false,
|
|
67
69
|
droppedFiles: { nodes: undefined, edges: undefined },
|
|
68
70
|
encoding: 'structural',
|
|
69
71
|
runtime: { maxSelfMs: 0, measuredCount: 0, totalSelfMs: 0 },
|
|
72
|
+
communities: [],
|
|
73
|
+
communityLabels: new Map(),
|
|
70
74
|
};
|
|
71
75
|
|
|
76
|
+
/* Register the fcose layout extension (loaded as a CDN global, see index.html) so the
|
|
77
|
+
label-aware force layout is selectable. Guarded so a missing script never breaks the viewer. */
|
|
78
|
+
if (window.cytoscapeFcose !== undefined) {
|
|
79
|
+
cytoscape.use(window.cytoscapeFcose);
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
/**
|
|
73
83
|
* Looks up a required element by id, throwing when it is absent so a missing
|
|
74
84
|
* template node fails loudly here instead of as a later `null` dereference.
|
|
@@ -122,18 +132,32 @@ const asInput = (target) => {
|
|
|
122
132
|
return target;
|
|
123
133
|
};
|
|
124
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Narrows a change-event target to a `<select>` so `.value` can be read inside
|
|
137
|
+
* the encoding-selector handler.
|
|
138
|
+
* @param {EventTarget | null} target
|
|
139
|
+
* @returns {HTMLSelectElement}
|
|
140
|
+
*/
|
|
141
|
+
const asSelect = (target) => {
|
|
142
|
+
if ((target instanceof HTMLSelectElement) === false) {
|
|
143
|
+
throw new Error('event target is not a select');
|
|
144
|
+
}
|
|
145
|
+
return target;
|
|
146
|
+
};
|
|
147
|
+
|
|
125
148
|
/* ---------- data loading ---------- */
|
|
126
149
|
|
|
127
150
|
function boot() {
|
|
128
151
|
setupDropzone();
|
|
129
152
|
setupFolds();
|
|
153
|
+
setupTheme();
|
|
130
154
|
el('hide-isolated').addEventListener('change', (event) => {
|
|
131
155
|
state.hideIsolated = asInput(event.target).checked;
|
|
132
156
|
applyFilters();
|
|
133
157
|
});
|
|
134
158
|
el('relayout').addEventListener('click', () => runLayout());
|
|
135
|
-
el('
|
|
136
|
-
state.encoding =
|
|
159
|
+
el('encoding-select').addEventListener('change', (event) => {
|
|
160
|
+
state.encoding = encodingFromValue(asSelect(event.target).value);
|
|
137
161
|
if (state.cy !== undefined) {
|
|
138
162
|
state.cy.style(cyStyle());
|
|
139
163
|
}
|
|
@@ -166,12 +190,12 @@ function boot() {
|
|
|
166
190
|
async function tryFetch() {
|
|
167
191
|
try {
|
|
168
192
|
const [nodesText, edgesText] = await Promise.all([
|
|
169
|
-
fetch('
|
|
170
|
-
fetch('
|
|
193
|
+
fetch('../../../.ts_knowledge_graph/graph/nodes.jsonl').then((r) => r.ok === true ? r.text() : Promise.reject(new Error(String(r.status)))),
|
|
194
|
+
fetch('../../../.ts_knowledge_graph/graph/edges.jsonl').then((r) => r.ok === true ? r.text() : Promise.reject(new Error(String(r.status)))),
|
|
171
195
|
]);
|
|
172
|
-
setData(parseJsonl(nodesText), parseJsonl(edgesText), 'fetched
|
|
196
|
+
setData(parseJsonl(nodesText), parseJsonl(edgesText), 'fetched ../../../.ts_knowledge_graph/graph/*.jsonl');
|
|
173
197
|
} catch {
|
|
174
|
-
el('status').textContent = 'no data — generate
|
|
198
|
+
el('status').textContent = 'no data — generate js_autogenerated/graph_data.js or drop the JSONL files here';
|
|
175
199
|
}
|
|
176
200
|
}
|
|
177
201
|
|
|
@@ -272,6 +296,83 @@ function setupFolds() {
|
|
|
272
296
|
}
|
|
273
297
|
}
|
|
274
298
|
|
|
299
|
+
/* ---------- theme ---------- */
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Reads a CSS custom property off the document root, trimmed. The Cytoscape
|
|
303
|
+
* style pulls its theme-dependent colours from the same variables the stylesheet
|
|
304
|
+
* uses, so switching theme is a single attribute flip plus a graph re-style.
|
|
305
|
+
* @param {string} name
|
|
306
|
+
* @returns {string}
|
|
307
|
+
*/
|
|
308
|
+
function cssVar(name) {
|
|
309
|
+
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Reads the persisted theme override, or `null` when none is set or storage is unavailable.
|
|
314
|
+
* @returns {'light' | 'dark' | null}
|
|
315
|
+
*/
|
|
316
|
+
function storedTheme() {
|
|
317
|
+
try {
|
|
318
|
+
const value = localStorage.getItem(THEME_STORAGE_KEY);
|
|
319
|
+
return value === 'light' || value === 'dark' ? value : null;
|
|
320
|
+
} catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Resolves the active theme: an explicit stored choice wins, otherwise the OS
|
|
327
|
+
* `prefers-color-scheme`, otherwise dark.
|
|
328
|
+
* @returns {'light' | 'dark'}
|
|
329
|
+
*/
|
|
330
|
+
function resolveTheme() {
|
|
331
|
+
const stored = storedTheme();
|
|
332
|
+
if (stored !== null) {
|
|
333
|
+
return stored;
|
|
334
|
+
}
|
|
335
|
+
return window.matchMedia('(prefers-color-scheme: light)').matches === true ? 'light' : 'dark';
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Applies a theme: flips the `data-theme` attribute the stylesheet keys off,
|
|
340
|
+
* updates the toggle glyph, and re-styles the graph so its canvas-drawn colours
|
|
341
|
+
* (labels, selection ring, node borders) track the theme.
|
|
342
|
+
* @param {'light' | 'dark'} theme
|
|
343
|
+
*/
|
|
344
|
+
function applyTheme(theme) {
|
|
345
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
346
|
+
const toggle = el('theme-toggle');
|
|
347
|
+
toggle.textContent = theme === 'dark' ? '☀' : '☾';
|
|
348
|
+
toggle.title = theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme';
|
|
349
|
+
if (state.cy !== undefined) {
|
|
350
|
+
state.cy.style(cyStyle());
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Wires the theme toggle: clicking persists and applies the opposite theme, and
|
|
356
|
+
* — while no explicit choice is stored — the viewer follows later OS changes.
|
|
357
|
+
*/
|
|
358
|
+
function setupTheme() {
|
|
359
|
+
applyTheme(resolveTheme());
|
|
360
|
+
el('theme-toggle').addEventListener('click', () => {
|
|
361
|
+
const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
|
362
|
+
try {
|
|
363
|
+
localStorage.setItem(THEME_STORAGE_KEY, next);
|
|
364
|
+
} catch {
|
|
365
|
+
/* storage unavailable (private mode, file://) — apply for this session only */
|
|
366
|
+
}
|
|
367
|
+
applyTheme(next);
|
|
368
|
+
});
|
|
369
|
+
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (event) => {
|
|
370
|
+
if (storedTheme() === null) {
|
|
371
|
+
applyTheme(event.matches === true ? 'light' : 'dark');
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
275
376
|
/* ---------- graph construction ---------- */
|
|
276
377
|
|
|
277
378
|
/**
|
|
@@ -305,11 +406,13 @@ function setData(nodes, edges, sourceLabel) {
|
|
|
305
406
|
maxSelfMs = Math.max(maxSelfMs, selfMs);
|
|
306
407
|
}
|
|
307
408
|
state.runtime = { maxSelfMs, measuredCount, totalSelfMs };
|
|
409
|
+
state.communities = communityCounts(nodes);
|
|
410
|
+
state.communityLabels = communityLabels(nodes);
|
|
308
411
|
|
|
309
412
|
const elements = [
|
|
310
413
|
...nodes.map((node) => ({
|
|
311
414
|
group: 'nodes',
|
|
312
|
-
data: { id: node.id, name: node.name, kind: node.kind, filePath: node.filePath, startLine: node.range === undefined ? 0 : node.range.startLine, exported: node.exported === true, degree: degree.get(node.id) ?? 0, runtime: nodeRuntime(node) },
|
|
415
|
+
data: { id: node.id, name: node.name, kind: node.kind, filePath: node.filePath, startLine: node.range === undefined ? 0 : node.range.startLine, exported: node.exported === true, degree: degree.get(node.id) ?? 0, runtime: nodeRuntime(node), community: nodeCommunity(node) },
|
|
313
416
|
})),
|
|
314
417
|
...edges
|
|
315
418
|
.filter((edge) => nodeIds.has(edge.from) === true && nodeIds.has(edge.to) === true)
|
|
@@ -326,7 +429,7 @@ function setData(nodes, edges, sourceLabel) {
|
|
|
326
429
|
container: el('cy'),
|
|
327
430
|
elements,
|
|
328
431
|
style: cyStyle(),
|
|
329
|
-
layout:
|
|
432
|
+
layout: layoutOptions('fcose'),
|
|
330
433
|
});
|
|
331
434
|
state.cy.on('tap', 'node', (event) => select(event.target));
|
|
332
435
|
state.cy.on('tap', (event) => {
|
|
@@ -337,21 +440,31 @@ function setData(nodes, edges, sourceLabel) {
|
|
|
337
440
|
|
|
338
441
|
buildLegends();
|
|
339
442
|
renderRuntime();
|
|
443
|
+
renderCommunities();
|
|
444
|
+
syncEncodingOptions();
|
|
340
445
|
applyFilters();
|
|
341
446
|
el('status').textContent = `${sourceLabel} — ${nodes.length} nodes, ${edges.length} edges`;
|
|
342
447
|
}
|
|
343
448
|
|
|
344
449
|
function cyStyle() {
|
|
450
|
+
const unmeasuredFill = cssVar('--unmeasured-fill');
|
|
451
|
+
const unmeasuredBorder = cssVar('--unmeasured-border');
|
|
452
|
+
const nodeBorder = cssVar('--graph-node-border');
|
|
453
|
+
const nodeBorderWidth = parseFloat(cssVar('--graph-node-border-width')) || 0;
|
|
454
|
+
const labelColor = cssVar('--graph-label');
|
|
455
|
+
const labelBg = cssVar('--graph-label-bg');
|
|
456
|
+
const selBorder = cssVar('--graph-sel-border');
|
|
345
457
|
/** @param {CyCollection} node */
|
|
346
458
|
const nodeColor = (node) => {
|
|
347
|
-
if (state.encoding
|
|
348
|
-
|
|
459
|
+
if (state.encoding === 'runtime') {
|
|
460
|
+
const runtime = node.data('runtime');
|
|
461
|
+
return runtime === undefined || runtime === null ? unmeasuredFill : heatColor(runtimeFraction(runtime.selfMs));
|
|
349
462
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
return
|
|
463
|
+
if (state.encoding === 'community') {
|
|
464
|
+
const community = node.data('community');
|
|
465
|
+
return community === undefined || community === null ? unmeasuredFill : communityColor(community);
|
|
353
466
|
}
|
|
354
|
-
return
|
|
467
|
+
return NODE_COLORS[node.data('kind')] ?? '#9ca3af';
|
|
355
468
|
};
|
|
356
469
|
/** @param {CyCollection} node */
|
|
357
470
|
const nodeSize = (node) => {
|
|
@@ -364,8 +477,15 @@ function cyStyle() {
|
|
|
364
477
|
}
|
|
365
478
|
return 12 + runtimeFraction(runtime.selfMs) * 40;
|
|
366
479
|
};
|
|
367
|
-
/**
|
|
368
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Whether the active encoding has no value for this node — un-measured in
|
|
482
|
+
* runtime mode, or unassigned to a community in community mode. Such nodes get
|
|
483
|
+
* the muted fill and a dashed border so the gap reads as "no data", not a colour.
|
|
484
|
+
* @param {CyCollection} node
|
|
485
|
+
*/
|
|
486
|
+
const isUnencoded = (node) =>
|
|
487
|
+
(state.encoding === 'runtime' && (node.data('runtime') === undefined || node.data('runtime') === null))
|
|
488
|
+
|| (state.encoding === 'community' && (node.data('community') === undefined || node.data('community') === null));
|
|
369
489
|
return [
|
|
370
490
|
{
|
|
371
491
|
selector: 'node',
|
|
@@ -373,15 +493,19 @@ function cyStyle() {
|
|
|
373
493
|
'background-color': nodeColor,
|
|
374
494
|
'width': nodeSize,
|
|
375
495
|
'height': nodeSize,
|
|
376
|
-
'border-width': (/** @type {CyCollection} */ node) =>
|
|
377
|
-
'border-color':
|
|
378
|
-
'border-style': 'dashed',
|
|
496
|
+
'border-width': (/** @type {CyCollection} */ node) => isUnencoded(node) === true ? 1 : nodeBorderWidth,
|
|
497
|
+
'border-color': (/** @type {CyCollection} */ node) => isUnencoded(node) === true ? unmeasuredBorder : nodeBorder,
|
|
498
|
+
'border-style': (/** @type {CyCollection} */ node) => isUnencoded(node) === true ? 'dashed' : 'solid',
|
|
379
499
|
'label': 'data(name)',
|
|
380
|
-
'color':
|
|
500
|
+
'color': labelColor,
|
|
381
501
|
'font-size': 8,
|
|
382
502
|
'min-zoomed-font-size': 7,
|
|
383
503
|
'text-valign': 'bottom',
|
|
384
504
|
'text-margin-y': 3,
|
|
505
|
+
'text-background-color': labelBg,
|
|
506
|
+
'text-background-opacity': 0.5,
|
|
507
|
+
'text-background-shape': 'roundrectangle',
|
|
508
|
+
'text-background-padding': 2,
|
|
385
509
|
},
|
|
386
510
|
},
|
|
387
511
|
{
|
|
@@ -397,21 +521,36 @@ function cyStyle() {
|
|
|
397
521
|
},
|
|
398
522
|
},
|
|
399
523
|
{ selector: '.hidden', style: { display: 'none' } },
|
|
400
|
-
{ selector: '.faded', style: { opacity: 0.08, 'text-opacity': 0 } },
|
|
401
|
-
{ selector: 'node.sel', style: { 'border-width': 3, 'border-color':
|
|
524
|
+
{ selector: '.faded', style: { opacity: 0.08, 'text-opacity': 0, 'text-background-opacity': 0 } },
|
|
525
|
+
{ selector: 'node.sel', style: { 'border-width': 3, 'border-color': selBorder, 'border-style': 'solid' } },
|
|
402
526
|
];
|
|
403
527
|
}
|
|
404
528
|
|
|
529
|
+
/**
|
|
530
|
+
* Builds Cytoscape layout options for the given layout name. The force layouts
|
|
531
|
+
* (`fcose`, `cose`) are made label-aware via `nodeDimensionsIncludeLabels`, so each
|
|
532
|
+
* node's label box is factored into spacing and labels overlap their neighbours less.
|
|
533
|
+
* @param {string} name
|
|
534
|
+
* @returns {Record<string, unknown>}
|
|
535
|
+
*/
|
|
536
|
+
function layoutOptions(name) {
|
|
537
|
+
const base = { name, animate: false, padding: 30 };
|
|
538
|
+
if (name === 'fcose' || name === 'cose') {
|
|
539
|
+
return { ...base, nodeDimensionsIncludeLabels: true };
|
|
540
|
+
}
|
|
541
|
+
if (name === 'concentric') {
|
|
542
|
+
return { ...base, concentric: (/** @type {CyCollection} */ node) => node.degree(), levelWidth: () => 2 };
|
|
543
|
+
}
|
|
544
|
+
return base;
|
|
545
|
+
}
|
|
546
|
+
|
|
405
547
|
function runLayout() {
|
|
406
548
|
const cy = state.cy;
|
|
407
549
|
if (cy === undefined) {
|
|
408
550
|
return;
|
|
409
551
|
}
|
|
410
552
|
const name = selectEl('layout-select').value;
|
|
411
|
-
|
|
412
|
-
? { name, concentric: (/** @type {CyCollection} */ node) => node.degree(), levelWidth: () => 2, animate: false, padding: 30 }
|
|
413
|
-
: { name, animate: false, padding: 30 };
|
|
414
|
-
cy.elements(':visible').layout(options).run();
|
|
553
|
+
cy.elements(':visible').layout(layoutOptions(name)).run();
|
|
415
554
|
}
|
|
416
555
|
|
|
417
556
|
/* ---------- edge weighting ---------- */
|
|
@@ -612,7 +751,9 @@ function applyFilters() {
|
|
|
612
751
|
const hiddenByKind = state.hiddenNodeKinds.has(node.data('kind')) === true;
|
|
613
752
|
const unmeasured = node.data('runtime') === undefined || node.data('runtime') === null;
|
|
614
753
|
const hiddenByMeasure = state.onlyMeasured === true && unmeasured === true;
|
|
615
|
-
node.
|
|
754
|
+
const community = node.data('community');
|
|
755
|
+
const hiddenByCommunity = community !== undefined && community !== null && state.hiddenCommunities.has(community) === true;
|
|
756
|
+
node.toggleClass('hidden', hiddenByKind === true || hiddenByMeasure === true || hiddenByCommunity === true);
|
|
616
757
|
});
|
|
617
758
|
cy.edges().forEach((edge) => {
|
|
618
759
|
edge.toggleClass('hidden', state.hiddenEdgeKinds.has(edge.data('kind')) === true);
|
|
@@ -729,7 +870,6 @@ function focusNode(id) {
|
|
|
729
870
|
/** Renders the coverage line and the ranked hotspots list from the loaded runtime metrics. */
|
|
730
871
|
function renderRuntime() {
|
|
731
872
|
const section = el('runtime');
|
|
732
|
-
const toggle = inputEl('runtime-heat');
|
|
733
873
|
const measured = state.nodes
|
|
734
874
|
.map((node) => ({ node, runtime: nodeRuntime(node) }))
|
|
735
875
|
.filter((entry) => entry.runtime !== undefined)
|
|
@@ -738,20 +878,13 @@ function renderRuntime() {
|
|
|
738
878
|
if (measured.length === 0) {
|
|
739
879
|
section.classList.add('empty');
|
|
740
880
|
el('coverage').textContent = 'no runtime data — run `enrich` to measure self-time';
|
|
741
|
-
toggle.checked = false;
|
|
742
|
-
toggle.disabled = true;
|
|
743
|
-
state.encoding = 'structural';
|
|
744
881
|
state.onlyMeasured = false;
|
|
745
882
|
inputEl('only-measured').checked = false;
|
|
746
883
|
el('hotspots').innerHTML = '';
|
|
747
|
-
if (state.cy !== undefined) {
|
|
748
|
-
state.cy.style(cyStyle());
|
|
749
|
-
}
|
|
750
884
|
return;
|
|
751
885
|
}
|
|
752
886
|
|
|
753
887
|
section.classList.remove('empty');
|
|
754
|
-
toggle.disabled = false;
|
|
755
888
|
inputEl('only-measured').disabled = false;
|
|
756
889
|
el('coverage').textContent = `${state.runtime.measuredCount} / ${state.nodes.length} nodes measured · ${formatMs(state.runtime.totalSelfMs)} total self-time`;
|
|
757
890
|
|
|
@@ -766,6 +899,212 @@ function renderRuntime() {
|
|
|
766
899
|
}
|
|
767
900
|
}
|
|
768
901
|
|
|
902
|
+
/* ---------- community ---------- */
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Reads the integer community index `cluster` attaches as `metadata.community`,
|
|
906
|
+
* or `undefined` when the graph has not been clustered.
|
|
907
|
+
* @param {RawNode} node
|
|
908
|
+
* @returns {number | undefined}
|
|
909
|
+
*/
|
|
910
|
+
function nodeCommunity(node) {
|
|
911
|
+
if (node.metadata === undefined || node.metadata === null) {
|
|
912
|
+
return undefined;
|
|
913
|
+
}
|
|
914
|
+
const community = node.metadata.community;
|
|
915
|
+
return typeof community === 'number' ? community : undefined;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Reads the human-readable community label `cluster` attaches as
|
|
920
|
+
* `metadata.communityLabel`. Every clustered node carries one, written alongside
|
|
921
|
+
* its community index, so this is defined whenever {@link nodeCommunity} is.
|
|
922
|
+
* @param {RawNode} node
|
|
923
|
+
* @returns {string | undefined}
|
|
924
|
+
*/
|
|
925
|
+
function nodeCommunityLabel(node) {
|
|
926
|
+
if (node.metadata === undefined || node.metadata === null) {
|
|
927
|
+
return undefined;
|
|
928
|
+
}
|
|
929
|
+
const label = node.metadata.communityLabel;
|
|
930
|
+
return typeof label === 'string' ? label : undefined;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* A stable, theme-independent colour per community index, spread around the hue
|
|
935
|
+
* circle by the golden angle so adjacent indices stay distinct. Fixed
|
|
936
|
+
* saturation/lightness keep it legible on both the light and dark canvas, like
|
|
937
|
+
* the kind palette.
|
|
938
|
+
* @param {number} index
|
|
939
|
+
* @returns {string}
|
|
940
|
+
*/
|
|
941
|
+
function communityColor(index) {
|
|
942
|
+
const hue = Math.round((index * 137.508) % 360);
|
|
943
|
+
return `hsl(${hue}, 65%, 55%)`;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Counts members per community across the loaded nodes, as `[index, count]`
|
|
948
|
+
* pairs sorted by size descending (the order `cluster` reports them in).
|
|
949
|
+
* @param {RawNode[]} nodes
|
|
950
|
+
* @returns {[number, number][]}
|
|
951
|
+
*/
|
|
952
|
+
function communityCounts(nodes) {
|
|
953
|
+
/** @type {Map<number, number>} */
|
|
954
|
+
const counts = new Map();
|
|
955
|
+
for (const node of nodes) {
|
|
956
|
+
const community = nodeCommunity(node);
|
|
957
|
+
if (community !== undefined) {
|
|
958
|
+
counts.set(community, (counts.get(community) ?? 0) + 1);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Maps each community index to its label, read from the first member node seen —
|
|
966
|
+
* `cluster` writes the same label onto every member, so one read per community
|
|
967
|
+
* suffices.
|
|
968
|
+
* @param {RawNode[]} nodes
|
|
969
|
+
* @returns {Map<number, string>}
|
|
970
|
+
*/
|
|
971
|
+
function communityLabels(nodes) {
|
|
972
|
+
/** @type {Map<number, string>} */
|
|
973
|
+
const labels = new Map();
|
|
974
|
+
for (const node of nodes) {
|
|
975
|
+
const community = nodeCommunity(node);
|
|
976
|
+
if (community === undefined || labels.has(community) === true) {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const label = nodeCommunityLabel(node);
|
|
980
|
+
if (label !== undefined) {
|
|
981
|
+
labels.set(community, label);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return labels;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Renders the Communities legend as visibility filters — a checkbox + swatch +
|
|
989
|
+
* member count per community, plus a master "all" toggle — so a community can be
|
|
990
|
+
* shown or hidden on the graph, mirroring the node/edge kind legends. The section
|
|
991
|
+
* is hidden when the graph is un-clustered.
|
|
992
|
+
*/
|
|
993
|
+
function renderCommunities() {
|
|
994
|
+
const section = el('communities');
|
|
995
|
+
const container = el('community-legend');
|
|
996
|
+
container.innerHTML = '';
|
|
997
|
+
if (state.communities.length === 0) {
|
|
998
|
+
section.classList.add('empty');
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
section.classList.remove('empty');
|
|
1002
|
+
|
|
1003
|
+
const indices = state.communities.map(([index]) => index);
|
|
1004
|
+
/** @type {HTMLInputElement[]} */
|
|
1005
|
+
const childCheckboxes = [];
|
|
1006
|
+
|
|
1007
|
+
const master = document.createElement('input');
|
|
1008
|
+
master.type = 'checkbox';
|
|
1009
|
+
const syncMaster = () => {
|
|
1010
|
+
const hiddenCount = indices.filter((index) => state.hiddenCommunities.has(index) === true).length;
|
|
1011
|
+
master.checked = hiddenCount === 0;
|
|
1012
|
+
master.indeterminate = hiddenCount > 0 && hiddenCount < indices.length;
|
|
1013
|
+
};
|
|
1014
|
+
master.addEventListener('change', () => {
|
|
1015
|
+
const allVisible = indices.every((index) => state.hiddenCommunities.has(index) === false);
|
|
1016
|
+
for (const index of indices) {
|
|
1017
|
+
if (allVisible === true) {
|
|
1018
|
+
state.hiddenCommunities.add(index);
|
|
1019
|
+
} else {
|
|
1020
|
+
state.hiddenCommunities.delete(index);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
for (const child of childCheckboxes) {
|
|
1024
|
+
child.checked = state.hiddenCommunities.has(Number(child.dataset.community)) === false;
|
|
1025
|
+
}
|
|
1026
|
+
syncMaster();
|
|
1027
|
+
applyFilters();
|
|
1028
|
+
});
|
|
1029
|
+
const masterLabel = document.createElement('label');
|
|
1030
|
+
masterLabel.className = 'master';
|
|
1031
|
+
masterLabel.title = 'show or hide every community';
|
|
1032
|
+
const spacer = document.createElement('span');
|
|
1033
|
+
spacer.className = 'swatch spacer';
|
|
1034
|
+
const masterText = document.createElement('span');
|
|
1035
|
+
masterText.textContent = 'all';
|
|
1036
|
+
masterLabel.append(master, spacer, masterText);
|
|
1037
|
+
container.appendChild(masterLabel);
|
|
1038
|
+
|
|
1039
|
+
for (const [index, count] of state.communities) {
|
|
1040
|
+
const row = document.createElement('label');
|
|
1041
|
+
const checkbox = document.createElement('input');
|
|
1042
|
+
checkbox.type = 'checkbox';
|
|
1043
|
+
checkbox.dataset.community = String(index);
|
|
1044
|
+
checkbox.checked = state.hiddenCommunities.has(index) === false;
|
|
1045
|
+
checkbox.addEventListener('change', () => {
|
|
1046
|
+
if (checkbox.checked === true) {
|
|
1047
|
+
state.hiddenCommunities.delete(index);
|
|
1048
|
+
} else {
|
|
1049
|
+
state.hiddenCommunities.add(index);
|
|
1050
|
+
}
|
|
1051
|
+
syncMaster();
|
|
1052
|
+
applyFilters();
|
|
1053
|
+
});
|
|
1054
|
+
childCheckboxes.push(checkbox);
|
|
1055
|
+
const swatch = document.createElement('span');
|
|
1056
|
+
swatch.className = 'swatch';
|
|
1057
|
+
swatch.style.background = communityColor(index);
|
|
1058
|
+
const text = document.createElement('span');
|
|
1059
|
+
text.textContent = /** @type {string} */ (state.communityLabels.get(index));
|
|
1060
|
+
const countSpan = document.createElement('span');
|
|
1061
|
+
countSpan.className = 'count';
|
|
1062
|
+
countSpan.textContent = String(count);
|
|
1063
|
+
row.append(checkbox, swatch, text, countSpan);
|
|
1064
|
+
container.appendChild(row);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
syncMaster();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Enables the `self-time` and `community` colour modes only when the loaded
|
|
1072
|
+
* graph carries that data, falls back to `structural` if the active mode lost
|
|
1073
|
+
* its data, mirrors the choice into the `<select>`, and re-applies the style.
|
|
1074
|
+
*/
|
|
1075
|
+
function syncEncodingOptions() {
|
|
1076
|
+
const select = selectEl('encoding-select');
|
|
1077
|
+
/**
|
|
1078
|
+
* @param {string} value
|
|
1079
|
+
* @param {boolean} enabled
|
|
1080
|
+
*/
|
|
1081
|
+
const setEnabled = (value, enabled) => {
|
|
1082
|
+
const option = select.querySelector(`option[value="${value}"]`);
|
|
1083
|
+
if (option instanceof HTMLOptionElement) {
|
|
1084
|
+
option.disabled = enabled === false;
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
setEnabled('runtime', state.runtime.measuredCount > 0);
|
|
1088
|
+
setEnabled('community', state.communities.length > 0);
|
|
1089
|
+
if ((state.encoding === 'runtime' && state.runtime.measuredCount === 0)
|
|
1090
|
+
|| (state.encoding === 'community' && state.communities.length === 0)) {
|
|
1091
|
+
state.encoding = 'structural';
|
|
1092
|
+
}
|
|
1093
|
+
select.value = state.encoding;
|
|
1094
|
+
if (state.cy !== undefined) {
|
|
1095
|
+
state.cy.style(cyStyle());
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Narrows an arbitrary `<select>` value to a known encoding mode, defaulting to `structural`.
|
|
1101
|
+
* @param {string} value
|
|
1102
|
+
* @returns {'structural' | 'runtime' | 'community'}
|
|
1103
|
+
*/
|
|
1104
|
+
function encodingFromValue(value) {
|
|
1105
|
+
return value === 'runtime' || value === 'community' ? value : 'structural';
|
|
1106
|
+
}
|
|
1107
|
+
|
|
769
1108
|
/* ---------- search ---------- */
|
|
770
1109
|
|
|
771
1110
|
function renderSearchResults() {
|
package/contribs/{web_visualisation/web/data → webview/web/js_autogenerated}/kind_descriptions.js
RENAMED
|
@@ -33,6 +33,7 @@ window.KIND_DESCRIPTIONS = {
|
|
|
33
33
|
"WRITES": "The source assigns to the target variable or property.",
|
|
34
34
|
"READS_CONFIG": "The source reads the target configuration flag (an environment variable).",
|
|
35
35
|
"CALLS_EXTERNAL": "The source makes an outbound HTTP call to the target external API.",
|
|
36
|
-
"HANDLES": "Links an HTTP endpoint to the function that handles it (route to handler)."
|
|
36
|
+
"HANDLES": "Links an HTTP endpoint to the function that handles it (route to handler).",
|
|
37
|
+
"CALLS_RUNTIME": "A call observed at runtime in a CPU profile: the source function or method was on the stack directly above the target. Captures dynamic dispatch that static CALLS cannot see."
|
|
37
38
|
}
|
|
38
39
|
};
|
|
@@ -33,7 +33,7 @@ interface RawNode {
|
|
|
33
33
|
filePath: string;
|
|
34
34
|
range?: RawRange;
|
|
35
35
|
exported?: boolean;
|
|
36
|
-
metadata?: { runtime?: NodeRuntime | null; [key: string]: unknown } | null;
|
|
36
|
+
metadata?: { runtime?: NodeRuntime | null; community?: number | null; communityLabel?: string | null; [key: string]: unknown } | null;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/** A graph edge as serialised in `edges.jsonl`; mirrors `GraphEdgeSchema`. */
|
|
@@ -57,7 +57,7 @@ interface KindDescriptions {
|
|
|
57
57
|
edges: Record<string, string>;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/** GitHub permalink descriptor; mirrors `GitHubSource` in `src/commands/
|
|
60
|
+
/** GitHub permalink descriptor; mirrors `GitHubSource` in `src/commands/webview_command.ts`. */
|
|
61
61
|
interface GitHubSource {
|
|
62
62
|
baseUrl: string;
|
|
63
63
|
commit: string;
|
|
@@ -76,11 +76,14 @@ interface AppState {
|
|
|
76
76
|
cy: CyCore | undefined;
|
|
77
77
|
hiddenNodeKinds: Set<string>;
|
|
78
78
|
hiddenEdgeKinds: Set<string>;
|
|
79
|
+
hiddenCommunities: Set<number>;
|
|
79
80
|
hideIsolated: boolean;
|
|
80
81
|
onlyMeasured: boolean;
|
|
81
82
|
droppedFiles: { nodes: RawNode[] | undefined; edges: RawEdge[] | undefined };
|
|
82
|
-
encoding: 'structural' | 'runtime';
|
|
83
|
+
encoding: 'structural' | 'runtime' | 'community';
|
|
83
84
|
runtime: { maxSelfMs: number; measuredCount: number; totalSelfMs: number };
|
|
85
|
+
communities: [number, number][];
|
|
86
|
+
communityLabels: Map<number, string>;
|
|
84
87
|
}
|
|
85
88
|
|
|
86
89
|
/* ---------- Cytoscape.js (loaded as a CDN global, untyped) ---------- */
|
|
@@ -136,6 +139,10 @@ interface CytoscapeOptions {
|
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
declare function cytoscape(options?: CytoscapeOptions): CyCore;
|
|
142
|
+
declare namespace cytoscape {
|
|
143
|
+
/** Registers a Cytoscape extension (such as the fcose layout) loaded as a CDN global. */
|
|
144
|
+
function use(extension: unknown): void;
|
|
145
|
+
}
|
|
139
146
|
|
|
140
147
|
/* ---------- globals injected into the page ---------- */
|
|
141
148
|
|
|
@@ -143,4 +150,5 @@ interface Window {
|
|
|
143
150
|
GRAPH_DATA?: GraphData;
|
|
144
151
|
KIND_DESCRIPTIONS?: KindDescriptions;
|
|
145
152
|
GRAPH_SOURCE?: GraphSource | null;
|
|
153
|
+
cytoscapeFcose?: unknown;
|
|
146
154
|
}
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAqBA,qBAAa,GAAG;IACf,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;CA2BhC"}
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { BenchmarkCommand } from './commands/benchmark_command.js';
|
|
4
4
|
import { BlastRadiusCommand } from './commands/blast_radius_command.js';
|
|
5
5
|
import { CallsCommand } from './commands/calls_command.js';
|
|
6
|
+
import { ClusterCommand } from './commands/cluster_command.js';
|
|
6
7
|
import { CostCommand } from './commands/cost_command.js';
|
|
7
8
|
import { DeadExportsCommand } from './commands/dead_exports_command.js';
|
|
8
9
|
import { EnrichCommand } from './commands/enrich_command.js';
|
|
@@ -13,8 +14,9 @@ import { InstallCommand } from './commands/install_command.js';
|
|
|
13
14
|
import { LoadCommand } from './commands/load_command.js';
|
|
14
15
|
import { NeighborsCommand } from './commands/neighbors_command.js';
|
|
15
16
|
import { ReferencesCommand } from './commands/references_command.js';
|
|
17
|
+
import { ReportCommand } from './commands/report_command.js';
|
|
16
18
|
import { VerifyCommand } from './commands/verify_command.js';
|
|
17
|
-
import {
|
|
19
|
+
import { WebviewCommand } from './commands/webview_command.js';
|
|
18
20
|
import { WhoCallsCommand } from './commands/who_calls_command.js';
|
|
19
21
|
export class Cli {
|
|
20
22
|
static run(argv) {
|
|
@@ -25,6 +27,7 @@ export class Cli {
|
|
|
25
27
|
ExtractCommand.register(program);
|
|
26
28
|
LoadCommand.register(program);
|
|
27
29
|
EnrichCommand.register(program);
|
|
30
|
+
ClusterCommand.register(program);
|
|
28
31
|
FindCommand.register(program);
|
|
29
32
|
WhoCallsCommand.register(program);
|
|
30
33
|
CallsCommand.register(program);
|
|
@@ -36,7 +39,8 @@ export class Cli {
|
|
|
36
39
|
BlastRadiusCommand.register(program);
|
|
37
40
|
NeighborsCommand.register(program);
|
|
38
41
|
ReferencesCommand.register(program);
|
|
39
|
-
|
|
42
|
+
ReportCommand.register(program);
|
|
43
|
+
WebviewCommand.register(program);
|
|
40
44
|
InstallCommand.register(program);
|
|
41
45
|
void program.parseAsync(argv);
|
|
42
46
|
}
|