monarch-code-graph 0.3.4 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +2632 -96
- package/dist/index.d.ts +33 -6
- package/dist/index.js +1552 -7
- package/package.json +7 -5
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/analyze.d.ts +0 -8
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js +0 -76
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/diff.d.ts +0 -8
- package/dist/commands/diff.d.ts.map +0 -1
- package/dist/commands/diff.js +0 -203
- package/dist/commands/diff.js.map +0 -1
- package/dist/commands/graph.d.ts +0 -12
- package/dist/commands/graph.d.ts.map +0 -1
- package/dist/commands/graph.js +0 -72
- package/dist/commands/graph.js.map +0 -1
- package/dist/commands/help.d.ts +0 -2
- package/dist/commands/help.d.ts.map +0 -1
- package/dist/commands/help.js +0 -234
- package/dist/commands/help.js.map +0 -1
- package/dist/commands/init.d.ts +0 -6
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -37
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/list.d.ts +0 -7
- package/dist/commands/list.d.ts.map +0 -1
- package/dist/commands/list.js +0 -148
- package/dist/commands/list.js.map +0 -1
- package/dist/commands/run.d.ts +0 -10
- package/dist/commands/run.d.ts.map +0 -1
- package/dist/commands/run.js +0 -388
- package/dist/commands/run.js.map +0 -1
- package/dist/commands/serve.d.ts +0 -8
- package/dist/commands/serve.d.ts.map +0 -1
- package/dist/commands/serve.js +0 -137
- package/dist/commands/serve.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/utils/ascii-graph.d.ts +0 -21
- package/dist/utils/ascii-graph.d.ts.map +0 -1
- package/dist/utils/ascii-graph.js +0 -431
- package/dist/utils/ascii-graph.js.map +0 -1
- package/dist/utils/output.d.ts +0 -12
- package/dist/utils/output.d.ts.map +0 -1
- package/dist/utils/output.js +0 -24
- package/dist/utils/output.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,100 +1,2636 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../shared/dist/schemas/artifact.js
|
|
13
|
+
function isCodeFlowArtifact(obj) {
|
|
14
|
+
if (typeof obj !== "object" || obj === null)
|
|
15
|
+
return false;
|
|
16
|
+
const artifact = obj;
|
|
17
|
+
return typeof artifact.metadata === "object" && typeof artifact.nodes === "object" && typeof artifact.edges === "object" && typeof artifact.layout === "object";
|
|
18
|
+
}
|
|
19
|
+
var init_artifact = __esm({
|
|
20
|
+
"../shared/dist/schemas/artifact.js"() {
|
|
21
|
+
"use strict";
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ../shared/dist/utils/hashing.js
|
|
26
|
+
function generateStableId(input) {
|
|
27
|
+
let hash = 0;
|
|
28
|
+
for (let i = 0; i < input.length; i++) {
|
|
29
|
+
const char = input.charCodeAt(i);
|
|
30
|
+
hash = (hash << 5) - hash + char;
|
|
31
|
+
hash = hash & hash;
|
|
32
|
+
}
|
|
33
|
+
const positiveHash = hash >>> 0;
|
|
34
|
+
return positiveHash.toString(16).padStart(8, "0");
|
|
35
|
+
}
|
|
36
|
+
function generateFunctionId(file, name, startLine) {
|
|
37
|
+
const input = `fn:${file}:${name}:${startLine}`;
|
|
38
|
+
return `fn-${generateStableId(input)}`;
|
|
39
|
+
}
|
|
40
|
+
function generateModuleId(path14) {
|
|
41
|
+
const input = `mod:${path14}`;
|
|
42
|
+
return `mod-${generateStableId(input)}`;
|
|
43
|
+
}
|
|
44
|
+
function generateComponentId(name) {
|
|
45
|
+
const input = `comp:${name}`;
|
|
46
|
+
return `comp-${generateStableId(input)}`;
|
|
47
|
+
}
|
|
48
|
+
var init_hashing = __esm({
|
|
49
|
+
"../shared/dist/utils/hashing.js"() {
|
|
50
|
+
"use strict";
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ../shared/dist/index.js
|
|
55
|
+
var init_dist = __esm({
|
|
56
|
+
"../shared/dist/index.js"() {
|
|
57
|
+
"use strict";
|
|
58
|
+
init_artifact();
|
|
59
|
+
init_hashing();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ../analyzer/dist/exporters/mock-exporter.js
|
|
64
|
+
import fs from "fs";
|
|
65
|
+
import path from "path";
|
|
66
|
+
var MockExporter;
|
|
67
|
+
var init_mock_exporter = __esm({
|
|
68
|
+
"../analyzer/dist/exporters/mock-exporter.js"() {
|
|
69
|
+
"use strict";
|
|
70
|
+
init_dist();
|
|
71
|
+
MockExporter = class {
|
|
72
|
+
targetPath;
|
|
73
|
+
files = [];
|
|
74
|
+
functions = [];
|
|
75
|
+
modules = [];
|
|
76
|
+
components = [];
|
|
77
|
+
callEdges = [];
|
|
78
|
+
dataflowEdges = [];
|
|
79
|
+
constructor(targetPath) {
|
|
80
|
+
this.targetPath = targetPath;
|
|
81
|
+
}
|
|
82
|
+
async prepare() {
|
|
83
|
+
this.files = this.scanTsFiles(this.targetPath);
|
|
84
|
+
console.log(`Found ${this.files.length} TypeScript files`);
|
|
85
|
+
}
|
|
86
|
+
async analyze() {
|
|
87
|
+
for (const file of this.files) {
|
|
88
|
+
const relativePath = path.relative(this.targetPath, file);
|
|
89
|
+
this.processFile(relativePath);
|
|
90
|
+
}
|
|
91
|
+
this.generateCallEdges();
|
|
92
|
+
this.generateDataflowEdges();
|
|
93
|
+
this.generateComponents();
|
|
94
|
+
}
|
|
95
|
+
async export() {
|
|
96
|
+
return {
|
|
97
|
+
metadata: {
|
|
98
|
+
analyzerVersion: "0.1.0",
|
|
99
|
+
language: "typescript",
|
|
100
|
+
repoCommit: this.generateCommitHash(),
|
|
101
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
102
|
+
configHash: this.generateConfigHash()
|
|
103
|
+
},
|
|
104
|
+
nodes: {
|
|
105
|
+
functions: this.sortById(this.functions),
|
|
106
|
+
modules: this.sortById(this.modules),
|
|
107
|
+
components: this.sortById(this.components)
|
|
108
|
+
},
|
|
109
|
+
edges: {
|
|
110
|
+
calls: this.sortEdges(this.callEdges),
|
|
111
|
+
dataflow: this.sortEdges(this.dataflowEdges)
|
|
112
|
+
},
|
|
113
|
+
layout: {
|
|
114
|
+
positions: {}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async cleanup() {
|
|
119
|
+
}
|
|
120
|
+
scanTsFiles(dir) {
|
|
121
|
+
const results = [];
|
|
122
|
+
if (!fs.existsSync(dir)) {
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
const stat = fs.statSync(dir);
|
|
126
|
+
if (stat.isFile() && (dir.endsWith(".ts") || dir.endsWith(".tsx"))) {
|
|
127
|
+
if (!dir.includes(".test.") && !dir.includes(".spec.") && !dir.endsWith(".d.ts")) {
|
|
128
|
+
results.push(dir);
|
|
129
|
+
}
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
if (!stat.isDirectory()) {
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
const entries = fs.readdirSync(dir);
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
if (entry === "node_modules" || entry.startsWith(".") || entry === "dist") {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const fullPath = path.join(dir, entry);
|
|
141
|
+
results.push(...this.scanTsFiles(fullPath));
|
|
142
|
+
}
|
|
143
|
+
return results.sort();
|
|
144
|
+
}
|
|
145
|
+
processFile(relativePath) {
|
|
146
|
+
const moduleId = generateModuleId(relativePath);
|
|
147
|
+
const moduleNode = {
|
|
148
|
+
id: moduleId,
|
|
149
|
+
path: relativePath,
|
|
150
|
+
functionIds: []
|
|
151
|
+
};
|
|
152
|
+
const fnCount = 2 + this.hashString(relativePath) % 3;
|
|
153
|
+
const baseName = path.basename(relativePath, path.extname(relativePath));
|
|
154
|
+
for (let i = 0; i < fnCount; i++) {
|
|
155
|
+
const fnName = this.generateFunctionName(baseName, i);
|
|
156
|
+
const startLine = 10 + i * 20;
|
|
157
|
+
const fnId = generateFunctionId(relativePath, fnName, startLine);
|
|
158
|
+
const fn = {
|
|
159
|
+
id: fnId,
|
|
160
|
+
name: fnName,
|
|
161
|
+
fullyQualifiedName: `${relativePath}::${fnName}`,
|
|
162
|
+
file: relativePath,
|
|
163
|
+
startLine,
|
|
164
|
+
endLine: startLine + 15,
|
|
165
|
+
kind: i === 0 ? "function" : i === 1 ? "arrow" : "method"
|
|
166
|
+
};
|
|
167
|
+
this.functions.push(fn);
|
|
168
|
+
moduleNode.functionIds.push(fnId);
|
|
169
|
+
}
|
|
170
|
+
this.modules.push(moduleNode);
|
|
171
|
+
}
|
|
172
|
+
generateFunctionName(baseName, index) {
|
|
173
|
+
const prefixes = ["handle", "process", "get", "set", "create", "update", "delete"];
|
|
174
|
+
const prefix = prefixes[(this.hashString(baseName) + index) % prefixes.length];
|
|
175
|
+
const camelName = baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
176
|
+
return `${prefix}${camelName}${index > 0 ? index : ""}`;
|
|
177
|
+
}
|
|
178
|
+
generateCallEdges() {
|
|
179
|
+
for (let i = 0; i < this.functions.length; i++) {
|
|
180
|
+
const caller = this.functions[i];
|
|
181
|
+
const callCount = this.hashString(caller.id) % 3;
|
|
182
|
+
for (let j = 0; j < callCount; j++) {
|
|
183
|
+
const targetIndex = (i + j + 1) % this.functions.length;
|
|
184
|
+
if (targetIndex === i)
|
|
185
|
+
continue;
|
|
186
|
+
const callee = this.functions[targetIndex];
|
|
187
|
+
if (this.callEdges.some((e) => e.from === caller.id && e.to === callee.id)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
this.callEdges.push({
|
|
191
|
+
from: caller.id,
|
|
192
|
+
to: callee.id,
|
|
193
|
+
callsites: [
|
|
194
|
+
{
|
|
195
|
+
file: caller.file,
|
|
196
|
+
line: caller.startLine + 5
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
generateDataflowEdges() {
|
|
204
|
+
if (this.functions.length < 2)
|
|
205
|
+
return;
|
|
206
|
+
const sources = this.functions.filter((fn) => fn.name.startsWith("handle") || fn.name.startsWith("get"));
|
|
207
|
+
const sinks = this.functions.filter((fn) => fn.name.startsWith("create") || fn.name.startsWith("update") || fn.name.startsWith("set"));
|
|
208
|
+
for (const source of sources.slice(0, 2)) {
|
|
209
|
+
for (const sink of sinks.slice(0, 2)) {
|
|
210
|
+
if (source.id === sink.id)
|
|
211
|
+
continue;
|
|
212
|
+
this.dataflowEdges.push({
|
|
213
|
+
from: source.id,
|
|
214
|
+
to: sink.id,
|
|
215
|
+
label: `data from ${source.name} to ${sink.name}`,
|
|
216
|
+
evidence: [`${source.name} data flows to ${sink.name}`]
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
generateComponents() {
|
|
222
|
+
const dirToModules = /* @__PURE__ */ new Map();
|
|
223
|
+
for (const mod of this.modules) {
|
|
224
|
+
const dir = path.dirname(mod.path) || "root";
|
|
225
|
+
if (!dirToModules.has(dir)) {
|
|
226
|
+
dirToModules.set(dir, []);
|
|
227
|
+
}
|
|
228
|
+
dirToModules.get(dir).push(mod);
|
|
229
|
+
}
|
|
230
|
+
for (const [dir, modules] of dirToModules) {
|
|
231
|
+
const compName = dir === "." || dir === "root" ? "main" : dir.replace(/\//g, "-");
|
|
232
|
+
const compId = generateComponentId(compName);
|
|
233
|
+
this.components.push({
|
|
234
|
+
id: compId,
|
|
235
|
+
name: compName,
|
|
236
|
+
moduleIds: modules.map((m) => m.id)
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
hashString(str) {
|
|
241
|
+
let hash = 0;
|
|
242
|
+
for (let i = 0; i < str.length; i++) {
|
|
243
|
+
const char = str.charCodeAt(i);
|
|
244
|
+
hash = (hash << 5) - hash + char;
|
|
245
|
+
hash = hash & hash;
|
|
246
|
+
}
|
|
247
|
+
return Math.abs(hash);
|
|
248
|
+
}
|
|
249
|
+
generateCommitHash() {
|
|
250
|
+
const input = this.files.sort().join("");
|
|
251
|
+
return `mock-${this.hashString(input).toString(16).padStart(8, "0")}`;
|
|
252
|
+
}
|
|
253
|
+
generateConfigHash() {
|
|
254
|
+
return "mock-config-hash";
|
|
255
|
+
}
|
|
256
|
+
sortById(items) {
|
|
257
|
+
return [...items].sort((a, b) => a.id.localeCompare(b.id));
|
|
258
|
+
}
|
|
259
|
+
sortEdges(edges) {
|
|
260
|
+
return [...edges].sort((a, b) => {
|
|
261
|
+
const fromCmp = a.from.localeCompare(b.from);
|
|
262
|
+
return fromCmp !== 0 ? fromCmp : a.to.localeCompare(b.to);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ../analyzer/dist/exporters/codeql/query-runner.js
|
|
270
|
+
import { execSync } from "child_process";
|
|
271
|
+
import fs2 from "fs";
|
|
272
|
+
import path2 from "path";
|
|
273
|
+
var QueryRunner;
|
|
274
|
+
var init_query_runner = __esm({
|
|
275
|
+
"../analyzer/dist/exporters/codeql/query-runner.js"() {
|
|
276
|
+
"use strict";
|
|
277
|
+
QueryRunner = class {
|
|
278
|
+
dbPath;
|
|
279
|
+
queriesPath;
|
|
280
|
+
targetPath;
|
|
281
|
+
constructor(dbPath, queriesPath, targetPath) {
|
|
282
|
+
this.dbPath = dbPath;
|
|
283
|
+
this.queriesPath = queriesPath;
|
|
284
|
+
this.targetPath = targetPath;
|
|
285
|
+
}
|
|
286
|
+
async runQuery(queryFile) {
|
|
287
|
+
const queryPath = path2.join(this.queriesPath, queryFile);
|
|
288
|
+
const outputPath = path2.join(this.targetPath, `.codeql-results-${queryFile}.json`);
|
|
289
|
+
try {
|
|
290
|
+
execSync(`codeql query run --database="${this.dbPath}" --output="${outputPath}" --format=json "${queryPath}"`, {
|
|
291
|
+
stdio: "pipe",
|
|
292
|
+
cwd: this.targetPath
|
|
293
|
+
});
|
|
294
|
+
if (fs2.existsSync(outputPath)) {
|
|
295
|
+
const content = fs2.readFileSync(outputPath, "utf-8");
|
|
296
|
+
const data = JSON.parse(content);
|
|
297
|
+
fs2.unlinkSync(outputPath);
|
|
298
|
+
return this.parseCodeQLResults(data, queryFile);
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.warn(` Warning: Query ${queryFile} failed: ${error}`);
|
|
302
|
+
}
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
parseCodeQLResults(data, queryFile) {
|
|
306
|
+
if (!data || typeof data !== "object")
|
|
307
|
+
return [];
|
|
308
|
+
const results = [];
|
|
309
|
+
if ("#select" in data && Array.isArray(data["#select"])) {
|
|
310
|
+
const tuples = data["#select"];
|
|
311
|
+
const columns = this.getColumnsForQuery(queryFile);
|
|
312
|
+
for (const tuple of tuples) {
|
|
313
|
+
const obj = {};
|
|
314
|
+
for (let i = 0; i < columns.length && i < tuple.length; i++) {
|
|
315
|
+
obj[columns[i]] = tuple[i];
|
|
316
|
+
}
|
|
317
|
+
results.push(obj);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return results;
|
|
321
|
+
}
|
|
322
|
+
getColumnsForQuery(queryFile) {
|
|
323
|
+
switch (queryFile) {
|
|
324
|
+
case "functions.ql":
|
|
325
|
+
return ["_element", "name", "file", "startLine", "endLine", "kind"];
|
|
326
|
+
case "calls.ql":
|
|
327
|
+
return [
|
|
328
|
+
"callerName",
|
|
329
|
+
"callerFile",
|
|
330
|
+
"callerStartLine",
|
|
331
|
+
"calleeName",
|
|
332
|
+
"calleeFile",
|
|
333
|
+
"calleeStartLine",
|
|
334
|
+
"callsiteLine",
|
|
335
|
+
"callsiteFile"
|
|
336
|
+
];
|
|
337
|
+
case "dataflow.ql":
|
|
338
|
+
return [
|
|
339
|
+
"_element",
|
|
340
|
+
"_source",
|
|
341
|
+
"_sink",
|
|
342
|
+
"sourceFunction",
|
|
343
|
+
"sourceFile",
|
|
344
|
+
"sourceStartLine",
|
|
345
|
+
"sinkFunction",
|
|
346
|
+
"sinkFile",
|
|
347
|
+
"sinkStartLine"
|
|
348
|
+
];
|
|
349
|
+
default:
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ../analyzer/dist/exporters/codeql/codeql-exporter.js
|
|
358
|
+
import { execSync as execSync2 } from "child_process";
|
|
359
|
+
import fs3 from "fs";
|
|
360
|
+
import path3 from "path";
|
|
361
|
+
var CodeQLExporter;
|
|
362
|
+
var init_codeql_exporter = __esm({
|
|
363
|
+
"../analyzer/dist/exporters/codeql/codeql-exporter.js"() {
|
|
364
|
+
"use strict";
|
|
365
|
+
init_dist();
|
|
366
|
+
init_query_runner();
|
|
367
|
+
CodeQLExporter = class {
|
|
368
|
+
targetPath;
|
|
369
|
+
dbPath;
|
|
370
|
+
queriesPath;
|
|
371
|
+
queryRunner;
|
|
372
|
+
functions = [];
|
|
373
|
+
modules = [];
|
|
374
|
+
components = [];
|
|
375
|
+
callEdges = [];
|
|
376
|
+
dataflowEdges = [];
|
|
377
|
+
functionIdMap = /* @__PURE__ */ new Map();
|
|
378
|
+
constructor(targetPath) {
|
|
379
|
+
this.targetPath = path3.resolve(targetPath);
|
|
380
|
+
this.dbPath = path3.join(this.targetPath, ".codeql-db");
|
|
381
|
+
this.queriesPath = path3.join(__dirname, "queries");
|
|
382
|
+
this.queryRunner = new QueryRunner(this.dbPath, this.queriesPath, this.targetPath);
|
|
383
|
+
}
|
|
384
|
+
async prepare() {
|
|
385
|
+
try {
|
|
386
|
+
execSync2("codeql --version", { stdio: "pipe" });
|
|
387
|
+
} catch {
|
|
388
|
+
throw new Error("CodeQL CLI is not installed. Please install it from https://github.com/github/codeql-cli-binaries");
|
|
389
|
+
}
|
|
390
|
+
console.log("Creating CodeQL database...");
|
|
391
|
+
await this.createDatabase();
|
|
392
|
+
}
|
|
393
|
+
async createDatabase() {
|
|
394
|
+
if (fs3.existsSync(this.dbPath)) {
|
|
395
|
+
fs3.rmSync(this.dbPath, { recursive: true });
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
execSync2(`codeql database create "${this.dbPath}" --language=javascript --source-root="${this.targetPath}" --overwrite`, {
|
|
399
|
+
stdio: "inherit",
|
|
400
|
+
cwd: this.targetPath
|
|
401
|
+
});
|
|
402
|
+
} catch (error) {
|
|
403
|
+
throw new Error(`Failed to create CodeQL database: ${error}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
async analyze() {
|
|
407
|
+
console.log("Running CodeQL queries...");
|
|
408
|
+
await this.runFunctionsQuery();
|
|
409
|
+
await this.runCallsQuery();
|
|
410
|
+
await this.runDataflowQuery();
|
|
411
|
+
this.buildModulesAndComponents();
|
|
412
|
+
}
|
|
413
|
+
async runFunctionsQuery() {
|
|
414
|
+
console.log(" Extracting functions...");
|
|
415
|
+
const results = await this.queryRunner.runQuery("functions.ql");
|
|
416
|
+
for (const result of results) {
|
|
417
|
+
const fnId = generateFunctionId(result.file, result.name, result.startLine);
|
|
418
|
+
const key = `${result.file}:${result.name}:${result.startLine}`;
|
|
419
|
+
this.functionIdMap.set(key, fnId);
|
|
420
|
+
this.functions.push({
|
|
421
|
+
id: fnId,
|
|
422
|
+
name: result.name,
|
|
423
|
+
fullyQualifiedName: `${result.file}::${result.name}`,
|
|
424
|
+
file: result.file,
|
|
425
|
+
startLine: result.startLine,
|
|
426
|
+
endLine: result.endLine,
|
|
427
|
+
kind: this.normalizeKind(result.kind)
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
console.log(` Found ${this.functions.length} functions`);
|
|
431
|
+
}
|
|
432
|
+
async runCallsQuery() {
|
|
433
|
+
console.log(" Extracting call edges...");
|
|
434
|
+
const results = await this.queryRunner.runQuery("calls.ql");
|
|
435
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const result of results) {
|
|
437
|
+
const callerId = this.getFunctionId(result.callerFile, result.callerName, result.callerStartLine);
|
|
438
|
+
const calleeId = this.getFunctionId(result.calleeFile, result.calleeName, result.calleeStartLine);
|
|
439
|
+
if (!callerId || !calleeId)
|
|
440
|
+
continue;
|
|
441
|
+
const edgeKey = `${callerId}->${calleeId}`;
|
|
442
|
+
if (edgeMap.has(edgeKey)) {
|
|
443
|
+
edgeMap.get(edgeKey).callsites.push({
|
|
444
|
+
file: result.callsiteFile,
|
|
445
|
+
line: result.callsiteLine
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
edgeMap.set(edgeKey, {
|
|
449
|
+
from: callerId,
|
|
450
|
+
to: calleeId,
|
|
451
|
+
callsites: [{ file: result.callsiteFile, line: result.callsiteLine }]
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
this.callEdges = Array.from(edgeMap.values());
|
|
456
|
+
console.log(` Found ${this.callEdges.length} call edges`);
|
|
457
|
+
}
|
|
458
|
+
async runDataflowQuery() {
|
|
459
|
+
console.log(" Extracting dataflow edges...");
|
|
460
|
+
const results = await this.queryRunner.runQuery("dataflow.ql");
|
|
461
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
462
|
+
for (const result of results) {
|
|
463
|
+
const sourceId = this.getFunctionId(result.sourceFile, result.sourceFunction, result.sourceStartLine);
|
|
464
|
+
const sinkId = this.getFunctionId(result.sinkFile, result.sinkFunction, result.sinkStartLine);
|
|
465
|
+
if (!sourceId || !sinkId || sourceId === sinkId)
|
|
466
|
+
continue;
|
|
467
|
+
const edgeKey = `${sourceId}->${sinkId}`;
|
|
468
|
+
if (!edgeMap.has(edgeKey)) {
|
|
469
|
+
edgeMap.set(edgeKey, {
|
|
470
|
+
from: sourceId,
|
|
471
|
+
to: sinkId,
|
|
472
|
+
label: `data from ${result.sourceFunction} to ${result.sinkFunction}`,
|
|
473
|
+
evidence: []
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
edgeMap.get(edgeKey).evidence.push(`Flow from ${result.sourceFile}:${result.sourceStartLine} to ${result.sinkFile}:${result.sinkStartLine}`);
|
|
477
|
+
}
|
|
478
|
+
this.dataflowEdges = Array.from(edgeMap.values());
|
|
479
|
+
console.log(` Found ${this.dataflowEdges.length} dataflow edges`);
|
|
480
|
+
}
|
|
481
|
+
getFunctionId(file, name, startLine) {
|
|
482
|
+
return this.functionIdMap.get(`${file}:${name}:${startLine}`);
|
|
483
|
+
}
|
|
484
|
+
normalizeKind(kind) {
|
|
485
|
+
switch (kind.toLowerCase()) {
|
|
486
|
+
case "arrow":
|
|
487
|
+
return "arrow";
|
|
488
|
+
case "method":
|
|
489
|
+
return "method";
|
|
490
|
+
case "constructor":
|
|
491
|
+
return "constructor";
|
|
492
|
+
default:
|
|
493
|
+
return "function";
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
buildModulesAndComponents() {
|
|
497
|
+
const fileToFunctions = /* @__PURE__ */ new Map();
|
|
498
|
+
for (const fn of this.functions) {
|
|
499
|
+
if (!fileToFunctions.has(fn.file)) {
|
|
500
|
+
fileToFunctions.set(fn.file, []);
|
|
501
|
+
}
|
|
502
|
+
fileToFunctions.get(fn.file).push(fn);
|
|
503
|
+
}
|
|
504
|
+
for (const [file, fns] of fileToFunctions) {
|
|
505
|
+
this.modules.push({
|
|
506
|
+
id: generateModuleId(file),
|
|
507
|
+
path: file,
|
|
508
|
+
functionIds: fns.map((f) => f.id)
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
const dirToModules = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const mod of this.modules) {
|
|
513
|
+
const dir = path3.dirname(mod.path) || "root";
|
|
514
|
+
if (!dirToModules.has(dir)) {
|
|
515
|
+
dirToModules.set(dir, []);
|
|
516
|
+
}
|
|
517
|
+
dirToModules.get(dir).push(mod);
|
|
518
|
+
}
|
|
519
|
+
for (const [dir, mods] of dirToModules) {
|
|
520
|
+
const compName = dir === "." || dir === "root" ? "main" : dir.replace(/[/\\]/g, "-");
|
|
521
|
+
this.components.push({
|
|
522
|
+
id: generateComponentId(compName),
|
|
523
|
+
name: compName,
|
|
524
|
+
moduleIds: mods.map((m) => m.id)
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async export() {
|
|
529
|
+
let repoCommit = "unknown";
|
|
530
|
+
try {
|
|
531
|
+
repoCommit = execSync2("git rev-parse HEAD", { cwd: this.targetPath, stdio: "pipe" }).toString().trim().slice(0, 8);
|
|
532
|
+
} catch {
|
|
533
|
+
repoCommit = `local-${Date.now().toString(16)}`;
|
|
534
|
+
}
|
|
535
|
+
return {
|
|
536
|
+
metadata: {
|
|
537
|
+
analyzerVersion: "0.1.0",
|
|
538
|
+
language: "typescript",
|
|
539
|
+
repoCommit,
|
|
540
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
541
|
+
configHash: this.generateConfigHash()
|
|
542
|
+
},
|
|
543
|
+
nodes: {
|
|
544
|
+
functions: this.sortById(this.functions),
|
|
545
|
+
modules: this.sortById(this.modules),
|
|
546
|
+
components: this.sortById(this.components)
|
|
547
|
+
},
|
|
548
|
+
edges: {
|
|
549
|
+
calls: this.sortEdges(this.callEdges),
|
|
550
|
+
dataflow: this.sortEdges(this.dataflowEdges)
|
|
551
|
+
},
|
|
552
|
+
layout: { positions: {} }
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
async cleanup() {
|
|
556
|
+
if (fs3.existsSync(this.dbPath)) {
|
|
557
|
+
try {
|
|
558
|
+
fs3.rmSync(this.dbPath, { recursive: true });
|
|
559
|
+
} catch {
|
|
560
|
+
console.warn("Warning: Could not clean up CodeQL database");
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
generateConfigHash() {
|
|
565
|
+
const queryFiles = fs3.readdirSync(this.queriesPath).sort();
|
|
566
|
+
return `codeql-${this.hashString(queryFiles.join(",")).toString(16).padStart(8, "0")}`;
|
|
567
|
+
}
|
|
568
|
+
hashString(str) {
|
|
569
|
+
let hash = 0;
|
|
570
|
+
for (let i = 0; i < str.length; i++) {
|
|
571
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
572
|
+
hash = hash & hash;
|
|
573
|
+
}
|
|
574
|
+
return Math.abs(hash);
|
|
575
|
+
}
|
|
576
|
+
sortById(items) {
|
|
577
|
+
return [...items].sort((a, b) => a.id.localeCompare(b.id));
|
|
578
|
+
}
|
|
579
|
+
sortEdges(edges) {
|
|
580
|
+
return [...edges].sort((a, b) => {
|
|
581
|
+
const cmp = a.from.localeCompare(b.from);
|
|
582
|
+
return cmp !== 0 ? cmp : a.to.localeCompare(b.to);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// ../analyzer/dist/exporters/codeql/index.js
|
|
590
|
+
var init_codeql = __esm({
|
|
591
|
+
"../analyzer/dist/exporters/codeql/index.js"() {
|
|
592
|
+
"use strict";
|
|
593
|
+
init_codeql_exporter();
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
// ../analyzer/dist/layout/elk-layout.js
|
|
598
|
+
import { createRequire as _createRequire } from "module";
|
|
599
|
+
async function applyElkLayout(artifact) {
|
|
600
|
+
const elkGraph = buildElkGraph(artifact);
|
|
601
|
+
const layoutedGraph = await elk.layout(elkGraph);
|
|
602
|
+
const positions = extractPositions(layoutedGraph);
|
|
603
|
+
return {
|
|
604
|
+
...artifact,
|
|
605
|
+
layout: {
|
|
606
|
+
...artifact.layout,
|
|
607
|
+
positions
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
function buildElkGraph(artifact) {
|
|
612
|
+
const children = artifact.nodes.functions.map((fn) => ({
|
|
613
|
+
id: fn.id,
|
|
614
|
+
width: 120,
|
|
615
|
+
height: 40
|
|
616
|
+
}));
|
|
617
|
+
const edges = [
|
|
618
|
+
...artifact.edges.calls.map((edge) => ({
|
|
619
|
+
id: `call-${edge.from}-${edge.to}`,
|
|
620
|
+
sources: [edge.from],
|
|
621
|
+
targets: [edge.to]
|
|
622
|
+
})),
|
|
623
|
+
...artifact.edges.dataflow.map((edge) => ({
|
|
624
|
+
id: `df-${edge.from}-${edge.to}`,
|
|
625
|
+
sources: [edge.from],
|
|
626
|
+
targets: [edge.to]
|
|
627
|
+
}))
|
|
628
|
+
];
|
|
629
|
+
return {
|
|
630
|
+
id: "root",
|
|
631
|
+
layoutOptions: {
|
|
632
|
+
"elk.algorithm": "layered",
|
|
633
|
+
"elk.direction": "RIGHT",
|
|
634
|
+
"elk.spacing.nodeNode": "50",
|
|
635
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": "100",
|
|
636
|
+
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX"
|
|
637
|
+
},
|
|
638
|
+
children,
|
|
639
|
+
edges
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function extractPositions(layoutedGraph) {
|
|
643
|
+
const positions = {};
|
|
644
|
+
if (layoutedGraph.children) {
|
|
645
|
+
for (const child of layoutedGraph.children) {
|
|
646
|
+
if (child.x !== void 0 && child.y !== void 0) {
|
|
647
|
+
positions[child.id] = {
|
|
648
|
+
x: child.x,
|
|
649
|
+
y: child.y
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return positions;
|
|
655
|
+
}
|
|
656
|
+
var __require, ELKModule, ELKConstructor, elk;
|
|
657
|
+
var init_elk_layout = __esm({
|
|
658
|
+
"../analyzer/dist/layout/elk-layout.js"() {
|
|
659
|
+
"use strict";
|
|
660
|
+
__require = _createRequire(import.meta.url);
|
|
661
|
+
ELKModule = __require("elkjs");
|
|
662
|
+
ELKConstructor = ELKModule.default;
|
|
663
|
+
elk = new ELKConstructor();
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// ../analyzer/dist/commands/analyze.js
|
|
668
|
+
import fs4 from "fs";
|
|
669
|
+
import path4 from "path";
|
|
670
|
+
async function analyzeCommand(targetPath, options) {
|
|
671
|
+
console.log(`Analyzing: ${path4.resolve(targetPath)}`);
|
|
672
|
+
console.log(`Exporter: ${options.exporter}`);
|
|
673
|
+
console.log(`Output: ${options.output}`);
|
|
674
|
+
console.log("");
|
|
675
|
+
const absolutePath = path4.resolve(targetPath);
|
|
676
|
+
if (!fs4.existsSync(absolutePath)) {
|
|
677
|
+
console.error(`Error: Path does not exist: ${absolutePath}`);
|
|
678
|
+
process.exit(1);
|
|
679
|
+
}
|
|
680
|
+
let artifact;
|
|
681
|
+
if (options.exporter === "mock") {
|
|
682
|
+
console.log("Using mock exporter...");
|
|
683
|
+
const exporter = new MockExporter(absolutePath);
|
|
684
|
+
await exporter.prepare();
|
|
685
|
+
await exporter.analyze();
|
|
686
|
+
artifact = await exporter.export();
|
|
687
|
+
await exporter.cleanup();
|
|
688
|
+
} else if (options.exporter === "codeql-ts") {
|
|
689
|
+
console.log("Using CodeQL exporter for TypeScript...");
|
|
690
|
+
const exporter = new CodeQLExporter(absolutePath);
|
|
691
|
+
await exporter.prepare();
|
|
692
|
+
await exporter.analyze();
|
|
693
|
+
artifact = await exporter.export();
|
|
694
|
+
await exporter.cleanup();
|
|
695
|
+
} else {
|
|
696
|
+
console.error(`Unknown exporter: ${options.exporter}`);
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
console.log("Applying ELK layout...");
|
|
700
|
+
artifact = await applyElkLayout(artifact);
|
|
701
|
+
const outputDir = path4.dirname(path4.resolve(options.output));
|
|
702
|
+
if (!fs4.existsSync(outputDir)) {
|
|
703
|
+
fs4.mkdirSync(outputDir, { recursive: true });
|
|
704
|
+
}
|
|
705
|
+
const outputPath = path4.resolve(options.output);
|
|
706
|
+
const jsonContent = JSON.stringify(artifact, null, 2);
|
|
707
|
+
fs4.writeFileSync(outputPath, jsonContent, "utf-8");
|
|
708
|
+
console.log("");
|
|
709
|
+
console.log(`Artifact written to: ${outputPath}`);
|
|
710
|
+
console.log(` Functions: ${artifact.nodes.functions.length}`);
|
|
711
|
+
console.log(` Modules: ${artifact.nodes.modules.length}`);
|
|
712
|
+
console.log(` Call edges: ${artifact.edges.calls.length}`);
|
|
713
|
+
console.log(` Dataflow edges: ${artifact.edges.dataflow.length}`);
|
|
714
|
+
}
|
|
715
|
+
var init_analyze = __esm({
|
|
716
|
+
"../analyzer/dist/commands/analyze.js"() {
|
|
717
|
+
"use strict";
|
|
718
|
+
init_mock_exporter();
|
|
719
|
+
init_codeql();
|
|
720
|
+
init_elk_layout();
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// ../analyzer/dist/commands/serve.js
|
|
725
|
+
import fs5 from "fs";
|
|
726
|
+
import path5 from "path";
|
|
727
|
+
import { spawn } from "child_process";
|
|
728
|
+
async function serveCommand(options) {
|
|
729
|
+
const artifactPath = path5.resolve(options.artifact);
|
|
730
|
+
if (!fs5.existsSync(artifactPath)) {
|
|
731
|
+
console.error(`Error: Artifact not found: ${artifactPath}`);
|
|
732
|
+
console.error('Run "codeflow analyze" first to generate the artifact.');
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
console.log(`Starting CodeFlowMap UI...`);
|
|
736
|
+
console.log(`Artifact: ${artifactPath}`);
|
|
737
|
+
console.log(`Port: ${options.port}`);
|
|
738
|
+
console.log("");
|
|
739
|
+
const uiPackagePath = path5.resolve(__dirname, "../../ui");
|
|
740
|
+
if (!fs5.existsSync(uiPackagePath)) {
|
|
741
|
+
const monorepoUiPath = path5.resolve(__dirname, "../../../ui");
|
|
742
|
+
if (fs5.existsSync(monorepoUiPath)) {
|
|
743
|
+
startVite(monorepoUiPath, options.port);
|
|
744
|
+
} else {
|
|
745
|
+
console.error("UI package not found. Run from the monorepo root.");
|
|
746
|
+
process.exit(1);
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
startVite(uiPackagePath, options.port);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function startVite(uiPath, port) {
|
|
753
|
+
console.log(`Starting Vite dev server at ${uiPath}...`);
|
|
754
|
+
console.log(`Open http://localhost:${port} in your browser`);
|
|
755
|
+
console.log("");
|
|
756
|
+
const vite = spawn("npx", ["vite", "--port", port], {
|
|
757
|
+
cwd: uiPath,
|
|
758
|
+
stdio: "inherit",
|
|
759
|
+
shell: true
|
|
760
|
+
});
|
|
761
|
+
vite.on("error", (err) => {
|
|
762
|
+
console.error("Failed to start Vite:", err);
|
|
763
|
+
process.exit(1);
|
|
764
|
+
});
|
|
765
|
+
vite.on("close", (code) => {
|
|
766
|
+
process.exit(code ?? 0);
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
var init_serve = __esm({
|
|
770
|
+
"../analyzer/dist/commands/serve.js"() {
|
|
771
|
+
"use strict";
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// ../analyzer/dist/commands/diff.js
|
|
776
|
+
import fs6 from "fs";
|
|
777
|
+
import path6 from "path";
|
|
778
|
+
async function diffCommand(baselinePath, currentPath, options) {
|
|
779
|
+
const baselineArtifact = loadArtifact(baselinePath, "Baseline");
|
|
780
|
+
if (!baselineArtifact) {
|
|
781
|
+
process.exit(1);
|
|
782
|
+
}
|
|
783
|
+
const currentArtifact = loadArtifact(currentPath, "Current");
|
|
784
|
+
if (!currentArtifact) {
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
const diff = calculateDiff(baselineArtifact, currentArtifact);
|
|
788
|
+
if (options.format === "json") {
|
|
789
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
790
|
+
} else {
|
|
791
|
+
printTextDiff(diff, baselinePath, currentPath);
|
|
792
|
+
}
|
|
793
|
+
if (options.exitOnChanges && diff.hasChanges) {
|
|
794
|
+
process.exit(1);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function loadArtifact(filePath, label) {
|
|
798
|
+
const absolutePath = path6.resolve(filePath);
|
|
799
|
+
if (!fs6.existsSync(absolutePath)) {
|
|
800
|
+
console.error(`Error: ${label} artifact not found: ${absolutePath}`);
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
const content = fs6.readFileSync(absolutePath, "utf-8");
|
|
805
|
+
const artifact = JSON.parse(content);
|
|
806
|
+
if (!isCodeFlowArtifact(artifact)) {
|
|
807
|
+
console.error(`Error: ${label} file is not a valid CodeFlow artifact`);
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
return artifact;
|
|
811
|
+
} catch (error) {
|
|
812
|
+
console.error(`Error: Failed to parse ${label} artifact: ${error}`);
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
function calculateDiff(baseline, current) {
|
|
817
|
+
const baselineFnIds = new Set(baseline.nodes.functions.map((fn) => fn.id));
|
|
818
|
+
const currentFnIds = new Set(current.nodes.functions.map((fn) => fn.id));
|
|
819
|
+
const addedFunctions = current.nodes.functions.filter((fn) => !baselineFnIds.has(fn.id)).map((fn) => `${fn.name} (${fn.file}:${fn.startLine})`);
|
|
820
|
+
const removedFunctions = baseline.nodes.functions.filter((fn) => !currentFnIds.has(fn.id)).map((fn) => `${fn.name} (${fn.file}:${fn.startLine})`);
|
|
821
|
+
const baselineModIds = new Set(baseline.nodes.modules.map((m) => m.id));
|
|
822
|
+
const currentModIds = new Set(current.nodes.modules.map((m) => m.id));
|
|
823
|
+
const addedModules = current.nodes.modules.filter((m) => !baselineModIds.has(m.id)).map((m) => m.path);
|
|
824
|
+
const removedModules = baseline.nodes.modules.filter((m) => !currentModIds.has(m.id)).map((m) => m.path);
|
|
825
|
+
const edgeKey = (e) => `${e.from}->${e.to}`;
|
|
826
|
+
const baselineCallEdges = new Set(baseline.edges.calls.map(edgeKey));
|
|
827
|
+
const currentCallEdges = new Set(current.edges.calls.map(edgeKey));
|
|
828
|
+
const addedCallEdges = current.edges.calls.filter((e) => !baselineCallEdges.has(edgeKey(e))).map((e) => formatEdge(e, current));
|
|
829
|
+
const removedCallEdges = baseline.edges.calls.filter((e) => !currentCallEdges.has(edgeKey(e))).map((e) => formatEdge(e, baseline));
|
|
830
|
+
const baselineDataflowEdges = new Set(baseline.edges.dataflow.map(edgeKey));
|
|
831
|
+
const currentDataflowEdges = new Set(current.edges.dataflow.map(edgeKey));
|
|
832
|
+
const addedDataflowEdges = current.edges.dataflow.filter((e) => !baselineDataflowEdges.has(edgeKey(e))).map((e) => formatEdge(e, current));
|
|
833
|
+
const removedDataflowEdges = baseline.edges.dataflow.filter((e) => !currentDataflowEdges.has(edgeKey(e))).map((e) => formatEdge(e, baseline));
|
|
834
|
+
const hasChanges = addedFunctions.length > 0 || removedFunctions.length > 0 || addedModules.length > 0 || removedModules.length > 0 || addedCallEdges.length > 0 || removedCallEdges.length > 0 || addedDataflowEdges.length > 0 || removedDataflowEdges.length > 0;
|
|
835
|
+
return {
|
|
836
|
+
hasChanges,
|
|
837
|
+
summary: {
|
|
838
|
+
functionsAdded: addedFunctions.length,
|
|
839
|
+
functionsRemoved: removedFunctions.length,
|
|
840
|
+
modulesAdded: addedModules.length,
|
|
841
|
+
modulesRemoved: removedModules.length,
|
|
842
|
+
callEdgesAdded: addedCallEdges.length,
|
|
843
|
+
callEdgesRemoved: removedCallEdges.length,
|
|
844
|
+
dataflowEdgesAdded: addedDataflowEdges.length,
|
|
845
|
+
dataflowEdgesRemoved: removedDataflowEdges.length
|
|
846
|
+
},
|
|
847
|
+
details: {
|
|
848
|
+
addedFunctions,
|
|
849
|
+
removedFunctions,
|
|
850
|
+
addedModules,
|
|
851
|
+
removedModules,
|
|
852
|
+
addedCallEdges,
|
|
853
|
+
removedCallEdges,
|
|
854
|
+
addedDataflowEdges,
|
|
855
|
+
removedDataflowEdges
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function formatEdge(edge, artifact) {
|
|
860
|
+
const fromFn = artifact.nodes.functions.find((fn) => fn.id === edge.from);
|
|
861
|
+
const toFn = artifact.nodes.functions.find((fn) => fn.id === edge.to);
|
|
862
|
+
const fromName = fromFn ? fromFn.name : edge.from;
|
|
863
|
+
const toName = toFn ? toFn.name : edge.to;
|
|
864
|
+
return `${fromName} -> ${toName}`;
|
|
865
|
+
}
|
|
866
|
+
function printTextDiff(diff, baselinePath, currentPath) {
|
|
867
|
+
console.log("CodeFlow Artifact Diff");
|
|
868
|
+
console.log("======================");
|
|
869
|
+
console.log(`Baseline: ${baselinePath}`);
|
|
870
|
+
console.log(`Current: ${currentPath}`);
|
|
871
|
+
console.log("");
|
|
872
|
+
if (!diff.hasChanges) {
|
|
873
|
+
console.log("No changes detected.");
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
console.log("Summary:");
|
|
877
|
+
console.log(` Functions: +${diff.summary.functionsAdded} / -${diff.summary.functionsRemoved}`);
|
|
878
|
+
console.log(` Modules: +${diff.summary.modulesAdded} / -${diff.summary.modulesRemoved}`);
|
|
879
|
+
console.log(` Call Edges: +${diff.summary.callEdgesAdded} / -${diff.summary.callEdgesRemoved}`);
|
|
880
|
+
console.log(` Dataflow Edges: +${diff.summary.dataflowEdgesAdded} / -${diff.summary.dataflowEdgesRemoved}`);
|
|
881
|
+
console.log("");
|
|
882
|
+
if (diff.details.addedFunctions.length > 0) {
|
|
883
|
+
console.log("Added Functions:");
|
|
884
|
+
for (const fn of diff.details.addedFunctions) {
|
|
885
|
+
console.log(` + ${fn}`);
|
|
886
|
+
}
|
|
887
|
+
console.log("");
|
|
888
|
+
}
|
|
889
|
+
if (diff.details.removedFunctions.length > 0) {
|
|
890
|
+
console.log("Removed Functions:");
|
|
891
|
+
for (const fn of diff.details.removedFunctions) {
|
|
892
|
+
console.log(` - ${fn}`);
|
|
893
|
+
}
|
|
894
|
+
console.log("");
|
|
895
|
+
}
|
|
896
|
+
if (diff.details.addedModules.length > 0) {
|
|
897
|
+
console.log("Added Modules:");
|
|
898
|
+
for (const mod of diff.details.addedModules) {
|
|
899
|
+
console.log(` + ${mod}`);
|
|
900
|
+
}
|
|
901
|
+
console.log("");
|
|
902
|
+
}
|
|
903
|
+
if (diff.details.removedModules.length > 0) {
|
|
904
|
+
console.log("Removed Modules:");
|
|
905
|
+
for (const mod of diff.details.removedModules) {
|
|
906
|
+
console.log(` - ${mod}`);
|
|
907
|
+
}
|
|
908
|
+
console.log("");
|
|
909
|
+
}
|
|
910
|
+
if (diff.details.addedCallEdges.length > 0) {
|
|
911
|
+
console.log("Added Call Edges:");
|
|
912
|
+
for (const edge of diff.details.addedCallEdges) {
|
|
913
|
+
console.log(` + ${edge}`);
|
|
914
|
+
}
|
|
915
|
+
console.log("");
|
|
916
|
+
}
|
|
917
|
+
if (diff.details.removedCallEdges.length > 0) {
|
|
918
|
+
console.log("Removed Call Edges:");
|
|
919
|
+
for (const edge of diff.details.removedCallEdges) {
|
|
920
|
+
console.log(` - ${edge}`);
|
|
921
|
+
}
|
|
922
|
+
console.log("");
|
|
923
|
+
}
|
|
924
|
+
if (diff.details.addedDataflowEdges.length > 0) {
|
|
925
|
+
console.log("Added Dataflow Edges:");
|
|
926
|
+
for (const edge of diff.details.addedDataflowEdges) {
|
|
927
|
+
console.log(` + ${edge}`);
|
|
928
|
+
}
|
|
929
|
+
console.log("");
|
|
930
|
+
}
|
|
931
|
+
if (diff.details.removedDataflowEdges.length > 0) {
|
|
932
|
+
console.log("Removed Dataflow Edges:");
|
|
933
|
+
for (const edge of diff.details.removedDataflowEdges) {
|
|
934
|
+
console.log(` - ${edge}`);
|
|
935
|
+
}
|
|
936
|
+
console.log("");
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
var init_diff = __esm({
|
|
940
|
+
"../analyzer/dist/commands/diff.js"() {
|
|
941
|
+
"use strict";
|
|
942
|
+
init_dist();
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// ../analyzer/dist/index.js
|
|
947
|
+
var dist_exports = {};
|
|
948
|
+
__export(dist_exports, {
|
|
949
|
+
CodeQLExporter: () => CodeQLExporter,
|
|
950
|
+
MockExporter: () => MockExporter,
|
|
951
|
+
analyzeCommand: () => analyzeCommand,
|
|
952
|
+
applyElkLayout: () => applyElkLayout,
|
|
953
|
+
diffCommand: () => diffCommand,
|
|
954
|
+
serveCommand: () => serveCommand
|
|
955
|
+
});
|
|
956
|
+
var init_dist2 = __esm({
|
|
957
|
+
"../analyzer/dist/index.js"() {
|
|
958
|
+
"use strict";
|
|
959
|
+
init_analyze();
|
|
960
|
+
init_serve();
|
|
961
|
+
init_diff();
|
|
962
|
+
init_elk_layout();
|
|
963
|
+
init_mock_exporter();
|
|
964
|
+
init_codeql_exporter();
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// src/cli.ts
|
|
969
|
+
import { Command } from "commander";
|
|
970
|
+
|
|
971
|
+
// src/commands/analyze.ts
|
|
972
|
+
init_dist2();
|
|
973
|
+
import fs7 from "fs";
|
|
974
|
+
import path7 from "path";
|
|
975
|
+
import ora from "ora";
|
|
976
|
+
|
|
977
|
+
// src/utils/output.ts
|
|
978
|
+
import chalk from "chalk";
|
|
979
|
+
var log = {
|
|
980
|
+
info: (message) => console.log(chalk.blue("\u2139"), message),
|
|
981
|
+
success: (message) => console.log(chalk.green("\u2713"), message),
|
|
982
|
+
warning: (message) => console.log(chalk.yellow("\u26A0"), message),
|
|
983
|
+
error: (message) => console.error(chalk.red("\u2717"), message),
|
|
984
|
+
title: (message) => console.log(chalk.bold.cyan(`
|
|
985
|
+
${message}
|
|
986
|
+
`)),
|
|
987
|
+
dim: (message) => console.log(chalk.dim(message))
|
|
988
|
+
};
|
|
989
|
+
function formatNumber(n) {
|
|
990
|
+
return n.toLocaleString();
|
|
991
|
+
}
|
|
992
|
+
function formatPath(p) {
|
|
993
|
+
return chalk.underline(p);
|
|
994
|
+
}
|
|
995
|
+
function formatDiff(added, removed) {
|
|
996
|
+
const parts = [];
|
|
997
|
+
if (added > 0) parts.push(chalk.green(`+${added}`));
|
|
998
|
+
if (removed > 0) parts.push(chalk.red(`-${removed}`));
|
|
999
|
+
return parts.join(" / ") || chalk.dim("no changes");
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/commands/analyze.ts
|
|
1003
|
+
async function analyzeCommand2(targetPath, options) {
|
|
1004
|
+
const absolutePath = path7.resolve(targetPath);
|
|
1005
|
+
if (!fs7.existsSync(absolutePath)) {
|
|
1006
|
+
log.error(`Path does not exist: ${formatPath(absolutePath)}`);
|
|
1007
|
+
process.exit(1);
|
|
1008
|
+
}
|
|
1009
|
+
log.title("Monarch Analysis");
|
|
1010
|
+
log.info(`Target: ${formatPath(absolutePath)}`);
|
|
1011
|
+
log.info(`Exporter: ${options.exporter}`);
|
|
1012
|
+
log.info(`Output: ${formatPath(options.output)}`);
|
|
1013
|
+
const spinner = ora("Analyzing codebase...").start();
|
|
1014
|
+
try {
|
|
1015
|
+
let artifact;
|
|
1016
|
+
if (options.exporter === "mock") {
|
|
1017
|
+
const exporter = new MockExporter(absolutePath);
|
|
1018
|
+
await exporter.prepare();
|
|
1019
|
+
spinner.text = "Scanning files...";
|
|
1020
|
+
await exporter.analyze();
|
|
1021
|
+
spinner.text = "Generating artifact...";
|
|
1022
|
+
artifact = await exporter.export();
|
|
1023
|
+
await exporter.cleanup();
|
|
1024
|
+
} else if (options.exporter === "codeql-ts") {
|
|
1025
|
+
spinner.text = "Creating CodeQL database...";
|
|
1026
|
+
const { CodeQLExporter: CodeQLExporter2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports));
|
|
1027
|
+
const exporter = new CodeQLExporter2(absolutePath);
|
|
1028
|
+
await exporter.prepare();
|
|
1029
|
+
spinner.text = "Running CodeQL queries...";
|
|
1030
|
+
await exporter.analyze();
|
|
1031
|
+
spinner.text = "Generating artifact...";
|
|
1032
|
+
artifact = await exporter.export();
|
|
1033
|
+
await exporter.cleanup();
|
|
1034
|
+
} else {
|
|
1035
|
+
spinner.fail(`Unknown exporter: ${options.exporter}`);
|
|
1036
|
+
process.exit(1);
|
|
1037
|
+
}
|
|
1038
|
+
spinner.text = "Computing layout...";
|
|
1039
|
+
artifact = await applyElkLayout(artifact);
|
|
1040
|
+
const outputDir = path7.dirname(path7.resolve(options.output));
|
|
1041
|
+
if (!fs7.existsSync(outputDir)) {
|
|
1042
|
+
fs7.mkdirSync(outputDir, { recursive: true });
|
|
1043
|
+
}
|
|
1044
|
+
const outputPath = path7.resolve(options.output);
|
|
1045
|
+
fs7.writeFileSync(outputPath, JSON.stringify(artifact, null, 2));
|
|
1046
|
+
spinner.succeed("Analysis complete!");
|
|
1047
|
+
console.log("");
|
|
1048
|
+
log.success(`Graph saved to ${formatPath(outputPath)}`);
|
|
1049
|
+
console.log("");
|
|
1050
|
+
log.info(`Functions: ${formatNumber(artifact.nodes.functions.length)}`);
|
|
1051
|
+
log.info(`Modules: ${formatNumber(artifact.nodes.modules.length)}`);
|
|
1052
|
+
log.info(`Components: ${formatNumber(artifact.nodes.components.length)}`);
|
|
1053
|
+
log.info(`Call edges: ${formatNumber(artifact.edges.calls.length)}`);
|
|
1054
|
+
log.info(`Dataflow: ${formatNumber(artifact.edges.dataflow.length)}`);
|
|
1055
|
+
if (options.watch) {
|
|
1056
|
+
log.info("\nWatching for changes... (press Ctrl+C to stop)");
|
|
1057
|
+
}
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
spinner.fail("Analysis failed");
|
|
1060
|
+
log.error(String(error));
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/commands/diff.ts
|
|
1066
|
+
init_dist();
|
|
1067
|
+
import fs8 from "fs";
|
|
1068
|
+
import path8 from "path";
|
|
1069
|
+
import chalk2 from "chalk";
|
|
1070
|
+
async function diffCommand2(baselinePath, currentPath, options) {
|
|
1071
|
+
const baseline = loadArtifact2(baselinePath, "Baseline");
|
|
1072
|
+
const current = loadArtifact2(currentPath, "Current");
|
|
1073
|
+
if (!baseline || !current) {
|
|
1074
|
+
process.exit(1);
|
|
1075
|
+
}
|
|
1076
|
+
const diff = calculateDiff2(baseline, current);
|
|
1077
|
+
switch (options.format) {
|
|
1078
|
+
case "json":
|
|
1079
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
1080
|
+
break;
|
|
1081
|
+
case "markdown":
|
|
1082
|
+
printMarkdownDiff(diff, baselinePath, currentPath, options.only);
|
|
1083
|
+
break;
|
|
1084
|
+
default:
|
|
1085
|
+
printTextDiff2(diff, baselinePath, currentPath, options.only);
|
|
1086
|
+
}
|
|
1087
|
+
if (options.exitOnChanges && diff.hasChanges) {
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function loadArtifact2(filePath, label) {
|
|
1092
|
+
const absolutePath = path8.resolve(filePath);
|
|
1093
|
+
if (!fs8.existsSync(absolutePath)) {
|
|
1094
|
+
log.error(`${label} artifact not found: ${formatPath(absolutePath)}`);
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
const content = fs8.readFileSync(absolutePath, "utf-8");
|
|
1099
|
+
const artifact = JSON.parse(content);
|
|
1100
|
+
if (!isCodeFlowArtifact(artifact)) {
|
|
1101
|
+
log.error(`${label} file is not a valid Monarch artifact`);
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
return artifact;
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
log.error(`Failed to parse ${label} artifact: ${error}`);
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function calculateDiff2(baseline, current) {
|
|
1111
|
+
const baselineFnIds = new Set(baseline.nodes.functions.map((fn) => fn.id));
|
|
1112
|
+
const currentFnIds = new Set(current.nodes.functions.map((fn) => fn.id));
|
|
1113
|
+
const addedFunctions = current.nodes.functions.filter((fn) => !baselineFnIds.has(fn.id)).map((fn) => `${fn.name} (${fn.file}:${fn.startLine})`);
|
|
1114
|
+
const removedFunctions = baseline.nodes.functions.filter((fn) => !currentFnIds.has(fn.id)).map((fn) => `${fn.name} (${fn.file}:${fn.startLine})`);
|
|
1115
|
+
const baselineModIds = new Set(baseline.nodes.modules.map((m) => m.id));
|
|
1116
|
+
const currentModIds = new Set(current.nodes.modules.map((m) => m.id));
|
|
1117
|
+
const addedModules = current.nodes.modules.filter((m) => !baselineModIds.has(m.id)).map((m) => m.path);
|
|
1118
|
+
const removedModules = baseline.nodes.modules.filter((m) => !currentModIds.has(m.id)).map((m) => m.path);
|
|
1119
|
+
const edgeKey = (e) => `${e.from}->${e.to}`;
|
|
1120
|
+
const baselineCallEdges = new Set(baseline.edges.calls.map(edgeKey));
|
|
1121
|
+
const currentCallEdges = new Set(current.edges.calls.map(edgeKey));
|
|
1122
|
+
const addedCallEdges = current.edges.calls.filter((e) => !baselineCallEdges.has(edgeKey(e))).map((e) => formatEdge2(e, current));
|
|
1123
|
+
const removedCallEdges = baseline.edges.calls.filter((e) => !currentCallEdges.has(edgeKey(e))).map((e) => formatEdge2(e, baseline));
|
|
1124
|
+
const baselineDataflowEdges = new Set(baseline.edges.dataflow.map(edgeKey));
|
|
1125
|
+
const currentDataflowEdges = new Set(current.edges.dataflow.map(edgeKey));
|
|
1126
|
+
const addedDataflowEdges = current.edges.dataflow.filter((e) => !baselineDataflowEdges.has(edgeKey(e))).map((e) => formatEdge2(e, current));
|
|
1127
|
+
const removedDataflowEdges = baseline.edges.dataflow.filter((e) => !currentDataflowEdges.has(edgeKey(e))).map((e) => formatEdge2(e, baseline));
|
|
1128
|
+
const hasChanges = addedFunctions.length > 0 || removedFunctions.length > 0 || addedModules.length > 0 || removedModules.length > 0 || addedCallEdges.length > 0 || removedCallEdges.length > 0 || addedDataflowEdges.length > 0 || removedDataflowEdges.length > 0;
|
|
1129
|
+
return {
|
|
1130
|
+
hasChanges,
|
|
1131
|
+
summary: {
|
|
1132
|
+
functionsAdded: addedFunctions.length,
|
|
1133
|
+
functionsRemoved: removedFunctions.length,
|
|
1134
|
+
modulesAdded: addedModules.length,
|
|
1135
|
+
modulesRemoved: removedModules.length,
|
|
1136
|
+
callEdgesAdded: addedCallEdges.length,
|
|
1137
|
+
callEdgesRemoved: removedCallEdges.length,
|
|
1138
|
+
dataflowEdgesAdded: addedDataflowEdges.length,
|
|
1139
|
+
dataflowEdgesRemoved: removedDataflowEdges.length
|
|
1140
|
+
},
|
|
1141
|
+
details: {
|
|
1142
|
+
addedFunctions,
|
|
1143
|
+
removedFunctions,
|
|
1144
|
+
addedModules,
|
|
1145
|
+
removedModules,
|
|
1146
|
+
addedCallEdges,
|
|
1147
|
+
removedCallEdges,
|
|
1148
|
+
addedDataflowEdges,
|
|
1149
|
+
removedDataflowEdges
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
function formatEdge2(edge, artifact) {
|
|
1154
|
+
const fromFn = artifact.nodes.functions.find((fn) => fn.id === edge.from);
|
|
1155
|
+
const toFn = artifact.nodes.functions.find((fn) => fn.id === edge.to);
|
|
1156
|
+
return `${fromFn?.name || edge.from} \u2192 ${toFn?.name || edge.to}`;
|
|
1157
|
+
}
|
|
1158
|
+
function printTextDiff2(diff, baselinePath, currentPath, only) {
|
|
1159
|
+
log.title("Monarch Graph Diff");
|
|
1160
|
+
console.log(chalk2.dim(`Baseline: ${baselinePath}`));
|
|
1161
|
+
console.log(chalk2.dim(`Current: ${currentPath}`));
|
|
1162
|
+
console.log("");
|
|
1163
|
+
if (!diff.hasChanges) {
|
|
1164
|
+
log.success("No changes detected");
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
console.log(chalk2.bold("Summary:"));
|
|
1168
|
+
if (!only || only === "functions") {
|
|
1169
|
+
console.log(` Functions: ${formatDiff(diff.summary.functionsAdded, diff.summary.functionsRemoved)}`);
|
|
1170
|
+
}
|
|
1171
|
+
if (!only || only === "modules") {
|
|
1172
|
+
console.log(` Modules: ${formatDiff(diff.summary.modulesAdded, diff.summary.modulesRemoved)}`);
|
|
1173
|
+
}
|
|
1174
|
+
if (!only || only === "edges") {
|
|
1175
|
+
console.log(` Call Edges: ${formatDiff(diff.summary.callEdgesAdded, diff.summary.callEdgesRemoved)}`);
|
|
1176
|
+
console.log(` Dataflow Edges: ${formatDiff(diff.summary.dataflowEdgesAdded, diff.summary.dataflowEdgesRemoved)}`);
|
|
1177
|
+
}
|
|
1178
|
+
console.log("");
|
|
1179
|
+
if ((!only || only === "functions") && diff.details.addedFunctions.length > 0) {
|
|
1180
|
+
console.log(chalk2.green.bold("Added Functions:"));
|
|
1181
|
+
diff.details.addedFunctions.forEach((fn) => console.log(chalk2.green(` + ${fn}`)));
|
|
1182
|
+
console.log("");
|
|
1183
|
+
}
|
|
1184
|
+
if ((!only || only === "functions") && diff.details.removedFunctions.length > 0) {
|
|
1185
|
+
console.log(chalk2.red.bold("Removed Functions:"));
|
|
1186
|
+
diff.details.removedFunctions.forEach((fn) => console.log(chalk2.red(` - ${fn}`)));
|
|
1187
|
+
console.log("");
|
|
1188
|
+
}
|
|
1189
|
+
if ((!only || only === "modules") && diff.details.addedModules.length > 0) {
|
|
1190
|
+
console.log(chalk2.green.bold("Added Modules:"));
|
|
1191
|
+
diff.details.addedModules.forEach((m) => console.log(chalk2.green(` + ${m}`)));
|
|
1192
|
+
console.log("");
|
|
1193
|
+
}
|
|
1194
|
+
if ((!only || only === "modules") && diff.details.removedModules.length > 0) {
|
|
1195
|
+
console.log(chalk2.red.bold("Removed Modules:"));
|
|
1196
|
+
diff.details.removedModules.forEach((m) => console.log(chalk2.red(` - ${m}`)));
|
|
1197
|
+
console.log("");
|
|
1198
|
+
}
|
|
1199
|
+
if ((!only || only === "edges") && diff.details.addedCallEdges.length > 0) {
|
|
1200
|
+
console.log(chalk2.green.bold("Added Call Edges:"));
|
|
1201
|
+
diff.details.addedCallEdges.forEach((e) => console.log(chalk2.green(` + ${e}`)));
|
|
1202
|
+
console.log("");
|
|
1203
|
+
}
|
|
1204
|
+
if ((!only || only === "edges") && diff.details.removedCallEdges.length > 0) {
|
|
1205
|
+
console.log(chalk2.red.bold("Removed Call Edges:"));
|
|
1206
|
+
diff.details.removedCallEdges.forEach((e) => console.log(chalk2.red(` - ${e}`)));
|
|
1207
|
+
console.log("");
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
function printMarkdownDiff(diff, baselinePath, currentPath, only) {
|
|
1211
|
+
console.log("# Monarch Graph Diff\n");
|
|
1212
|
+
console.log(`- **Baseline:** \`${baselinePath}\``);
|
|
1213
|
+
console.log(`- **Current:** \`${currentPath}\`
|
|
1214
|
+
`);
|
|
1215
|
+
if (!diff.hasChanges) {
|
|
1216
|
+
console.log("\u2705 No changes detected\n");
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
console.log("## Summary\n");
|
|
1220
|
+
console.log("| Category | Added | Removed |");
|
|
1221
|
+
console.log("|----------|-------|---------|");
|
|
1222
|
+
if (!only || only === "functions") {
|
|
1223
|
+
console.log(`| Functions | ${diff.summary.functionsAdded} | ${diff.summary.functionsRemoved} |`);
|
|
1224
|
+
}
|
|
1225
|
+
if (!only || only === "modules") {
|
|
1226
|
+
console.log(`| Modules | ${diff.summary.modulesAdded} | ${diff.summary.modulesRemoved} |`);
|
|
1227
|
+
}
|
|
1228
|
+
if (!only || only === "edges") {
|
|
1229
|
+
console.log(`| Call Edges | ${diff.summary.callEdgesAdded} | ${diff.summary.callEdgesRemoved} |`);
|
|
1230
|
+
console.log(`| Dataflow Edges | ${diff.summary.dataflowEdgesAdded} | ${diff.summary.dataflowEdgesRemoved} |`);
|
|
1231
|
+
}
|
|
1232
|
+
console.log("");
|
|
1233
|
+
if ((!only || only === "functions") && diff.details.addedFunctions.length > 0) {
|
|
1234
|
+
console.log("### Added Functions\n");
|
|
1235
|
+
diff.details.addedFunctions.forEach((fn) => console.log(`- \`${fn}\``));
|
|
1236
|
+
console.log("");
|
|
1237
|
+
}
|
|
1238
|
+
if ((!only || only === "functions") && diff.details.removedFunctions.length > 0) {
|
|
1239
|
+
console.log("### Removed Functions\n");
|
|
1240
|
+
diff.details.removedFunctions.forEach((fn) => console.log(`- \`${fn}\``));
|
|
1241
|
+
console.log("");
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// src/commands/serve.ts
|
|
1246
|
+
init_dist();
|
|
1247
|
+
import fs9 from "fs";
|
|
1248
|
+
import path9 from "path";
|
|
1249
|
+
import { createServer } from "http";
|
|
1250
|
+
async function serveCommand2(options) {
|
|
1251
|
+
const artifactPath = path9.resolve(options.artifact);
|
|
1252
|
+
const port = parseInt(options.port, 10);
|
|
1253
|
+
if (!fs9.existsSync(artifactPath)) {
|
|
1254
|
+
log.error(`Artifact not found: ${formatPath(artifactPath)}`);
|
|
1255
|
+
log.info("Run `monarch analyze` first to generate a graph");
|
|
1256
|
+
process.exit(1);
|
|
1257
|
+
}
|
|
1258
|
+
try {
|
|
1259
|
+
const content = fs9.readFileSync(artifactPath, "utf-8");
|
|
1260
|
+
const artifact = JSON.parse(content);
|
|
1261
|
+
if (!isCodeFlowArtifact(artifact)) {
|
|
1262
|
+
log.error("Invalid artifact file");
|
|
1263
|
+
process.exit(1);
|
|
1264
|
+
}
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
log.error(`Failed to read artifact: ${error}`);
|
|
1267
|
+
process.exit(1);
|
|
1268
|
+
}
|
|
1269
|
+
const server = createServer((req, res) => {
|
|
1270
|
+
const url = req.url || "/";
|
|
1271
|
+
if (url === "/api/artifact") {
|
|
1272
|
+
res.setHeader("Content-Type", "application/json");
|
|
1273
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1274
|
+
const content = fs9.readFileSync(artifactPath, "utf-8");
|
|
1275
|
+
res.end(content);
|
|
1276
|
+
} else if (url === "/") {
|
|
1277
|
+
res.setHeader("Content-Type", "text/html");
|
|
1278
|
+
res.end(getIndexHtml(port));
|
|
1279
|
+
} else {
|
|
1280
|
+
res.statusCode = 404;
|
|
1281
|
+
res.end("Not Found");
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
server.listen(port, () => {
|
|
1285
|
+
log.title("Monarch Visualization Server");
|
|
1286
|
+
log.success(`Server running at http://localhost:${port}`);
|
|
1287
|
+
log.info(`Serving artifact: ${formatPath(artifactPath)}`);
|
|
1288
|
+
log.dim("\nPress Ctrl+C to stop");
|
|
1289
|
+
if (options.open) {
|
|
1290
|
+
const url = `http://localhost:${port}`;
|
|
1291
|
+
import("child_process").then(({ exec: exec2 }) => {
|
|
1292
|
+
const cmd = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
1293
|
+
exec2(`${cmd} ${url}`);
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
process.on("SIGINT", () => {
|
|
1298
|
+
log.info("\nShutting down server...");
|
|
1299
|
+
server.close(() => {
|
|
1300
|
+
process.exit(0);
|
|
1301
|
+
});
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
function getIndexHtml(port) {
|
|
1305
|
+
return `<!DOCTYPE html>
|
|
1306
|
+
<html lang="en">
|
|
1307
|
+
<head>
|
|
1308
|
+
<meta charset="UTF-8">
|
|
1309
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1310
|
+
<title>Monarch - Graph Visualization</title>
|
|
1311
|
+
<style>
|
|
1312
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1313
|
+
body {
|
|
1314
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1315
|
+
background: #16213e;
|
|
1316
|
+
color: #fff;
|
|
1317
|
+
min-height: 100vh;
|
|
1318
|
+
display: flex;
|
|
1319
|
+
align-items: center;
|
|
1320
|
+
justify-content: center;
|
|
1321
|
+
}
|
|
1322
|
+
.container {
|
|
1323
|
+
text-align: center;
|
|
1324
|
+
padding: 2rem;
|
|
1325
|
+
}
|
|
1326
|
+
h1 {
|
|
1327
|
+
font-size: 2.5rem;
|
|
1328
|
+
margin-bottom: 1rem;
|
|
1329
|
+
color: #4a90d9;
|
|
1330
|
+
}
|
|
1331
|
+
p {
|
|
1332
|
+
color: #888;
|
|
1333
|
+
margin-bottom: 2rem;
|
|
1334
|
+
}
|
|
1335
|
+
.info {
|
|
1336
|
+
background: #1a1a2e;
|
|
1337
|
+
padding: 1.5rem;
|
|
1338
|
+
border-radius: 8px;
|
|
1339
|
+
margin-bottom: 2rem;
|
|
1340
|
+
}
|
|
1341
|
+
.info code {
|
|
1342
|
+
background: #2d2d44;
|
|
1343
|
+
padding: 0.25rem 0.5rem;
|
|
1344
|
+
border-radius: 4px;
|
|
1345
|
+
font-family: monospace;
|
|
1346
|
+
}
|
|
1347
|
+
a {
|
|
1348
|
+
color: #4a90d9;
|
|
1349
|
+
text-decoration: none;
|
|
1350
|
+
}
|
|
1351
|
+
a:hover {
|
|
1352
|
+
text-decoration: underline;
|
|
1353
|
+
}
|
|
1354
|
+
</style>
|
|
1355
|
+
</head>
|
|
1356
|
+
<body>
|
|
1357
|
+
<div class="container">
|
|
1358
|
+
<h1>\u{1F52E} Monarch</h1>
|
|
1359
|
+
<p>Static Codebase Flow Mapping</p>
|
|
1360
|
+
<div class="info">
|
|
1361
|
+
<p>API Endpoint: <code>http://localhost:${port}/api/artifact</code></p>
|
|
1362
|
+
</div>
|
|
1363
|
+
<p>
|
|
1364
|
+
To view the full visualization, run the UI package:<br>
|
|
1365
|
+
<code>pnpm --filter @codeflow/ui dev</code>
|
|
1366
|
+
</p>
|
|
1367
|
+
</div>
|
|
1368
|
+
</body>
|
|
1369
|
+
</html>`;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/commands/init.ts
|
|
1373
|
+
import fs10 from "fs";
|
|
1374
|
+
import path10 from "path";
|
|
1375
|
+
var DEFAULT_CONFIG = {
|
|
1376
|
+
version: "1.0",
|
|
1377
|
+
exporter: "mock",
|
|
1378
|
+
output: "monarch-graph.json",
|
|
1379
|
+
include: ["src/**/*.ts", "src/**/*.tsx"],
|
|
1380
|
+
exclude: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"]
|
|
1381
|
+
};
|
|
1382
|
+
async function initCommand(options) {
|
|
1383
|
+
const configPath = path10.resolve("monarch.json");
|
|
1384
|
+
if (fs10.existsSync(configPath) && !options.force) {
|
|
1385
|
+
log.error(`Configuration file already exists: ${formatPath(configPath)}`);
|
|
1386
|
+
log.info("Use --force to overwrite");
|
|
1387
|
+
process.exit(1);
|
|
1388
|
+
}
|
|
1389
|
+
fs10.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
1390
|
+
log.success(`Created configuration file: ${formatPath(configPath)}`);
|
|
1391
|
+
const gitignorePath = path10.resolve(".gitignore");
|
|
1392
|
+
const gitignoreEntry = "\n# Monarch\nmonarch-graph.json\n.monarch/\n";
|
|
1393
|
+
if (fs10.existsSync(gitignorePath)) {
|
|
1394
|
+
const content = fs10.readFileSync(gitignorePath, "utf-8");
|
|
1395
|
+
if (!content.includes("monarch-graph.json")) {
|
|
1396
|
+
log.info(`Consider adding to ${formatPath(".gitignore")}:`);
|
|
1397
|
+
log.dim(gitignoreEntry.trim());
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
log.title("Monarch initialized successfully!");
|
|
1401
|
+
log.info("Next steps:");
|
|
1402
|
+
log.dim(" 1. Run `monarch analyze` to generate your first graph");
|
|
1403
|
+
log.dim(" 2. Run `monarch serve` to visualize the graph");
|
|
1404
|
+
log.dim(" 3. Run `monarch diff <old> <new>` to compare graphs");
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// src/commands/list.ts
|
|
1408
|
+
init_dist();
|
|
1409
|
+
import fs11 from "fs";
|
|
1410
|
+
import path11 from "path";
|
|
1411
|
+
import chalk3 from "chalk";
|
|
1412
|
+
async function listCommand(artifactPath, options) {
|
|
1413
|
+
const absolutePath = path11.resolve(artifactPath);
|
|
1414
|
+
if (!fs11.existsSync(absolutePath)) {
|
|
1415
|
+
log.error(`Artifact not found: ${formatPath(absolutePath)}`);
|
|
1416
|
+
log.info("Run `monarch analyze` first to generate a graph");
|
|
1417
|
+
process.exit(1);
|
|
1418
|
+
}
|
|
1419
|
+
let artifact;
|
|
1420
|
+
try {
|
|
1421
|
+
const content = fs11.readFileSync(absolutePath, "utf-8");
|
|
1422
|
+
artifact = JSON.parse(content);
|
|
1423
|
+
if (!isCodeFlowArtifact(artifact)) {
|
|
1424
|
+
log.error("Invalid artifact file");
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
log.error(`Failed to read artifact: ${error}`);
|
|
1429
|
+
process.exit(1);
|
|
1430
|
+
}
|
|
1431
|
+
if (options.json) {
|
|
1432
|
+
printJsonOutput(artifact, options.type);
|
|
1433
|
+
} else {
|
|
1434
|
+
printTextOutput(artifact, options.type);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
function printJsonOutput(artifact, type) {
|
|
1438
|
+
let output;
|
|
1439
|
+
switch (type) {
|
|
1440
|
+
case "functions":
|
|
1441
|
+
output = artifact.nodes.functions;
|
|
1442
|
+
break;
|
|
1443
|
+
case "modules":
|
|
1444
|
+
output = artifact.nodes.modules;
|
|
1445
|
+
break;
|
|
1446
|
+
case "components":
|
|
1447
|
+
output = artifact.nodes.components;
|
|
1448
|
+
break;
|
|
1449
|
+
case "edges":
|
|
1450
|
+
output = {
|
|
1451
|
+
calls: artifact.edges.calls,
|
|
1452
|
+
dataflow: artifact.edges.dataflow
|
|
1453
|
+
};
|
|
1454
|
+
break;
|
|
1455
|
+
default:
|
|
1456
|
+
output = {
|
|
1457
|
+
metadata: artifact.metadata,
|
|
1458
|
+
summary: {
|
|
1459
|
+
functions: artifact.nodes.functions.length,
|
|
1460
|
+
modules: artifact.nodes.modules.length,
|
|
1461
|
+
components: artifact.nodes.components.length,
|
|
1462
|
+
callEdges: artifact.edges.calls.length,
|
|
1463
|
+
dataflowEdges: artifact.edges.dataflow.length
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1468
|
+
}
|
|
1469
|
+
function printTextOutput(artifact, type) {
|
|
1470
|
+
log.title("Monarch Graph Contents");
|
|
1471
|
+
console.log(chalk3.bold("Metadata:"));
|
|
1472
|
+
console.log(` Version: ${artifact.metadata.analyzerVersion}`);
|
|
1473
|
+
console.log(` Language: ${artifact.metadata.language}`);
|
|
1474
|
+
console.log(` Commit: ${artifact.metadata.repoCommit}`);
|
|
1475
|
+
console.log(` Generated: ${artifact.metadata.generatedAt}`);
|
|
1476
|
+
console.log("");
|
|
1477
|
+
console.log(chalk3.bold("Summary:"));
|
|
1478
|
+
console.log(` Functions: ${formatNumber(artifact.nodes.functions.length)}`);
|
|
1479
|
+
console.log(` Modules: ${formatNumber(artifact.nodes.modules.length)}`);
|
|
1480
|
+
console.log(` Components: ${formatNumber(artifact.nodes.components.length)}`);
|
|
1481
|
+
console.log(` Call Edges: ${formatNumber(artifact.edges.calls.length)}`);
|
|
1482
|
+
console.log(` Dataflow Edges: ${formatNumber(artifact.edges.dataflow.length)}`);
|
|
1483
|
+
console.log("");
|
|
1484
|
+
if (!type || type === "functions") {
|
|
1485
|
+
console.log(chalk3.bold("Functions:"));
|
|
1486
|
+
const grouped = groupByFile(artifact.nodes.functions);
|
|
1487
|
+
for (const [file, fns] of Object.entries(grouped)) {
|
|
1488
|
+
console.log(chalk3.cyan(` ${file}`));
|
|
1489
|
+
for (const fn of fns) {
|
|
1490
|
+
console.log(` ${fn.name} ${chalk3.dim(`(${fn.startLine}-${fn.endLine})`)}`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
console.log("");
|
|
1494
|
+
}
|
|
1495
|
+
if (!type || type === "modules") {
|
|
1496
|
+
console.log(chalk3.bold("Modules:"));
|
|
1497
|
+
for (const mod of artifact.nodes.modules) {
|
|
1498
|
+
const fnCount = mod.functionIds?.length || 0;
|
|
1499
|
+
console.log(` ${mod.path} ${chalk3.dim(`(${fnCount} functions)`)}`);
|
|
1500
|
+
}
|
|
1501
|
+
console.log("");
|
|
1502
|
+
}
|
|
1503
|
+
if (!type || type === "components") {
|
|
1504
|
+
console.log(chalk3.bold("Components:"));
|
|
1505
|
+
for (const comp of artifact.nodes.components) {
|
|
1506
|
+
const modCount = comp.moduleIds?.length || 0;
|
|
1507
|
+
console.log(` ${comp.name} ${chalk3.dim(`(${modCount} modules)`)}`);
|
|
1508
|
+
}
|
|
1509
|
+
console.log("");
|
|
1510
|
+
}
|
|
1511
|
+
if (!type || type === "edges") {
|
|
1512
|
+
if (artifact.edges.calls.length > 0) {
|
|
1513
|
+
console.log(chalk3.bold("Call Edges:"));
|
|
1514
|
+
for (const edge of artifact.edges.calls.slice(0, 10)) {
|
|
1515
|
+
const from = artifact.nodes.functions.find((f) => f.id === edge.from);
|
|
1516
|
+
const to = artifact.nodes.functions.find((f) => f.id === edge.to);
|
|
1517
|
+
console.log(` ${from?.name || edge.from} ${chalk3.dim("\u2192")} ${to?.name || edge.to}`);
|
|
1518
|
+
}
|
|
1519
|
+
if (artifact.edges.calls.length > 10) {
|
|
1520
|
+
console.log(chalk3.dim(` ... and ${artifact.edges.calls.length - 10} more`));
|
|
1521
|
+
}
|
|
1522
|
+
console.log("");
|
|
1523
|
+
}
|
|
1524
|
+
if (artifact.edges.dataflow.length > 0) {
|
|
1525
|
+
console.log(chalk3.bold("Dataflow Edges:"));
|
|
1526
|
+
for (const edge of artifact.edges.dataflow.slice(0, 10)) {
|
|
1527
|
+
const from = artifact.nodes.functions.find((f) => f.id === edge.from);
|
|
1528
|
+
const to = artifact.nodes.functions.find((f) => f.id === edge.to);
|
|
1529
|
+
console.log(` ${from?.name || edge.from} ${chalk3.yellow("\u27FF")} ${to?.name || edge.to}`);
|
|
1530
|
+
if (edge.label) {
|
|
1531
|
+
console.log(chalk3.dim(` ${edge.label}`));
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (artifact.edges.dataflow.length > 10) {
|
|
1535
|
+
console.log(chalk3.dim(` ... and ${artifact.edges.dataflow.length - 10} more`));
|
|
1536
|
+
}
|
|
1537
|
+
console.log("");
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
function groupByFile(items) {
|
|
1542
|
+
return items.reduce((acc, item) => {
|
|
1543
|
+
if (!acc[item.file]) {
|
|
1544
|
+
acc[item.file] = [];
|
|
1545
|
+
}
|
|
1546
|
+
acc[item.file].push(item);
|
|
1547
|
+
return acc;
|
|
1548
|
+
}, {});
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/commands/help.ts
|
|
1552
|
+
import chalk4 from "chalk";
|
|
1553
|
+
var commands = {
|
|
1554
|
+
init: {
|
|
1555
|
+
name: "init",
|
|
1556
|
+
description: "Initialize a new Monarch project in the current directory",
|
|
1557
|
+
usage: "monarch init [options]",
|
|
1558
|
+
options: [
|
|
1559
|
+
{ flag: "-f, --force", description: "Overwrite existing configuration", default: "false" }
|
|
1560
|
+
],
|
|
1561
|
+
examples: [
|
|
1562
|
+
"monarch init",
|
|
1563
|
+
"monarch init --force"
|
|
1564
|
+
]
|
|
1565
|
+
},
|
|
1566
|
+
analyze: {
|
|
1567
|
+
name: "analyze",
|
|
1568
|
+
alias: "a",
|
|
1569
|
+
description: "Analyze a codebase and generate a flow artifact containing functions, modules, call edges, and dataflow relationships",
|
|
1570
|
+
usage: "monarch analyze [path] [options]",
|
|
1571
|
+
options: [
|
|
1572
|
+
{ flag: "-o, --output <path>", description: "Output artifact file path", default: "monarch-graph.json" },
|
|
1573
|
+
{ flag: "-e, --exporter <name>", description: "Exporter to use (mock, codeql-ts)", default: "mock" },
|
|
1574
|
+
{ flag: "-w, --watch", description: "Watch for changes and re-analyze", default: "false" }
|
|
1575
|
+
],
|
|
1576
|
+
examples: [
|
|
1577
|
+
"monarch analyze",
|
|
1578
|
+
"monarch analyze ./src",
|
|
1579
|
+
"monarch analyze ./src -o graph.json",
|
|
1580
|
+
"monarch analyze ./src --exporter codeql-ts",
|
|
1581
|
+
"monarch a ./src -o graph.json"
|
|
1582
|
+
]
|
|
1583
|
+
},
|
|
1584
|
+
diff: {
|
|
1585
|
+
name: "diff",
|
|
1586
|
+
alias: "d",
|
|
1587
|
+
description: "Compare two graph artifacts and show changes in functions, modules, and edges. Useful for tracking code structure changes between versions",
|
|
1588
|
+
usage: "monarch diff <baseline> <current> [options]",
|
|
1589
|
+
options: [
|
|
1590
|
+
{ flag: "-f, --format <type>", description: "Output format (text, json, markdown)", default: "text" },
|
|
1591
|
+
{ flag: "--exit-on-changes", description: "Exit with code 1 if changes detected (for CI)", default: "false" },
|
|
1592
|
+
{ flag: "--only <type>", description: "Show only specific changes (functions, modules, edges)" }
|
|
1593
|
+
],
|
|
1594
|
+
examples: [
|
|
1595
|
+
"monarch diff baseline.json current.json",
|
|
1596
|
+
"monarch diff baseline.json current.json --format json",
|
|
1597
|
+
"monarch diff baseline.json current.json --format markdown",
|
|
1598
|
+
"monarch diff baseline.json current.json --exit-on-changes",
|
|
1599
|
+
"monarch d old.json new.json --only functions"
|
|
1600
|
+
]
|
|
1601
|
+
},
|
|
1602
|
+
serve: {
|
|
1603
|
+
name: "serve",
|
|
1604
|
+
alias: "s",
|
|
1605
|
+
description: "Start an HTTP server to serve the artifact and visualization UI",
|
|
1606
|
+
usage: "monarch serve [options]",
|
|
1607
|
+
options: [
|
|
1608
|
+
{ flag: "-p, --port <number>", description: "Port to serve on", default: "3000" },
|
|
1609
|
+
{ flag: "-a, --artifact <path>", description: "Artifact file path", default: "monarch-graph.json" },
|
|
1610
|
+
{ flag: "--open", description: "Open browser automatically", default: "false" }
|
|
1611
|
+
],
|
|
1612
|
+
examples: [
|
|
1613
|
+
"monarch serve",
|
|
1614
|
+
"monarch serve --port 8080",
|
|
1615
|
+
"monarch serve --artifact my-graph.json --open",
|
|
1616
|
+
"monarch s -p 4000"
|
|
1617
|
+
]
|
|
1618
|
+
},
|
|
1619
|
+
list: {
|
|
1620
|
+
name: "list",
|
|
1621
|
+
alias: "ls",
|
|
1622
|
+
description: "List and inspect contents of a graph artifact including metadata, functions, modules, components, and edges",
|
|
1623
|
+
usage: "monarch list [artifact] [options]",
|
|
1624
|
+
options: [
|
|
1625
|
+
{ flag: "-t, --type <type>", description: "Filter by type (functions, modules, components, edges)" },
|
|
1626
|
+
{ flag: "--json", description: "Output as JSON", default: "false" }
|
|
1627
|
+
],
|
|
1628
|
+
examples: [
|
|
1629
|
+
"monarch list",
|
|
1630
|
+
"monarch list my-graph.json",
|
|
1631
|
+
"monarch list --type functions",
|
|
1632
|
+
"monarch list --json",
|
|
1633
|
+
"monarch ls -t modules --json"
|
|
1634
|
+
]
|
|
1635
|
+
},
|
|
1636
|
+
run: {
|
|
1637
|
+
name: "run",
|
|
1638
|
+
alias: "r",
|
|
1639
|
+
description: "All-in-one command that analyzes the codebase, detects changes from the previous graph, updates the artifact if needed, and starts the visualization server. Perfect for development workflow.",
|
|
1640
|
+
usage: "monarch run [path] [options]",
|
|
1641
|
+
options: [
|
|
1642
|
+
{ flag: "-o, --output <path>", description: "Output artifact file path", default: "monarch-graph.json" },
|
|
1643
|
+
{ flag: "-e, --exporter <name>", description: "Exporter to use (mock, codeql-ts)", default: "mock" },
|
|
1644
|
+
{ flag: "-p, --port <number>", description: "Port for visualization server", default: "3000" },
|
|
1645
|
+
{ flag: "--open", description: "Open browser automatically", default: "false" },
|
|
1646
|
+
{ flag: "-f, --force", description: "Force regeneration even if no changes detected", default: "false" }
|
|
1647
|
+
],
|
|
1648
|
+
examples: [
|
|
1649
|
+
"monarch run",
|
|
1650
|
+
"monarch run ./src",
|
|
1651
|
+
"monarch run ./src --open",
|
|
1652
|
+
"monarch run ./src -p 8080 --open",
|
|
1653
|
+
"monarch r ./src -o my-graph.json --force"
|
|
1654
|
+
]
|
|
1655
|
+
},
|
|
1656
|
+
graph: {
|
|
1657
|
+
name: "graph",
|
|
1658
|
+
alias: "g",
|
|
1659
|
+
description: "Render an ASCII graph visualization directly in the terminal. Supports different view levels (components, modules, functions) and can expand specific containers to show their internal structure.",
|
|
1660
|
+
usage: "monarch graph [artifact] [options]",
|
|
1661
|
+
options: [
|
|
1662
|
+
{ flag: "-l, --level <level>", description: "View level (components, modules, functions)", default: "modules" },
|
|
1663
|
+
{ flag: "-x, --expand <name>", description: "Expand a specific component or module by name" },
|
|
1664
|
+
{ flag: "--expand-type <type>", description: "Type of item to expand (component, module)" },
|
|
1665
|
+
{ flag: "--list", description: "List expandable components and modules", default: "false" },
|
|
1666
|
+
{ flag: "--no-color", description: "Disable colored output" },
|
|
1667
|
+
{ flag: "-w, --width <cols>", description: "Max output width in columns" }
|
|
1668
|
+
],
|
|
1669
|
+
examples: [
|
|
1670
|
+
"monarch graph",
|
|
1671
|
+
"monarch graph --level components",
|
|
1672
|
+
"monarch graph --level functions",
|
|
1673
|
+
"monarch graph --list",
|
|
1674
|
+
"monarch graph --expand src-utils",
|
|
1675
|
+
"monarch graph -x auth.ts --expand-type module",
|
|
1676
|
+
"monarch g -l components"
|
|
1677
|
+
]
|
|
1678
|
+
}
|
|
1679
|
+
};
|
|
1680
|
+
function helpCommand(command) {
|
|
1681
|
+
if (command && commands[command]) {
|
|
1682
|
+
printCommandHelp(commands[command]);
|
|
1683
|
+
} else if (command) {
|
|
1684
|
+
console.error(chalk4.red(`Unknown command: ${command}`));
|
|
1685
|
+
console.log(`
|
|
1686
|
+
Run ${chalk4.cyan("monarch help")} to see all available commands.`);
|
|
1687
|
+
process.exit(1);
|
|
1688
|
+
} else {
|
|
1689
|
+
printGeneralHelp();
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
function printGeneralHelp() {
|
|
1693
|
+
console.log(chalk4.bold.cyan(`
|
|
1694
|
+
__ __ _
|
|
1695
|
+
| \\/ | ___ _ __ __ _ _ __ ___| |__
|
|
1696
|
+
| |\\/| |/ _ \\| '_ \\ / _\` | '__/ __| '_ \\
|
|
1697
|
+
| | | | (_) | | | | (_| | | | (__| | | |
|
|
1698
|
+
|_| |_|\\___/|_| |_|\\__,_|_| \\___|_| |_|
|
|
1699
|
+
`));
|
|
1700
|
+
console.log(chalk4.bold("Monarch") + " - Static codebase flow mapping and visualization\n");
|
|
1701
|
+
console.log(chalk4.dim("Analyze TypeScript/JavaScript codebases to extract function call graphs"));
|
|
1702
|
+
console.log(chalk4.dim("and dataflow relationships for visualization and change tracking.\n"));
|
|
1703
|
+
console.log(chalk4.bold.underline("USAGE"));
|
|
1704
|
+
console.log(` ${chalk4.cyan("monarch")} ${chalk4.yellow("<command>")} ${chalk4.dim("[options]")}
|
|
1705
|
+
`);
|
|
1706
|
+
console.log(chalk4.bold.underline("COMMANDS"));
|
|
1707
|
+
console.log();
|
|
1708
|
+
const commandList = [
|
|
1709
|
+
{ cmd: "run", alias: "r", desc: "Analyze, detect changes, and start server (recommended)" },
|
|
1710
|
+
{ cmd: "init", alias: "", desc: "Initialize a new Monarch project" },
|
|
1711
|
+
{ cmd: "analyze", alias: "a", desc: "Analyze codebase and generate artifact" },
|
|
1712
|
+
{ cmd: "graph", alias: "g", desc: "Render ASCII graph in terminal" },
|
|
1713
|
+
{ cmd: "diff", alias: "d", desc: "Compare two artifacts for changes" },
|
|
1714
|
+
{ cmd: "serve", alias: "s", desc: "Start visualization server" },
|
|
1715
|
+
{ cmd: "list", alias: "ls", desc: "List artifact contents" },
|
|
1716
|
+
{ cmd: "help", alias: "", desc: "Show help for a command" }
|
|
1717
|
+
];
|
|
1718
|
+
for (const { cmd, alias, desc } of commandList) {
|
|
1719
|
+
const aliasStr = alias ? chalk4.dim(` (${alias})`) : "";
|
|
1720
|
+
console.log(` ${chalk4.cyan(cmd.padEnd(10))}${aliasStr.padEnd(8)} ${desc}`);
|
|
1721
|
+
}
|
|
1722
|
+
console.log();
|
|
1723
|
+
console.log(chalk4.bold.underline("QUICK START"));
|
|
1724
|
+
console.log();
|
|
1725
|
+
console.log(chalk4.dim(" # Fastest way: analyze and visualize in one command"));
|
|
1726
|
+
console.log(` ${chalk4.green("$")} monarch run ./src --open`);
|
|
1727
|
+
console.log();
|
|
1728
|
+
console.log(chalk4.dim(" # Or step by step:"));
|
|
1729
|
+
console.log(` ${chalk4.green("$")} monarch init`);
|
|
1730
|
+
console.log(` ${chalk4.green("$")} monarch analyze ./src`);
|
|
1731
|
+
console.log(` ${chalk4.green("$")} monarch serve --open`);
|
|
1732
|
+
console.log();
|
|
1733
|
+
console.log(chalk4.dim(" # Compare changes between versions"));
|
|
1734
|
+
console.log(` ${chalk4.green("$")} monarch diff baseline.json current.json`);
|
|
1735
|
+
console.log();
|
|
1736
|
+
console.log(chalk4.bold.underline("EXPORTERS"));
|
|
1737
|
+
console.log();
|
|
1738
|
+
console.log(` ${chalk4.cyan("mock")} Fast regex-based analysis (default, no dependencies)`);
|
|
1739
|
+
console.log(` ${chalk4.cyan("codeql-ts")} Production-grade CodeQL analysis (requires CodeQL CLI)`);
|
|
1740
|
+
console.log();
|
|
1741
|
+
console.log(chalk4.bold.underline("EXAMPLES"));
|
|
1742
|
+
console.log();
|
|
1743
|
+
console.log(chalk4.dim(" # Analyze with custom output"));
|
|
1744
|
+
console.log(` ${chalk4.green("$")} monarch analyze ./src -o my-graph.json`);
|
|
1745
|
+
console.log();
|
|
1746
|
+
console.log(chalk4.dim(" # Use CodeQL for production analysis"));
|
|
1747
|
+
console.log(` ${chalk4.green("$")} monarch analyze ./src --exporter codeql-ts`);
|
|
1748
|
+
console.log();
|
|
1749
|
+
console.log(chalk4.dim(" # CI pipeline with diff check"));
|
|
1750
|
+
console.log(` ${chalk4.green("$")} monarch diff baseline.json current.json --exit-on-changes`);
|
|
1751
|
+
console.log();
|
|
1752
|
+
console.log(chalk4.bold.underline("MORE INFORMATION"));
|
|
1753
|
+
console.log();
|
|
1754
|
+
console.log(` Run ${chalk4.cyan("monarch help <command>")} for detailed help on a specific command.`);
|
|
1755
|
+
console.log(` Documentation: ${chalk4.underline("https://github.com/your-username/monarch")}`);
|
|
1756
|
+
console.log();
|
|
1757
|
+
}
|
|
1758
|
+
function printCommandHelp(cmd) {
|
|
1759
|
+
const aliasStr = cmd.alias ? ` ${chalk4.dim(`(alias: ${cmd.alias})`)}` : "";
|
|
1760
|
+
console.log();
|
|
1761
|
+
console.log(chalk4.bold.cyan(`monarch ${cmd.name}`) + aliasStr);
|
|
1762
|
+
console.log();
|
|
1763
|
+
console.log(cmd.description);
|
|
1764
|
+
console.log();
|
|
1765
|
+
console.log(chalk4.bold.underline("USAGE"));
|
|
1766
|
+
console.log(` ${chalk4.cyan(cmd.usage)}`);
|
|
1767
|
+
console.log();
|
|
1768
|
+
if (cmd.options.length > 0) {
|
|
1769
|
+
console.log(chalk4.bold.underline("OPTIONS"));
|
|
1770
|
+
console.log();
|
|
1771
|
+
const maxFlagLen = Math.max(...cmd.options.map((o) => o.flag.length));
|
|
1772
|
+
for (const opt of cmd.options) {
|
|
1773
|
+
const defaultStr = opt.default !== void 0 ? chalk4.dim(` [default: ${opt.default}]`) : "";
|
|
1774
|
+
console.log(` ${chalk4.yellow(opt.flag.padEnd(maxFlagLen + 2))} ${opt.description}${defaultStr}`);
|
|
1775
|
+
}
|
|
1776
|
+
console.log();
|
|
1777
|
+
}
|
|
1778
|
+
console.log(chalk4.bold.underline("EXAMPLES"));
|
|
1779
|
+
console.log();
|
|
1780
|
+
for (const example of cmd.examples) {
|
|
1781
|
+
console.log(` ${chalk4.green("$")} ${example}`);
|
|
1782
|
+
}
|
|
1783
|
+
console.log();
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// src/commands/run.ts
|
|
1787
|
+
init_dist();
|
|
1788
|
+
init_dist2();
|
|
1789
|
+
import fs12 from "fs";
|
|
1790
|
+
import path12 from "path";
|
|
1791
|
+
import { createServer as createServer2 } from "http";
|
|
1792
|
+
import { exec } from "child_process";
|
|
1793
|
+
import ora2 from "ora";
|
|
1794
|
+
import chalk5 from "chalk";
|
|
1795
|
+
async function runCommand(targetPath, options) {
|
|
1796
|
+
const absolutePath = path12.resolve(targetPath);
|
|
1797
|
+
const artifactPath = path12.resolve(options.output);
|
|
1798
|
+
const port = parseInt(options.port, 10);
|
|
1799
|
+
if (!fs12.existsSync(absolutePath)) {
|
|
1800
|
+
log.error(`Path does not exist: ${formatPath(absolutePath)}`);
|
|
1801
|
+
process.exit(1);
|
|
1802
|
+
}
|
|
1803
|
+
log.title("Monarch Run");
|
|
1804
|
+
log.info(`Target: ${formatPath(absolutePath)}`);
|
|
1805
|
+
log.info(`Artifact: ${formatPath(artifactPath)}`);
|
|
1806
|
+
const existingArtifact = loadExistingArtifact(artifactPath);
|
|
1807
|
+
let artifact;
|
|
1808
|
+
let diffSummary = null;
|
|
1809
|
+
if (existingArtifact && !options.force) {
|
|
1810
|
+
log.info("");
|
|
1811
|
+
log.info("Existing graph found, checking for changes...");
|
|
1812
|
+
const newArtifact = await analyzeCodebase(absolutePath, options.exporter);
|
|
1813
|
+
if (!newArtifact) {
|
|
1814
|
+
process.exit(1);
|
|
1815
|
+
}
|
|
1816
|
+
diffSummary = calculateDiffSummary(existingArtifact, newArtifact);
|
|
1817
|
+
if (!diffSummary.hasChanges) {
|
|
1818
|
+
log.success("No changes detected, using existing graph");
|
|
1819
|
+
artifact = existingArtifact;
|
|
1820
|
+
} else {
|
|
1821
|
+
log.warning("Changes detected:");
|
|
1822
|
+
printDiffSummary(diffSummary);
|
|
1823
|
+
artifact = await applyElkLayout(newArtifact);
|
|
1824
|
+
saveArtifact(artifact, artifactPath);
|
|
1825
|
+
log.success(`Updated graph saved to ${formatPath(artifactPath)}`);
|
|
1826
|
+
}
|
|
1827
|
+
} else {
|
|
1828
|
+
if (options.force) {
|
|
1829
|
+
log.info("Force flag set, regenerating graph...");
|
|
1830
|
+
} else {
|
|
1831
|
+
log.info("No existing graph found, generating...");
|
|
1832
|
+
}
|
|
1833
|
+
const newArtifact = await analyzeCodebase(absolutePath, options.exporter);
|
|
1834
|
+
if (!newArtifact) {
|
|
1835
|
+
process.exit(1);
|
|
1836
|
+
}
|
|
1837
|
+
artifact = await applyElkLayout(newArtifact);
|
|
1838
|
+
saveArtifact(artifact, artifactPath);
|
|
1839
|
+
log.success(`Graph saved to ${formatPath(artifactPath)}`);
|
|
1840
|
+
}
|
|
1841
|
+
console.log("");
|
|
1842
|
+
log.info(chalk5.bold("Graph Summary:"));
|
|
1843
|
+
log.info(` Functions: ${formatNumber(artifact.nodes.functions.length)}`);
|
|
1844
|
+
log.info(` Modules: ${formatNumber(artifact.nodes.modules.length)}`);
|
|
1845
|
+
log.info(` Components: ${formatNumber(artifact.nodes.components.length)}`);
|
|
1846
|
+
log.info(` Call edges: ${formatNumber(artifact.edges.calls.length)}`);
|
|
1847
|
+
log.info(` Dataflow: ${formatNumber(artifact.edges.dataflow.length)}`);
|
|
1848
|
+
console.log("");
|
|
1849
|
+
await startServer(artifactPath, port, options.open);
|
|
1850
|
+
}
|
|
1851
|
+
function loadExistingArtifact(artifactPath) {
|
|
1852
|
+
if (!fs12.existsSync(artifactPath)) {
|
|
1853
|
+
return null;
|
|
1854
|
+
}
|
|
1855
|
+
try {
|
|
1856
|
+
const content = fs12.readFileSync(artifactPath, "utf-8");
|
|
1857
|
+
const artifact = JSON.parse(content);
|
|
1858
|
+
if (!isCodeFlowArtifact(artifact)) {
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
return artifact;
|
|
1862
|
+
} catch {
|
|
1863
|
+
return null;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
async function analyzeCodebase(targetPath, exporterName) {
|
|
1867
|
+
const spinner = ora2("Analyzing codebase...").start();
|
|
1868
|
+
try {
|
|
1869
|
+
let artifact;
|
|
1870
|
+
if (exporterName === "mock") {
|
|
1871
|
+
const exporter = new MockExporter(targetPath);
|
|
1872
|
+
await exporter.prepare();
|
|
1873
|
+
spinner.text = "Scanning files...";
|
|
1874
|
+
await exporter.analyze();
|
|
1875
|
+
spinner.text = "Generating artifact...";
|
|
1876
|
+
artifact = await exporter.export();
|
|
1877
|
+
await exporter.cleanup();
|
|
1878
|
+
} else if (exporterName === "codeql-ts") {
|
|
1879
|
+
spinner.text = "Creating CodeQL database...";
|
|
1880
|
+
const { CodeQLExporter: CodeQLExporter2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports));
|
|
1881
|
+
const exporter = new CodeQLExporter2(targetPath);
|
|
1882
|
+
await exporter.prepare();
|
|
1883
|
+
spinner.text = "Running CodeQL queries...";
|
|
1884
|
+
await exporter.analyze();
|
|
1885
|
+
spinner.text = "Generating artifact...";
|
|
1886
|
+
artifact = await exporter.export();
|
|
1887
|
+
await exporter.cleanup();
|
|
1888
|
+
} else {
|
|
1889
|
+
spinner.fail(`Unknown exporter: ${exporterName}`);
|
|
1890
|
+
return null;
|
|
1891
|
+
}
|
|
1892
|
+
spinner.succeed("Analysis complete");
|
|
1893
|
+
return artifact;
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
spinner.fail("Analysis failed");
|
|
1896
|
+
log.error(String(error));
|
|
1897
|
+
return null;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
function calculateDiffSummary(baseline, current) {
|
|
1901
|
+
const baselineFnIds = new Set(baseline.nodes.functions.map((fn) => fn.id));
|
|
1902
|
+
const currentFnIds = new Set(current.nodes.functions.map((fn) => fn.id));
|
|
1903
|
+
const functionsAdded = current.nodes.functions.filter(
|
|
1904
|
+
(fn) => !baselineFnIds.has(fn.id)
|
|
1905
|
+
).length;
|
|
1906
|
+
const functionsRemoved = baseline.nodes.functions.filter(
|
|
1907
|
+
(fn) => !currentFnIds.has(fn.id)
|
|
1908
|
+
).length;
|
|
1909
|
+
const baselineModIds = new Set(baseline.nodes.modules.map((m) => m.id));
|
|
1910
|
+
const currentModIds = new Set(current.nodes.modules.map((m) => m.id));
|
|
1911
|
+
const modulesAdded = current.nodes.modules.filter(
|
|
1912
|
+
(m) => !baselineModIds.has(m.id)
|
|
1913
|
+
).length;
|
|
1914
|
+
const modulesRemoved = baseline.nodes.modules.filter(
|
|
1915
|
+
(m) => !currentModIds.has(m.id)
|
|
1916
|
+
).length;
|
|
1917
|
+
const edgeKey = (e) => `${e.from}->${e.to}`;
|
|
1918
|
+
const baselineCallEdges = new Set(baseline.edges.calls.map(edgeKey));
|
|
1919
|
+
const currentCallEdges = new Set(current.edges.calls.map(edgeKey));
|
|
1920
|
+
const callEdgesAdded = current.edges.calls.filter(
|
|
1921
|
+
(e) => !baselineCallEdges.has(edgeKey(e))
|
|
1922
|
+
).length;
|
|
1923
|
+
const callEdgesRemoved = baseline.edges.calls.filter(
|
|
1924
|
+
(e) => !currentCallEdges.has(edgeKey(e))
|
|
1925
|
+
).length;
|
|
1926
|
+
const hasChanges = functionsAdded > 0 || functionsRemoved > 0 || modulesAdded > 0 || modulesRemoved > 0 || callEdgesAdded > 0 || callEdgesRemoved > 0;
|
|
1927
|
+
return {
|
|
1928
|
+
hasChanges,
|
|
1929
|
+
functionsAdded,
|
|
1930
|
+
functionsRemoved,
|
|
1931
|
+
modulesAdded,
|
|
1932
|
+
modulesRemoved,
|
|
1933
|
+
callEdgesAdded,
|
|
1934
|
+
callEdgesRemoved
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
function printDiffSummary(diff) {
|
|
1938
|
+
console.log(` Functions: ${formatDiff(diff.functionsAdded, diff.functionsRemoved)}`);
|
|
1939
|
+
console.log(` Modules: ${formatDiff(diff.modulesAdded, diff.modulesRemoved)}`);
|
|
1940
|
+
console.log(` Call edges: ${formatDiff(diff.callEdgesAdded, diff.callEdgesRemoved)}`);
|
|
1941
|
+
}
|
|
1942
|
+
function saveArtifact(artifact, outputPath) {
|
|
1943
|
+
const outputDir = path12.dirname(outputPath);
|
|
1944
|
+
if (!fs12.existsSync(outputDir)) {
|
|
1945
|
+
fs12.mkdirSync(outputDir, { recursive: true });
|
|
1946
|
+
}
|
|
1947
|
+
fs12.writeFileSync(outputPath, JSON.stringify(artifact, null, 2));
|
|
1948
|
+
}
|
|
1949
|
+
async function startServer(artifactPath, port, openBrowser) {
|
|
1950
|
+
const isPortInUse = await checkPort(port);
|
|
1951
|
+
if (isPortInUse) {
|
|
1952
|
+
log.info(`Server already running on port ${port}`);
|
|
1953
|
+
log.success(`View graph at ${chalk5.underline(`http://localhost:${port}`)}`);
|
|
1954
|
+
if (openBrowser) {
|
|
1955
|
+
openInBrowser(`http://localhost:${port}`);
|
|
1956
|
+
}
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
log.info("Starting visualization server...");
|
|
1960
|
+
const server = createServer2((req, res) => {
|
|
1961
|
+
const url = req.url || "/";
|
|
1962
|
+
if (url === "/api/artifact") {
|
|
1963
|
+
res.setHeader("Content-Type", "application/json");
|
|
1964
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1965
|
+
const content = fs12.readFileSync(artifactPath, "utf-8");
|
|
1966
|
+
res.end(content);
|
|
1967
|
+
} else if (url === "/api/reload") {
|
|
1968
|
+
res.setHeader("Content-Type", "application/json");
|
|
1969
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1970
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
1971
|
+
} else if (url === "/") {
|
|
1972
|
+
res.setHeader("Content-Type", "text/html");
|
|
1973
|
+
res.end(getIndexHtml2(port, artifactPath));
|
|
1974
|
+
} else {
|
|
1975
|
+
res.statusCode = 404;
|
|
1976
|
+
res.end("Not Found");
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
return new Promise((resolve) => {
|
|
1980
|
+
server.listen(port, () => {
|
|
1981
|
+
log.success(`Server running at ${chalk5.underline(`http://localhost:${port}`)}`);
|
|
1982
|
+
log.info(`Serving: ${formatPath(artifactPath)}`);
|
|
1983
|
+
log.dim("\nPress Ctrl+C to stop");
|
|
1984
|
+
if (openBrowser) {
|
|
1985
|
+
openInBrowser(`http://localhost:${port}`);
|
|
1986
|
+
}
|
|
1987
|
+
process.on("SIGINT", () => {
|
|
1988
|
+
log.info("\nShutting down server...");
|
|
1989
|
+
server.close(() => {
|
|
1990
|
+
process.exit(0);
|
|
1991
|
+
});
|
|
1992
|
+
});
|
|
1993
|
+
resolve();
|
|
1994
|
+
});
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
function checkPort(port) {
|
|
1998
|
+
return new Promise((resolve) => {
|
|
1999
|
+
const server = createServer2();
|
|
2000
|
+
server.once("error", (err) => {
|
|
2001
|
+
if (err.code === "EADDRINUSE") {
|
|
2002
|
+
resolve(true);
|
|
2003
|
+
} else {
|
|
2004
|
+
resolve(false);
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
server.once("listening", () => {
|
|
2008
|
+
server.close();
|
|
2009
|
+
resolve(false);
|
|
2010
|
+
});
|
|
2011
|
+
server.listen(port);
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
function openInBrowser(url) {
|
|
2015
|
+
const cmd = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
2016
|
+
exec(`${cmd} ${url}`);
|
|
2017
|
+
}
|
|
2018
|
+
function getIndexHtml2(port, artifactPath) {
|
|
2019
|
+
const fileName = path12.basename(artifactPath);
|
|
2020
|
+
return `<!DOCTYPE html>
|
|
2021
|
+
<html lang="en">
|
|
2022
|
+
<head>
|
|
2023
|
+
<meta charset="UTF-8">
|
|
2024
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2025
|
+
<title>Monarch - Graph Visualization</title>
|
|
2026
|
+
<style>
|
|
2027
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2028
|
+
body {
|
|
2029
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2030
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
2031
|
+
color: #fff;
|
|
2032
|
+
min-height: 100vh;
|
|
2033
|
+
display: flex;
|
|
2034
|
+
align-items: center;
|
|
2035
|
+
justify-content: center;
|
|
2036
|
+
}
|
|
2037
|
+
.container {
|
|
2038
|
+
text-align: center;
|
|
2039
|
+
padding: 2rem;
|
|
2040
|
+
max-width: 600px;
|
|
2041
|
+
}
|
|
2042
|
+
.logo {
|
|
2043
|
+
font-size: 4rem;
|
|
2044
|
+
margin-bottom: 1rem;
|
|
2045
|
+
}
|
|
2046
|
+
h1 {
|
|
2047
|
+
font-size: 2.5rem;
|
|
2048
|
+
margin-bottom: 0.5rem;
|
|
2049
|
+
background: linear-gradient(135deg, #4a90d9, #67b26f);
|
|
2050
|
+
-webkit-background-clip: text;
|
|
2051
|
+
-webkit-text-fill-color: transparent;
|
|
2052
|
+
background-clip: text;
|
|
2053
|
+
}
|
|
2054
|
+
.tagline {
|
|
2055
|
+
color: #888;
|
|
2056
|
+
margin-bottom: 2rem;
|
|
2057
|
+
font-size: 1.1rem;
|
|
2058
|
+
}
|
|
2059
|
+
.card {
|
|
2060
|
+
background: rgba(255, 255, 255, 0.05);
|
|
2061
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
2062
|
+
padding: 1.5rem;
|
|
2063
|
+
border-radius: 12px;
|
|
2064
|
+
margin-bottom: 1.5rem;
|
|
2065
|
+
}
|
|
2066
|
+
.card h2 {
|
|
2067
|
+
font-size: 1rem;
|
|
2068
|
+
color: #4a90d9;
|
|
2069
|
+
margin-bottom: 0.75rem;
|
|
2070
|
+
text-transform: uppercase;
|
|
2071
|
+
letter-spacing: 1px;
|
|
2072
|
+
}
|
|
2073
|
+
.endpoint {
|
|
2074
|
+
background: rgba(0, 0, 0, 0.3);
|
|
2075
|
+
padding: 0.75rem 1rem;
|
|
2076
|
+
border-radius: 6px;
|
|
2077
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
2078
|
+
font-size: 0.9rem;
|
|
2079
|
+
color: #67b26f;
|
|
2080
|
+
word-break: break-all;
|
|
2081
|
+
}
|
|
2082
|
+
.file-name {
|
|
2083
|
+
color: #ffd93d;
|
|
2084
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
2085
|
+
}
|
|
2086
|
+
.instructions {
|
|
2087
|
+
color: #888;
|
|
2088
|
+
font-size: 0.9rem;
|
|
2089
|
+
line-height: 1.6;
|
|
2090
|
+
}
|
|
2091
|
+
.instructions code {
|
|
2092
|
+
background: rgba(255, 255, 255, 0.1);
|
|
2093
|
+
padding: 0.2rem 0.5rem;
|
|
2094
|
+
border-radius: 4px;
|
|
2095
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
2096
|
+
color: #4a90d9;
|
|
2097
|
+
}
|
|
2098
|
+
.status {
|
|
2099
|
+
display: inline-flex;
|
|
2100
|
+
align-items: center;
|
|
2101
|
+
gap: 0.5rem;
|
|
2102
|
+
background: rgba(103, 178, 111, 0.2);
|
|
2103
|
+
color: #67b26f;
|
|
2104
|
+
padding: 0.5rem 1rem;
|
|
2105
|
+
border-radius: 20px;
|
|
2106
|
+
font-size: 0.85rem;
|
|
2107
|
+
margin-top: 1rem;
|
|
2108
|
+
}
|
|
2109
|
+
.status-dot {
|
|
2110
|
+
width: 8px;
|
|
2111
|
+
height: 8px;
|
|
2112
|
+
background: #67b26f;
|
|
2113
|
+
border-radius: 50%;
|
|
2114
|
+
animation: pulse 2s infinite;
|
|
2115
|
+
}
|
|
2116
|
+
@keyframes pulse {
|
|
2117
|
+
0%, 100% { opacity: 1; }
|
|
2118
|
+
50% { opacity: 0.5; }
|
|
2119
|
+
}
|
|
2120
|
+
</style>
|
|
2121
|
+
</head>
|
|
2122
|
+
<body>
|
|
2123
|
+
<div class="container">
|
|
2124
|
+
<div class="logo">\u{1F52E}</div>
|
|
2125
|
+
<h1>Monarch</h1>
|
|
2126
|
+
<p class="tagline">Static Codebase Flow Mapping</p>
|
|
2127
|
+
|
|
2128
|
+
<div class="card">
|
|
2129
|
+
<h2>Current Graph</h2>
|
|
2130
|
+
<p class="file-name">${fileName}</p>
|
|
2131
|
+
</div>
|
|
2132
|
+
|
|
2133
|
+
<div class="card">
|
|
2134
|
+
<h2>API Endpoint</h2>
|
|
2135
|
+
<p class="endpoint">http://localhost:${port}/api/artifact</p>
|
|
2136
|
+
</div>
|
|
2137
|
+
|
|
2138
|
+
<div class="card">
|
|
2139
|
+
<h2>View Full Visualization</h2>
|
|
2140
|
+
<p class="instructions">
|
|
2141
|
+
Run the UI development server:<br><br>
|
|
2142
|
+
<code>pnpm --filter @codeflow/ui dev</code>
|
|
2143
|
+
</p>
|
|
2144
|
+
</div>
|
|
2145
|
+
|
|
2146
|
+
<div class="status">
|
|
2147
|
+
<span class="status-dot"></span>
|
|
2148
|
+
Server running on port ${port}
|
|
2149
|
+
</div>
|
|
2150
|
+
</div>
|
|
2151
|
+
</body>
|
|
2152
|
+
</html>`;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// src/commands/graph.ts
|
|
2156
|
+
init_dist();
|
|
2157
|
+
import fs13 from "fs";
|
|
2158
|
+
import path13 from "path";
|
|
2159
|
+
|
|
2160
|
+
// src/utils/ascii-graph.ts
|
|
2161
|
+
import chalk6 from "chalk";
|
|
2162
|
+
var BOX_CHARS = {
|
|
2163
|
+
topLeft: "\u250C",
|
|
2164
|
+
topRight: "\u2510",
|
|
2165
|
+
bottomLeft: "\u2514",
|
|
2166
|
+
bottomRight: "\u2518",
|
|
2167
|
+
horizontal: "\u2500",
|
|
2168
|
+
vertical: "\u2502",
|
|
2169
|
+
arrow: "\u2192",
|
|
2170
|
+
arrowDown: "\u2193",
|
|
2171
|
+
junction: "\u251C",
|
|
2172
|
+
junctionRight: "\u2524"
|
|
2173
|
+
};
|
|
2174
|
+
function renderAsciiGraph(artifact, level, options = {}) {
|
|
2175
|
+
const { maxWidth = 80, showEdgeCounts = true, colorize = true } = options;
|
|
2176
|
+
const { nodes, edges } = extractGraphData(artifact, level);
|
|
2177
|
+
if (nodes.length === 0) {
|
|
2178
|
+
return chalk6.yellow("No nodes found at this level.");
|
|
2179
|
+
}
|
|
2180
|
+
const lines = [];
|
|
2181
|
+
lines.push(formatHeader(level, nodes.length, edges.length, colorize));
|
|
2182
|
+
lines.push("");
|
|
2183
|
+
const { roots, connected, isolated } = categorizeNodes(nodes, edges);
|
|
2184
|
+
if (roots.length > 0 || connected.length > 0) {
|
|
2185
|
+
lines.push(formatSectionHeader("Connected Graph", colorize));
|
|
2186
|
+
lines.push(...renderConnectedGraph(nodes, edges, maxWidth, showEdgeCounts, colorize));
|
|
2187
|
+
lines.push("");
|
|
2188
|
+
}
|
|
2189
|
+
if (isolated.length > 0) {
|
|
2190
|
+
lines.push(formatSectionHeader(`Isolated Nodes (${isolated.length})`, colorize));
|
|
2191
|
+
lines.push(...renderIsolatedNodes(isolated, nodes, maxWidth, colorize));
|
|
2192
|
+
}
|
|
2193
|
+
return lines.join("\n");
|
|
2194
|
+
}
|
|
2195
|
+
function renderExpandedView(artifact, targetName, targetType, options = {}) {
|
|
2196
|
+
const { maxWidth = 80, showEdgeCounts = true, colorize = true } = options;
|
|
2197
|
+
const lines = [];
|
|
2198
|
+
if (targetType === "component") {
|
|
2199
|
+
const component = artifact.nodes.components.find(
|
|
2200
|
+
(c) => c.name === targetName || c.id === targetName
|
|
2201
|
+
);
|
|
2202
|
+
if (!component) {
|
|
2203
|
+
return chalk6.red(`Component not found: ${targetName}`);
|
|
2204
|
+
}
|
|
2205
|
+
lines.push(formatHeader(`Component: ${component.name}`, 0, 0, colorize));
|
|
2206
|
+
lines.push("");
|
|
2207
|
+
const moduleIds = new Set(component.moduleIds || []);
|
|
2208
|
+
const modules = artifact.nodes.modules.filter((m) => moduleIds.has(m.id));
|
|
2209
|
+
if (modules.length === 0) {
|
|
2210
|
+
lines.push(chalk6.yellow(" No modules in this component."));
|
|
2211
|
+
return lines.join("\n");
|
|
2212
|
+
}
|
|
2213
|
+
const moduleFunctionIds = /* @__PURE__ */ new Map();
|
|
2214
|
+
for (const mod of modules) {
|
|
2215
|
+
moduleFunctionIds.set(mod.id, new Set(mod.functionIds || []));
|
|
2216
|
+
}
|
|
2217
|
+
const edges = aggregateEdgesBetweenContainers(
|
|
2218
|
+
artifact,
|
|
2219
|
+
modules.map((m) => m.id),
|
|
2220
|
+
moduleFunctionIds
|
|
2221
|
+
);
|
|
2222
|
+
const nodes = modules.map((m) => ({
|
|
2223
|
+
id: m.id,
|
|
2224
|
+
label: formatModulePath(m.path),
|
|
2225
|
+
type: "module",
|
|
2226
|
+
children: m.functionIds?.length || 0
|
|
2227
|
+
}));
|
|
2228
|
+
lines.push(formatSectionHeader(`Modules (${modules.length})`, colorize));
|
|
2229
|
+
if (edges.length > 0) {
|
|
2230
|
+
lines.push(...renderConnectedGraph(nodes, edges, maxWidth, showEdgeCounts, colorize));
|
|
2231
|
+
} else {
|
|
2232
|
+
for (const node of nodes) {
|
|
2233
|
+
lines.push(formatNode(node, colorize));
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
} else {
|
|
2237
|
+
const module = artifact.nodes.modules.find(
|
|
2238
|
+
(m) => m.path === targetName || m.id === targetName || m.path.endsWith(targetName)
|
|
2239
|
+
);
|
|
2240
|
+
if (!module) {
|
|
2241
|
+
return chalk6.red(`Module not found: ${targetName}`);
|
|
2242
|
+
}
|
|
2243
|
+
lines.push(formatHeader(`Module: ${module.path}`, 0, 0, colorize));
|
|
2244
|
+
lines.push("");
|
|
2245
|
+
const functionIds = new Set(module.functionIds || []);
|
|
2246
|
+
const functions = artifact.nodes.functions.filter((f) => functionIds.has(f.id));
|
|
2247
|
+
if (functions.length === 0) {
|
|
2248
|
+
lines.push(chalk6.yellow(" No functions in this module."));
|
|
2249
|
+
return lines.join("\n");
|
|
2250
|
+
}
|
|
2251
|
+
const edges = getEdgesBetweenFunctions(artifact, functionIds);
|
|
2252
|
+
const nodes = functions.map((f) => ({
|
|
2253
|
+
id: f.id,
|
|
2254
|
+
label: formatFunctionName(f),
|
|
2255
|
+
type: "function"
|
|
2256
|
+
}));
|
|
2257
|
+
lines.push(formatSectionHeader(`Functions (${functions.length})`, colorize));
|
|
2258
|
+
if (edges.length > 0) {
|
|
2259
|
+
lines.push(...renderConnectedGraph(nodes, edges, maxWidth, showEdgeCounts, colorize));
|
|
2260
|
+
} else {
|
|
2261
|
+
for (const node of nodes) {
|
|
2262
|
+
lines.push(formatNode(node, colorize));
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return lines.join("\n");
|
|
2267
|
+
}
|
|
2268
|
+
function listExpandableItems(artifact) {
|
|
2269
|
+
const lines = [];
|
|
2270
|
+
lines.push(chalk6.bold.cyan("\nExpandable Items\n"));
|
|
2271
|
+
lines.push(chalk6.bold("Components:"));
|
|
2272
|
+
if (artifact.nodes.components.length === 0) {
|
|
2273
|
+
lines.push(chalk6.dim(" (none)"));
|
|
2274
|
+
} else {
|
|
2275
|
+
for (const comp of artifact.nodes.components) {
|
|
2276
|
+
const modCount = comp.moduleIds?.length || 0;
|
|
2277
|
+
lines.push(` ${chalk6.cyan(comp.name)} ${chalk6.dim(`(${modCount} modules)`)}`);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
lines.push("");
|
|
2281
|
+
lines.push(chalk6.bold("Modules:"));
|
|
2282
|
+
if (artifact.nodes.modules.length === 0) {
|
|
2283
|
+
lines.push(chalk6.dim(" (none)"));
|
|
2284
|
+
} else {
|
|
2285
|
+
for (const mod of artifact.nodes.modules) {
|
|
2286
|
+
const fnCount = mod.functionIds?.length || 0;
|
|
2287
|
+
lines.push(` ${chalk6.cyan(formatModulePath(mod.path))} ${chalk6.dim(`(${fnCount} functions)`)}`);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
return lines.join("\n");
|
|
2291
|
+
}
|
|
2292
|
+
function extractGraphData(artifact, level) {
|
|
2293
|
+
switch (level) {
|
|
2294
|
+
case "components":
|
|
2295
|
+
return extractComponentGraph(artifact);
|
|
2296
|
+
case "modules":
|
|
2297
|
+
return extractModuleGraph(artifact);
|
|
2298
|
+
case "functions":
|
|
2299
|
+
return extractFunctionGraph(artifact);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
function extractComponentGraph(artifact) {
|
|
2303
|
+
const nodes = artifact.nodes.components.map((c) => ({
|
|
2304
|
+
id: c.id,
|
|
2305
|
+
label: c.name,
|
|
2306
|
+
type: "component",
|
|
2307
|
+
children: c.moduleIds?.length || 0
|
|
2308
|
+
}));
|
|
2309
|
+
const functionToComponent = /* @__PURE__ */ new Map();
|
|
2310
|
+
for (const comp of artifact.nodes.components) {
|
|
2311
|
+
for (const modId of comp.moduleIds || []) {
|
|
2312
|
+
const mod = artifact.nodes.modules.find((m) => m.id === modId);
|
|
2313
|
+
if (mod) {
|
|
2314
|
+
for (const fnId of mod.functionIds || []) {
|
|
2315
|
+
functionToComponent.set(fnId, comp.id);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
2321
|
+
for (const edge of artifact.edges.calls) {
|
|
2322
|
+
const fromComp = functionToComponent.get(edge.from);
|
|
2323
|
+
const toComp = functionToComponent.get(edge.to);
|
|
2324
|
+
if (fromComp && toComp && fromComp !== toComp) {
|
|
2325
|
+
const key = `${fromComp}->${toComp}`;
|
|
2326
|
+
edgeMap.set(key, (edgeMap.get(key) || 0) + 1);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
const edges = Array.from(edgeMap.entries()).map(([key, count]) => {
|
|
2330
|
+
const [from, to] = key.split("->");
|
|
2331
|
+
return { from, to, count };
|
|
2332
|
+
});
|
|
2333
|
+
return { nodes, edges };
|
|
2334
|
+
}
|
|
2335
|
+
function extractModuleGraph(artifact) {
|
|
2336
|
+
const nodes = artifact.nodes.modules.map((m) => ({
|
|
2337
|
+
id: m.id,
|
|
2338
|
+
label: formatModulePath(m.path),
|
|
2339
|
+
type: "module",
|
|
2340
|
+
children: m.functionIds?.length || 0
|
|
2341
|
+
}));
|
|
2342
|
+
const functionToModule = /* @__PURE__ */ new Map();
|
|
2343
|
+
for (const mod of artifact.nodes.modules) {
|
|
2344
|
+
for (const fnId of mod.functionIds || []) {
|
|
2345
|
+
functionToModule.set(fnId, mod.id);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
2349
|
+
for (const edge of artifact.edges.calls) {
|
|
2350
|
+
const fromMod = functionToModule.get(edge.from);
|
|
2351
|
+
const toMod = functionToModule.get(edge.to);
|
|
2352
|
+
if (fromMod && toMod && fromMod !== toMod) {
|
|
2353
|
+
const key = `${fromMod}->${toMod}`;
|
|
2354
|
+
edgeMap.set(key, (edgeMap.get(key) || 0) + 1);
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
const edges = Array.from(edgeMap.entries()).map(([key, count]) => {
|
|
2358
|
+
const [from, to] = key.split("->");
|
|
2359
|
+
return { from, to, count };
|
|
2360
|
+
});
|
|
2361
|
+
return { nodes, edges };
|
|
2362
|
+
}
|
|
2363
|
+
function extractFunctionGraph(artifact) {
|
|
2364
|
+
const nodes = artifact.nodes.functions.map((f) => ({
|
|
2365
|
+
id: f.id,
|
|
2366
|
+
label: formatFunctionName(f),
|
|
2367
|
+
type: "function"
|
|
2368
|
+
}));
|
|
2369
|
+
const edges = artifact.edges.calls.map((e) => ({
|
|
2370
|
+
from: e.from,
|
|
2371
|
+
to: e.to,
|
|
2372
|
+
count: e.callsites.length
|
|
2373
|
+
}));
|
|
2374
|
+
return { nodes, edges };
|
|
2375
|
+
}
|
|
2376
|
+
function aggregateEdgesBetweenContainers(artifact, containerIds, containerFunctions) {
|
|
2377
|
+
const containerSet = new Set(containerIds);
|
|
2378
|
+
const functionToContainer = /* @__PURE__ */ new Map();
|
|
2379
|
+
for (const [containerId, fnIds] of containerFunctions) {
|
|
2380
|
+
for (const fnId of fnIds) {
|
|
2381
|
+
functionToContainer.set(fnId, containerId);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
2385
|
+
for (const edge of artifact.edges.calls) {
|
|
2386
|
+
const fromContainer = functionToContainer.get(edge.from);
|
|
2387
|
+
const toContainer = functionToContainer.get(edge.to);
|
|
2388
|
+
if (fromContainer && toContainer && containerSet.has(fromContainer) && containerSet.has(toContainer) && fromContainer !== toContainer) {
|
|
2389
|
+
const key = `${fromContainer}->${toContainer}`;
|
|
2390
|
+
edgeMap.set(key, (edgeMap.get(key) || 0) + 1);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
return Array.from(edgeMap.entries()).map(([key, count]) => {
|
|
2394
|
+
const [from, to] = key.split("->");
|
|
2395
|
+
return { from, to, count };
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
function getEdgesBetweenFunctions(artifact, functionIds) {
|
|
2399
|
+
return artifact.edges.calls.filter((e) => functionIds.has(e.from) && functionIds.has(e.to)).map((e) => ({
|
|
2400
|
+
from: e.from,
|
|
2401
|
+
to: e.to,
|
|
2402
|
+
count: e.callsites.length
|
|
2403
|
+
}));
|
|
2404
|
+
}
|
|
2405
|
+
function categorizeNodes(nodes, edges) {
|
|
2406
|
+
const hasIncoming = /* @__PURE__ */ new Set();
|
|
2407
|
+
const hasOutgoing = /* @__PURE__ */ new Set();
|
|
2408
|
+
for (const edge of edges) {
|
|
2409
|
+
hasOutgoing.add(edge.from);
|
|
2410
|
+
hasIncoming.add(edge.to);
|
|
2411
|
+
}
|
|
2412
|
+
const roots = [];
|
|
2413
|
+
const connected = [];
|
|
2414
|
+
const isolated = [];
|
|
2415
|
+
for (const node of nodes) {
|
|
2416
|
+
const incoming = hasIncoming.has(node.id);
|
|
2417
|
+
const outgoing = hasOutgoing.has(node.id);
|
|
2418
|
+
if (!incoming && !outgoing) {
|
|
2419
|
+
isolated.push(node.id);
|
|
2420
|
+
} else if (!incoming && outgoing) {
|
|
2421
|
+
roots.push(node.id);
|
|
2422
|
+
} else {
|
|
2423
|
+
connected.push(node.id);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
return { roots, connected, isolated };
|
|
2427
|
+
}
|
|
2428
|
+
function renderConnectedGraph(nodes, edges, _maxWidth, showEdgeCounts, colorize) {
|
|
2429
|
+
const lines = [];
|
|
2430
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
2431
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
2432
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
2433
|
+
for (const edge of edges) {
|
|
2434
|
+
if (!outgoing.has(edge.from)) outgoing.set(edge.from, []);
|
|
2435
|
+
if (!incoming.has(edge.to)) incoming.set(edge.to, []);
|
|
2436
|
+
outgoing.get(edge.from).push(edge);
|
|
2437
|
+
incoming.get(edge.to).push(edge);
|
|
2438
|
+
}
|
|
2439
|
+
const roots = nodes.filter((n) => !incoming.has(n.id) && outgoing.has(n.id));
|
|
2440
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2441
|
+
for (const root of roots) {
|
|
2442
|
+
lines.push(...renderNodeTree(root, nodeMap, outgoing, visited, 0, showEdgeCounts, colorize));
|
|
2443
|
+
}
|
|
2444
|
+
for (const node of nodes) {
|
|
2445
|
+
if (!visited.has(node.id) && (outgoing.has(node.id) || incoming.has(node.id))) {
|
|
2446
|
+
lines.push(...renderNodeTree(node, nodeMap, outgoing, visited, 0, showEdgeCounts, colorize));
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
return lines;
|
|
2450
|
+
}
|
|
2451
|
+
function renderNodeTree(node, nodeMap, outgoing, visited, depth, showEdgeCounts, colorize) {
|
|
2452
|
+
if (visited.has(node.id)) {
|
|
2453
|
+
return [`${" ".repeat(depth)}${BOX_CHARS.arrow} ${chalk6.dim(`(${node.label} - cycle)`)}`];
|
|
2454
|
+
}
|
|
2455
|
+
visited.add(node.id);
|
|
2456
|
+
const lines = [];
|
|
2457
|
+
const indent = " ".repeat(depth);
|
|
2458
|
+
const nodeStr = formatNode(node, colorize);
|
|
2459
|
+
lines.push(`${indent}${nodeStr}`);
|
|
2460
|
+
const children = outgoing.get(node.id) || [];
|
|
2461
|
+
for (let i = 0; i < children.length; i++) {
|
|
2462
|
+
const edge = children[i];
|
|
2463
|
+
const targetNode = nodeMap.get(edge.to);
|
|
2464
|
+
const isLast = i === children.length - 1;
|
|
2465
|
+
const prefix = isLast ? BOX_CHARS.bottomLeft : BOX_CHARS.junction;
|
|
2466
|
+
const countStr = showEdgeCounts && edge.count > 1 ? chalk6.dim(` (${edge.count}x)`) : "";
|
|
2467
|
+
if (targetNode) {
|
|
2468
|
+
if (visited.has(targetNode.id)) {
|
|
2469
|
+
lines.push(
|
|
2470
|
+
`${indent} ${prefix}${BOX_CHARS.horizontal}${BOX_CHARS.arrow} ${chalk6.dim(`${targetNode.label} (cycle)`)}${countStr}`
|
|
2471
|
+
);
|
|
2472
|
+
} else {
|
|
2473
|
+
lines.push(`${indent} ${prefix}${BOX_CHARS.horizontal}${BOX_CHARS.arrow}${countStr}`);
|
|
2474
|
+
lines.push(
|
|
2475
|
+
...renderNodeTree(targetNode, nodeMap, outgoing, visited, depth + 2, showEdgeCounts, colorize)
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
return lines;
|
|
2481
|
+
}
|
|
2482
|
+
function renderIsolatedNodes(isolatedIds, nodes, maxWidth, colorize) {
|
|
2483
|
+
const lines = [];
|
|
2484
|
+
const isolatedNodes = nodes.filter((n) => isolatedIds.includes(n.id));
|
|
2485
|
+
const colWidth = 30;
|
|
2486
|
+
const cols = Math.max(1, Math.floor(maxWidth / colWidth));
|
|
2487
|
+
for (let i = 0; i < isolatedNodes.length; i += cols) {
|
|
2488
|
+
const row = isolatedNodes.slice(i, i + cols);
|
|
2489
|
+
const rowStr = row.map((n) => formatNode(n, colorize).padEnd(colWidth)).join("");
|
|
2490
|
+
lines.push(` ${rowStr}`);
|
|
2491
|
+
}
|
|
2492
|
+
return lines;
|
|
2493
|
+
}
|
|
2494
|
+
function formatNode(node, colorize) {
|
|
2495
|
+
const icon = getNodeIcon(node.type);
|
|
2496
|
+
const label = node.label;
|
|
2497
|
+
const childInfo = node.children !== void 0 ? chalk6.dim(` (${node.children})`) : "";
|
|
2498
|
+
if (colorize) {
|
|
2499
|
+
const color = getNodeColor(node.type);
|
|
2500
|
+
return `${icon} ${color(label)}${childInfo}`;
|
|
2501
|
+
}
|
|
2502
|
+
return `${icon} ${label}${childInfo}`;
|
|
2503
|
+
}
|
|
2504
|
+
function getNodeIcon(type) {
|
|
2505
|
+
switch (type) {
|
|
2506
|
+
case "component":
|
|
2507
|
+
return "\u{1F4E6}";
|
|
2508
|
+
case "module":
|
|
2509
|
+
return "\u{1F4C4}";
|
|
2510
|
+
case "function":
|
|
2511
|
+
return "\u26A1";
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
function getNodeColor(type) {
|
|
2515
|
+
switch (type) {
|
|
2516
|
+
case "component":
|
|
2517
|
+
return chalk6.cyan;
|
|
2518
|
+
case "module":
|
|
2519
|
+
return chalk6.yellow;
|
|
2520
|
+
case "function":
|
|
2521
|
+
return chalk6.green;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
function formatHeader(title, nodeCount, edgeCount, colorize) {
|
|
2525
|
+
const levelTitles = {
|
|
2526
|
+
components: "Component Graph",
|
|
2527
|
+
modules: "Module Graph",
|
|
2528
|
+
functions: "Function Graph"
|
|
2529
|
+
};
|
|
2530
|
+
const displayTitle = levelTitles[title] || title;
|
|
2531
|
+
if (nodeCount === 0 && edgeCount === 0) {
|
|
2532
|
+
return colorize ? chalk6.bold.cyan(`
|
|
2533
|
+
${displayTitle}
|
|
2534
|
+
`) : `
|
|
2535
|
+
${displayTitle}
|
|
2536
|
+
`;
|
|
2537
|
+
}
|
|
2538
|
+
const stats = `${nodeCount} nodes, ${edgeCount} edges`;
|
|
2539
|
+
const header = `${displayTitle} (${stats})`;
|
|
2540
|
+
return colorize ? chalk6.bold.cyan(`
|
|
2541
|
+
${header}
|
|
2542
|
+
`) : `
|
|
2543
|
+
${header}
|
|
2544
|
+
`;
|
|
2545
|
+
}
|
|
2546
|
+
function formatSectionHeader(title, colorize) {
|
|
2547
|
+
return colorize ? chalk6.bold.underline(title) : title;
|
|
2548
|
+
}
|
|
2549
|
+
function formatModulePath(path14) {
|
|
2550
|
+
const parts = path14.split(/[/\\]/);
|
|
2551
|
+
if (parts.length > 3) {
|
|
2552
|
+
return `.../${parts.slice(-2).join("/")}`;
|
|
2553
|
+
}
|
|
2554
|
+
return path14;
|
|
2555
|
+
}
|
|
2556
|
+
function formatFunctionName(fn) {
|
|
2557
|
+
const kindIcon = fn.kind === "method" ? "." : fn.kind === "arrow" ? "=>" : "";
|
|
2558
|
+
return `${kindIcon}${fn.name}`;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
// src/commands/graph.ts
|
|
2562
|
+
async function graphCommand(artifactPath, options) {
|
|
2563
|
+
const absolutePath = path13.resolve(artifactPath);
|
|
2564
|
+
if (!fs13.existsSync(absolutePath)) {
|
|
2565
|
+
log.error(`Artifact not found: ${formatPath(absolutePath)}`);
|
|
2566
|
+
log.info("Run `monarch analyze` or `monarch run` first to generate a graph");
|
|
2567
|
+
process.exit(1);
|
|
2568
|
+
}
|
|
2569
|
+
let artifact;
|
|
2570
|
+
try {
|
|
2571
|
+
const content = fs13.readFileSync(absolutePath, "utf-8");
|
|
2572
|
+
artifact = JSON.parse(content);
|
|
2573
|
+
if (!isCodeFlowArtifact(artifact)) {
|
|
2574
|
+
log.error("Invalid artifact file");
|
|
2575
|
+
process.exit(1);
|
|
2576
|
+
}
|
|
2577
|
+
} catch (error) {
|
|
2578
|
+
log.error(`Failed to read artifact: ${error}`);
|
|
2579
|
+
process.exit(1);
|
|
2580
|
+
}
|
|
2581
|
+
const maxWidth = options.width ? parseInt(options.width, 10) : process.stdout.columns || 80;
|
|
2582
|
+
const colorize = !options.noColor;
|
|
2583
|
+
if (options.list) {
|
|
2584
|
+
console.log(listExpandableItems(artifact));
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
if (options.expand) {
|
|
2588
|
+
const expandType = options.expandType || detectExpandType(artifact, options.expand);
|
|
2589
|
+
if (!expandType) {
|
|
2590
|
+
log.error(`Could not find "${options.expand}" as a component or module.`);
|
|
2591
|
+
log.info("Use --list to see available components and modules.");
|
|
2592
|
+
process.exit(1);
|
|
2593
|
+
}
|
|
2594
|
+
const output2 = renderExpandedView(artifact, options.expand, expandType, {
|
|
2595
|
+
maxWidth,
|
|
2596
|
+
colorize,
|
|
2597
|
+
showEdgeCounts: true
|
|
2598
|
+
});
|
|
2599
|
+
console.log(output2);
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const output = renderAsciiGraph(artifact, options.level, {
|
|
2603
|
+
maxWidth,
|
|
2604
|
+
colorize,
|
|
2605
|
+
showEdgeCounts: true
|
|
2606
|
+
});
|
|
2607
|
+
console.log(output);
|
|
2608
|
+
}
|
|
2609
|
+
function detectExpandType(artifact, name) {
|
|
2610
|
+
const component = artifact.nodes.components.find(
|
|
2611
|
+
(c) => c.name === name || c.id === name
|
|
2612
|
+
);
|
|
2613
|
+
if (component) return "component";
|
|
2614
|
+
const module = artifact.nodes.modules.find(
|
|
2615
|
+
(m) => m.path === name || m.id === name || m.path.endsWith(name)
|
|
2616
|
+
);
|
|
2617
|
+
if (module) return "module";
|
|
2618
|
+
return null;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
// src/cli.ts
|
|
2622
|
+
var program = new Command();
|
|
2623
|
+
program.name("monarch").description("Monarch - Static codebase flow mapping and visualization").version("0.1.0").addHelpCommand(false);
|
|
2624
|
+
program.command("init").description("Initialize a new Monarch project in the current directory").option("-f, --force", "Overwrite existing configuration", false).action(initCommand);
|
|
2625
|
+
program.command("analyze").alias("a").description("Analyze a codebase and generate flow artifact").argument("[path]", "Path to the codebase", ".").option("-o, --output <path>", "Output artifact path", "monarch-graph.json").option("-e, --exporter <name>", "Exporter to use (mock, codeql-ts)", "mock").option("-w, --watch", "Watch for changes and re-analyze", false).action(analyzeCommand2);
|
|
2626
|
+
program.command("diff").alias("d").description("Compare two graph artifacts and show changes").argument("<baseline>", "Path to baseline artifact").argument("<current>", "Path to current artifact").option("-f, --format <type>", "Output format (text, json, markdown)", "text").option("--exit-on-changes", "Exit with code 1 if changes detected", false).option("--only <type>", "Show only specific changes (functions, modules, edges)").action(diffCommand2);
|
|
2627
|
+
program.command("serve").alias("s").description("Start the visualization UI server").option("-p, --port <number>", "Port to serve on", "3000").option("-a, --artifact <path>", "Artifact file path", "monarch-graph.json").option("--open", "Open browser automatically", false).action(serveCommand2);
|
|
2628
|
+
program.command("list").alias("ls").description("List contents of a graph artifact").argument("[artifact]", "Path to artifact file", "monarch-graph.json").option("-t, --type <type>", "Filter by type (functions, modules, components, edges)").option("--json", "Output as JSON", false).action(listCommand);
|
|
2629
|
+
program.command("run").alias("r").description("Analyze codebase, detect changes, and start visualization server").argument("[path]", "Path to the codebase", ".").option("-o, --output <path>", "Output artifact path", "monarch-graph.json").option("-e, --exporter <name>", "Exporter to use (mock, codeql-ts)", "mock").option("-p, --port <number>", "Port for visualization server", "3000").option("--open", "Open browser automatically", false).option("-f, --force", "Force regeneration even if no changes", false).action(runCommand);
|
|
2630
|
+
program.command("graph").alias("g").description("Render ASCII graph visualization in terminal").argument("[artifact]", "Path to artifact file", "monarch-graph.json").option("-l, --level <level>", "View level (components, modules, functions)", "modules").option("-x, --expand <name>", "Expand a specific component or module by name").option("--expand-type <type>", "Type of item to expand (component, module)").option("--list", "List expandable components and modules", false).option("--no-color", "Disable colored output").option("-w, --width <cols>", "Max output width in columns").action(graphCommand);
|
|
2631
|
+
program.command("help").description("Show detailed help for Monarch or a specific command").argument("[command]", "Command to get help for").action(helpCommand);
|
|
94
2632
|
if (process.argv.length === 2) {
|
|
95
|
-
|
|
96
|
-
|
|
2633
|
+
helpCommand();
|
|
2634
|
+
process.exit(0);
|
|
97
2635
|
}
|
|
98
|
-
// Parse and execute
|
|
99
2636
|
program.parse();
|
|
100
|
-
//# sourceMappingURL=cli.js.map
|