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/index.js
CHANGED
|
@@ -1,7 +1,1552 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// ../shared/dist/schemas/artifact.js
|
|
12
|
+
function isCodeFlowArtifact(obj) {
|
|
13
|
+
if (typeof obj !== "object" || obj === null)
|
|
14
|
+
return false;
|
|
15
|
+
const artifact = obj;
|
|
16
|
+
return typeof artifact.metadata === "object" && typeof artifact.nodes === "object" && typeof artifact.edges === "object" && typeof artifact.layout === "object";
|
|
17
|
+
}
|
|
18
|
+
var init_artifact = __esm({
|
|
19
|
+
"../shared/dist/schemas/artifact.js"() {
|
|
20
|
+
"use strict";
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// ../shared/dist/utils/hashing.js
|
|
25
|
+
function generateStableId(input) {
|
|
26
|
+
let hash = 0;
|
|
27
|
+
for (let i = 0; i < input.length; i++) {
|
|
28
|
+
const char = input.charCodeAt(i);
|
|
29
|
+
hash = (hash << 5) - hash + char;
|
|
30
|
+
hash = hash & hash;
|
|
31
|
+
}
|
|
32
|
+
const positiveHash = hash >>> 0;
|
|
33
|
+
return positiveHash.toString(16).padStart(8, "0");
|
|
34
|
+
}
|
|
35
|
+
function generateFunctionId(file, name, startLine) {
|
|
36
|
+
const input = `fn:${file}:${name}:${startLine}`;
|
|
37
|
+
return `fn-${generateStableId(input)}`;
|
|
38
|
+
}
|
|
39
|
+
function generateModuleId(path12) {
|
|
40
|
+
const input = `mod:${path12}`;
|
|
41
|
+
return `mod-${generateStableId(input)}`;
|
|
42
|
+
}
|
|
43
|
+
function generateComponentId(name) {
|
|
44
|
+
const input = `comp:${name}`;
|
|
45
|
+
return `comp-${generateStableId(input)}`;
|
|
46
|
+
}
|
|
47
|
+
var init_hashing = __esm({
|
|
48
|
+
"../shared/dist/utils/hashing.js"() {
|
|
49
|
+
"use strict";
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ../shared/dist/index.js
|
|
54
|
+
var init_dist = __esm({
|
|
55
|
+
"../shared/dist/index.js"() {
|
|
56
|
+
"use strict";
|
|
57
|
+
init_artifact();
|
|
58
|
+
init_hashing();
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ../analyzer/dist/exporters/mock-exporter.js
|
|
63
|
+
import fs from "fs";
|
|
64
|
+
import path from "path";
|
|
65
|
+
var MockExporter;
|
|
66
|
+
var init_mock_exporter = __esm({
|
|
67
|
+
"../analyzer/dist/exporters/mock-exporter.js"() {
|
|
68
|
+
"use strict";
|
|
69
|
+
init_dist();
|
|
70
|
+
MockExporter = class {
|
|
71
|
+
targetPath;
|
|
72
|
+
files = [];
|
|
73
|
+
functions = [];
|
|
74
|
+
modules = [];
|
|
75
|
+
components = [];
|
|
76
|
+
callEdges = [];
|
|
77
|
+
dataflowEdges = [];
|
|
78
|
+
constructor(targetPath) {
|
|
79
|
+
this.targetPath = targetPath;
|
|
80
|
+
}
|
|
81
|
+
async prepare() {
|
|
82
|
+
this.files = this.scanTsFiles(this.targetPath);
|
|
83
|
+
console.log(`Found ${this.files.length} TypeScript files`);
|
|
84
|
+
}
|
|
85
|
+
async analyze() {
|
|
86
|
+
for (const file of this.files) {
|
|
87
|
+
const relativePath = path.relative(this.targetPath, file);
|
|
88
|
+
this.processFile(relativePath);
|
|
89
|
+
}
|
|
90
|
+
this.generateCallEdges();
|
|
91
|
+
this.generateDataflowEdges();
|
|
92
|
+
this.generateComponents();
|
|
93
|
+
}
|
|
94
|
+
async export() {
|
|
95
|
+
return {
|
|
96
|
+
metadata: {
|
|
97
|
+
analyzerVersion: "0.1.0",
|
|
98
|
+
language: "typescript",
|
|
99
|
+
repoCommit: this.generateCommitHash(),
|
|
100
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
101
|
+
configHash: this.generateConfigHash()
|
|
102
|
+
},
|
|
103
|
+
nodes: {
|
|
104
|
+
functions: this.sortById(this.functions),
|
|
105
|
+
modules: this.sortById(this.modules),
|
|
106
|
+
components: this.sortById(this.components)
|
|
107
|
+
},
|
|
108
|
+
edges: {
|
|
109
|
+
calls: this.sortEdges(this.callEdges),
|
|
110
|
+
dataflow: this.sortEdges(this.dataflowEdges)
|
|
111
|
+
},
|
|
112
|
+
layout: {
|
|
113
|
+
positions: {}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async cleanup() {
|
|
118
|
+
}
|
|
119
|
+
scanTsFiles(dir) {
|
|
120
|
+
const results = [];
|
|
121
|
+
if (!fs.existsSync(dir)) {
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
const stat = fs.statSync(dir);
|
|
125
|
+
if (stat.isFile() && (dir.endsWith(".ts") || dir.endsWith(".tsx"))) {
|
|
126
|
+
if (!dir.includes(".test.") && !dir.includes(".spec.") && !dir.endsWith(".d.ts")) {
|
|
127
|
+
results.push(dir);
|
|
128
|
+
}
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
if (!stat.isDirectory()) {
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
const entries = fs.readdirSync(dir);
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
if (entry === "node_modules" || entry.startsWith(".") || entry === "dist") {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const fullPath = path.join(dir, entry);
|
|
140
|
+
results.push(...this.scanTsFiles(fullPath));
|
|
141
|
+
}
|
|
142
|
+
return results.sort();
|
|
143
|
+
}
|
|
144
|
+
processFile(relativePath) {
|
|
145
|
+
const moduleId = generateModuleId(relativePath);
|
|
146
|
+
const moduleNode = {
|
|
147
|
+
id: moduleId,
|
|
148
|
+
path: relativePath,
|
|
149
|
+
functionIds: []
|
|
150
|
+
};
|
|
151
|
+
const fnCount = 2 + this.hashString(relativePath) % 3;
|
|
152
|
+
const baseName = path.basename(relativePath, path.extname(relativePath));
|
|
153
|
+
for (let i = 0; i < fnCount; i++) {
|
|
154
|
+
const fnName = this.generateFunctionName(baseName, i);
|
|
155
|
+
const startLine = 10 + i * 20;
|
|
156
|
+
const fnId = generateFunctionId(relativePath, fnName, startLine);
|
|
157
|
+
const fn = {
|
|
158
|
+
id: fnId,
|
|
159
|
+
name: fnName,
|
|
160
|
+
fullyQualifiedName: `${relativePath}::${fnName}`,
|
|
161
|
+
file: relativePath,
|
|
162
|
+
startLine,
|
|
163
|
+
endLine: startLine + 15,
|
|
164
|
+
kind: i === 0 ? "function" : i === 1 ? "arrow" : "method"
|
|
165
|
+
};
|
|
166
|
+
this.functions.push(fn);
|
|
167
|
+
moduleNode.functionIds.push(fnId);
|
|
168
|
+
}
|
|
169
|
+
this.modules.push(moduleNode);
|
|
170
|
+
}
|
|
171
|
+
generateFunctionName(baseName, index) {
|
|
172
|
+
const prefixes = ["handle", "process", "get", "set", "create", "update", "delete"];
|
|
173
|
+
const prefix = prefixes[(this.hashString(baseName) + index) % prefixes.length];
|
|
174
|
+
const camelName = baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
|
175
|
+
return `${prefix}${camelName}${index > 0 ? index : ""}`;
|
|
176
|
+
}
|
|
177
|
+
generateCallEdges() {
|
|
178
|
+
for (let i = 0; i < this.functions.length; i++) {
|
|
179
|
+
const caller = this.functions[i];
|
|
180
|
+
const callCount = this.hashString(caller.id) % 3;
|
|
181
|
+
for (let j = 0; j < callCount; j++) {
|
|
182
|
+
const targetIndex = (i + j + 1) % this.functions.length;
|
|
183
|
+
if (targetIndex === i)
|
|
184
|
+
continue;
|
|
185
|
+
const callee = this.functions[targetIndex];
|
|
186
|
+
if (this.callEdges.some((e) => e.from === caller.id && e.to === callee.id)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
this.callEdges.push({
|
|
190
|
+
from: caller.id,
|
|
191
|
+
to: callee.id,
|
|
192
|
+
callsites: [
|
|
193
|
+
{
|
|
194
|
+
file: caller.file,
|
|
195
|
+
line: caller.startLine + 5
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
generateDataflowEdges() {
|
|
203
|
+
if (this.functions.length < 2)
|
|
204
|
+
return;
|
|
205
|
+
const sources = this.functions.filter((fn) => fn.name.startsWith("handle") || fn.name.startsWith("get"));
|
|
206
|
+
const sinks = this.functions.filter((fn) => fn.name.startsWith("create") || fn.name.startsWith("update") || fn.name.startsWith("set"));
|
|
207
|
+
for (const source of sources.slice(0, 2)) {
|
|
208
|
+
for (const sink of sinks.slice(0, 2)) {
|
|
209
|
+
if (source.id === sink.id)
|
|
210
|
+
continue;
|
|
211
|
+
this.dataflowEdges.push({
|
|
212
|
+
from: source.id,
|
|
213
|
+
to: sink.id,
|
|
214
|
+
label: `data from ${source.name} to ${sink.name}`,
|
|
215
|
+
evidence: [`${source.name} data flows to ${sink.name}`]
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
generateComponents() {
|
|
221
|
+
const dirToModules = /* @__PURE__ */ new Map();
|
|
222
|
+
for (const mod of this.modules) {
|
|
223
|
+
const dir = path.dirname(mod.path) || "root";
|
|
224
|
+
if (!dirToModules.has(dir)) {
|
|
225
|
+
dirToModules.set(dir, []);
|
|
226
|
+
}
|
|
227
|
+
dirToModules.get(dir).push(mod);
|
|
228
|
+
}
|
|
229
|
+
for (const [dir, modules] of dirToModules) {
|
|
230
|
+
const compName = dir === "." || dir === "root" ? "main" : dir.replace(/\//g, "-");
|
|
231
|
+
const compId = generateComponentId(compName);
|
|
232
|
+
this.components.push({
|
|
233
|
+
id: compId,
|
|
234
|
+
name: compName,
|
|
235
|
+
moduleIds: modules.map((m) => m.id)
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
hashString(str) {
|
|
240
|
+
let hash = 0;
|
|
241
|
+
for (let i = 0; i < str.length; i++) {
|
|
242
|
+
const char = str.charCodeAt(i);
|
|
243
|
+
hash = (hash << 5) - hash + char;
|
|
244
|
+
hash = hash & hash;
|
|
245
|
+
}
|
|
246
|
+
return Math.abs(hash);
|
|
247
|
+
}
|
|
248
|
+
generateCommitHash() {
|
|
249
|
+
const input = this.files.sort().join("");
|
|
250
|
+
return `mock-${this.hashString(input).toString(16).padStart(8, "0")}`;
|
|
251
|
+
}
|
|
252
|
+
generateConfigHash() {
|
|
253
|
+
return "mock-config-hash";
|
|
254
|
+
}
|
|
255
|
+
sortById(items) {
|
|
256
|
+
return [...items].sort((a, b) => a.id.localeCompare(b.id));
|
|
257
|
+
}
|
|
258
|
+
sortEdges(edges) {
|
|
259
|
+
return [...edges].sort((a, b) => {
|
|
260
|
+
const fromCmp = a.from.localeCompare(b.from);
|
|
261
|
+
return fromCmp !== 0 ? fromCmp : a.to.localeCompare(b.to);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ../analyzer/dist/exporters/codeql/query-runner.js
|
|
269
|
+
import { execSync } from "child_process";
|
|
270
|
+
import fs2 from "fs";
|
|
271
|
+
import path2 from "path";
|
|
272
|
+
var QueryRunner;
|
|
273
|
+
var init_query_runner = __esm({
|
|
274
|
+
"../analyzer/dist/exporters/codeql/query-runner.js"() {
|
|
275
|
+
"use strict";
|
|
276
|
+
QueryRunner = class {
|
|
277
|
+
dbPath;
|
|
278
|
+
queriesPath;
|
|
279
|
+
targetPath;
|
|
280
|
+
constructor(dbPath, queriesPath, targetPath) {
|
|
281
|
+
this.dbPath = dbPath;
|
|
282
|
+
this.queriesPath = queriesPath;
|
|
283
|
+
this.targetPath = targetPath;
|
|
284
|
+
}
|
|
285
|
+
async runQuery(queryFile) {
|
|
286
|
+
const queryPath = path2.join(this.queriesPath, queryFile);
|
|
287
|
+
const outputPath = path2.join(this.targetPath, `.codeql-results-${queryFile}.json`);
|
|
288
|
+
try {
|
|
289
|
+
execSync(`codeql query run --database="${this.dbPath}" --output="${outputPath}" --format=json "${queryPath}"`, {
|
|
290
|
+
stdio: "pipe",
|
|
291
|
+
cwd: this.targetPath
|
|
292
|
+
});
|
|
293
|
+
if (fs2.existsSync(outputPath)) {
|
|
294
|
+
const content = fs2.readFileSync(outputPath, "utf-8");
|
|
295
|
+
const data = JSON.parse(content);
|
|
296
|
+
fs2.unlinkSync(outputPath);
|
|
297
|
+
return this.parseCodeQLResults(data, queryFile);
|
|
298
|
+
}
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.warn(` Warning: Query ${queryFile} failed: ${error}`);
|
|
301
|
+
}
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
parseCodeQLResults(data, queryFile) {
|
|
305
|
+
if (!data || typeof data !== "object")
|
|
306
|
+
return [];
|
|
307
|
+
const results = [];
|
|
308
|
+
if ("#select" in data && Array.isArray(data["#select"])) {
|
|
309
|
+
const tuples = data["#select"];
|
|
310
|
+
const columns = this.getColumnsForQuery(queryFile);
|
|
311
|
+
for (const tuple of tuples) {
|
|
312
|
+
const obj = {};
|
|
313
|
+
for (let i = 0; i < columns.length && i < tuple.length; i++) {
|
|
314
|
+
obj[columns[i]] = tuple[i];
|
|
315
|
+
}
|
|
316
|
+
results.push(obj);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return results;
|
|
320
|
+
}
|
|
321
|
+
getColumnsForQuery(queryFile) {
|
|
322
|
+
switch (queryFile) {
|
|
323
|
+
case "functions.ql":
|
|
324
|
+
return ["_element", "name", "file", "startLine", "endLine", "kind"];
|
|
325
|
+
case "calls.ql":
|
|
326
|
+
return [
|
|
327
|
+
"callerName",
|
|
328
|
+
"callerFile",
|
|
329
|
+
"callerStartLine",
|
|
330
|
+
"calleeName",
|
|
331
|
+
"calleeFile",
|
|
332
|
+
"calleeStartLine",
|
|
333
|
+
"callsiteLine",
|
|
334
|
+
"callsiteFile"
|
|
335
|
+
];
|
|
336
|
+
case "dataflow.ql":
|
|
337
|
+
return [
|
|
338
|
+
"_element",
|
|
339
|
+
"_source",
|
|
340
|
+
"_sink",
|
|
341
|
+
"sourceFunction",
|
|
342
|
+
"sourceFile",
|
|
343
|
+
"sourceStartLine",
|
|
344
|
+
"sinkFunction",
|
|
345
|
+
"sinkFile",
|
|
346
|
+
"sinkStartLine"
|
|
347
|
+
];
|
|
348
|
+
default:
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// ../analyzer/dist/exporters/codeql/codeql-exporter.js
|
|
357
|
+
import { execSync as execSync2 } from "child_process";
|
|
358
|
+
import fs3 from "fs";
|
|
359
|
+
import path3 from "path";
|
|
360
|
+
var CodeQLExporter;
|
|
361
|
+
var init_codeql_exporter = __esm({
|
|
362
|
+
"../analyzer/dist/exporters/codeql/codeql-exporter.js"() {
|
|
363
|
+
"use strict";
|
|
364
|
+
init_dist();
|
|
365
|
+
init_query_runner();
|
|
366
|
+
CodeQLExporter = class {
|
|
367
|
+
targetPath;
|
|
368
|
+
dbPath;
|
|
369
|
+
queriesPath;
|
|
370
|
+
queryRunner;
|
|
371
|
+
functions = [];
|
|
372
|
+
modules = [];
|
|
373
|
+
components = [];
|
|
374
|
+
callEdges = [];
|
|
375
|
+
dataflowEdges = [];
|
|
376
|
+
functionIdMap = /* @__PURE__ */ new Map();
|
|
377
|
+
constructor(targetPath) {
|
|
378
|
+
this.targetPath = path3.resolve(targetPath);
|
|
379
|
+
this.dbPath = path3.join(this.targetPath, ".codeql-db");
|
|
380
|
+
this.queriesPath = path3.join(__dirname, "queries");
|
|
381
|
+
this.queryRunner = new QueryRunner(this.dbPath, this.queriesPath, this.targetPath);
|
|
382
|
+
}
|
|
383
|
+
async prepare() {
|
|
384
|
+
try {
|
|
385
|
+
execSync2("codeql --version", { stdio: "pipe" });
|
|
386
|
+
} catch {
|
|
387
|
+
throw new Error("CodeQL CLI is not installed. Please install it from https://github.com/github/codeql-cli-binaries");
|
|
388
|
+
}
|
|
389
|
+
console.log("Creating CodeQL database...");
|
|
390
|
+
await this.createDatabase();
|
|
391
|
+
}
|
|
392
|
+
async createDatabase() {
|
|
393
|
+
if (fs3.existsSync(this.dbPath)) {
|
|
394
|
+
fs3.rmSync(this.dbPath, { recursive: true });
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
execSync2(`codeql database create "${this.dbPath}" --language=javascript --source-root="${this.targetPath}" --overwrite`, {
|
|
398
|
+
stdio: "inherit",
|
|
399
|
+
cwd: this.targetPath
|
|
400
|
+
});
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw new Error(`Failed to create CodeQL database: ${error}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async analyze() {
|
|
406
|
+
console.log("Running CodeQL queries...");
|
|
407
|
+
await this.runFunctionsQuery();
|
|
408
|
+
await this.runCallsQuery();
|
|
409
|
+
await this.runDataflowQuery();
|
|
410
|
+
this.buildModulesAndComponents();
|
|
411
|
+
}
|
|
412
|
+
async runFunctionsQuery() {
|
|
413
|
+
console.log(" Extracting functions...");
|
|
414
|
+
const results = await this.queryRunner.runQuery("functions.ql");
|
|
415
|
+
for (const result of results) {
|
|
416
|
+
const fnId = generateFunctionId(result.file, result.name, result.startLine);
|
|
417
|
+
const key = `${result.file}:${result.name}:${result.startLine}`;
|
|
418
|
+
this.functionIdMap.set(key, fnId);
|
|
419
|
+
this.functions.push({
|
|
420
|
+
id: fnId,
|
|
421
|
+
name: result.name,
|
|
422
|
+
fullyQualifiedName: `${result.file}::${result.name}`,
|
|
423
|
+
file: result.file,
|
|
424
|
+
startLine: result.startLine,
|
|
425
|
+
endLine: result.endLine,
|
|
426
|
+
kind: this.normalizeKind(result.kind)
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
console.log(` Found ${this.functions.length} functions`);
|
|
430
|
+
}
|
|
431
|
+
async runCallsQuery() {
|
|
432
|
+
console.log(" Extracting call edges...");
|
|
433
|
+
const results = await this.queryRunner.runQuery("calls.ql");
|
|
434
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
435
|
+
for (const result of results) {
|
|
436
|
+
const callerId = this.getFunctionId(result.callerFile, result.callerName, result.callerStartLine);
|
|
437
|
+
const calleeId = this.getFunctionId(result.calleeFile, result.calleeName, result.calleeStartLine);
|
|
438
|
+
if (!callerId || !calleeId)
|
|
439
|
+
continue;
|
|
440
|
+
const edgeKey = `${callerId}->${calleeId}`;
|
|
441
|
+
if (edgeMap.has(edgeKey)) {
|
|
442
|
+
edgeMap.get(edgeKey).callsites.push({
|
|
443
|
+
file: result.callsiteFile,
|
|
444
|
+
line: result.callsiteLine
|
|
445
|
+
});
|
|
446
|
+
} else {
|
|
447
|
+
edgeMap.set(edgeKey, {
|
|
448
|
+
from: callerId,
|
|
449
|
+
to: calleeId,
|
|
450
|
+
callsites: [{ file: result.callsiteFile, line: result.callsiteLine }]
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
this.callEdges = Array.from(edgeMap.values());
|
|
455
|
+
console.log(` Found ${this.callEdges.length} call edges`);
|
|
456
|
+
}
|
|
457
|
+
async runDataflowQuery() {
|
|
458
|
+
console.log(" Extracting dataflow edges...");
|
|
459
|
+
const results = await this.queryRunner.runQuery("dataflow.ql");
|
|
460
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
461
|
+
for (const result of results) {
|
|
462
|
+
const sourceId = this.getFunctionId(result.sourceFile, result.sourceFunction, result.sourceStartLine);
|
|
463
|
+
const sinkId = this.getFunctionId(result.sinkFile, result.sinkFunction, result.sinkStartLine);
|
|
464
|
+
if (!sourceId || !sinkId || sourceId === sinkId)
|
|
465
|
+
continue;
|
|
466
|
+
const edgeKey = `${sourceId}->${sinkId}`;
|
|
467
|
+
if (!edgeMap.has(edgeKey)) {
|
|
468
|
+
edgeMap.set(edgeKey, {
|
|
469
|
+
from: sourceId,
|
|
470
|
+
to: sinkId,
|
|
471
|
+
label: `data from ${result.sourceFunction} to ${result.sinkFunction}`,
|
|
472
|
+
evidence: []
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
edgeMap.get(edgeKey).evidence.push(`Flow from ${result.sourceFile}:${result.sourceStartLine} to ${result.sinkFile}:${result.sinkStartLine}`);
|
|
476
|
+
}
|
|
477
|
+
this.dataflowEdges = Array.from(edgeMap.values());
|
|
478
|
+
console.log(` Found ${this.dataflowEdges.length} dataflow edges`);
|
|
479
|
+
}
|
|
480
|
+
getFunctionId(file, name, startLine) {
|
|
481
|
+
return this.functionIdMap.get(`${file}:${name}:${startLine}`);
|
|
482
|
+
}
|
|
483
|
+
normalizeKind(kind) {
|
|
484
|
+
switch (kind.toLowerCase()) {
|
|
485
|
+
case "arrow":
|
|
486
|
+
return "arrow";
|
|
487
|
+
case "method":
|
|
488
|
+
return "method";
|
|
489
|
+
case "constructor":
|
|
490
|
+
return "constructor";
|
|
491
|
+
default:
|
|
492
|
+
return "function";
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
buildModulesAndComponents() {
|
|
496
|
+
const fileToFunctions = /* @__PURE__ */ new Map();
|
|
497
|
+
for (const fn of this.functions) {
|
|
498
|
+
if (!fileToFunctions.has(fn.file)) {
|
|
499
|
+
fileToFunctions.set(fn.file, []);
|
|
500
|
+
}
|
|
501
|
+
fileToFunctions.get(fn.file).push(fn);
|
|
502
|
+
}
|
|
503
|
+
for (const [file, fns] of fileToFunctions) {
|
|
504
|
+
this.modules.push({
|
|
505
|
+
id: generateModuleId(file),
|
|
506
|
+
path: file,
|
|
507
|
+
functionIds: fns.map((f) => f.id)
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
const dirToModules = /* @__PURE__ */ new Map();
|
|
511
|
+
for (const mod of this.modules) {
|
|
512
|
+
const dir = path3.dirname(mod.path) || "root";
|
|
513
|
+
if (!dirToModules.has(dir)) {
|
|
514
|
+
dirToModules.set(dir, []);
|
|
515
|
+
}
|
|
516
|
+
dirToModules.get(dir).push(mod);
|
|
517
|
+
}
|
|
518
|
+
for (const [dir, mods] of dirToModules) {
|
|
519
|
+
const compName = dir === "." || dir === "root" ? "main" : dir.replace(/[/\\]/g, "-");
|
|
520
|
+
this.components.push({
|
|
521
|
+
id: generateComponentId(compName),
|
|
522
|
+
name: compName,
|
|
523
|
+
moduleIds: mods.map((m) => m.id)
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async export() {
|
|
528
|
+
let repoCommit = "unknown";
|
|
529
|
+
try {
|
|
530
|
+
repoCommit = execSync2("git rev-parse HEAD", { cwd: this.targetPath, stdio: "pipe" }).toString().trim().slice(0, 8);
|
|
531
|
+
} catch {
|
|
532
|
+
repoCommit = `local-${Date.now().toString(16)}`;
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
metadata: {
|
|
536
|
+
analyzerVersion: "0.1.0",
|
|
537
|
+
language: "typescript",
|
|
538
|
+
repoCommit,
|
|
539
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
540
|
+
configHash: this.generateConfigHash()
|
|
541
|
+
},
|
|
542
|
+
nodes: {
|
|
543
|
+
functions: this.sortById(this.functions),
|
|
544
|
+
modules: this.sortById(this.modules),
|
|
545
|
+
components: this.sortById(this.components)
|
|
546
|
+
},
|
|
547
|
+
edges: {
|
|
548
|
+
calls: this.sortEdges(this.callEdges),
|
|
549
|
+
dataflow: this.sortEdges(this.dataflowEdges)
|
|
550
|
+
},
|
|
551
|
+
layout: { positions: {} }
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
async cleanup() {
|
|
555
|
+
if (fs3.existsSync(this.dbPath)) {
|
|
556
|
+
try {
|
|
557
|
+
fs3.rmSync(this.dbPath, { recursive: true });
|
|
558
|
+
} catch {
|
|
559
|
+
console.warn("Warning: Could not clean up CodeQL database");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
generateConfigHash() {
|
|
564
|
+
const queryFiles = fs3.readdirSync(this.queriesPath).sort();
|
|
565
|
+
return `codeql-${this.hashString(queryFiles.join(",")).toString(16).padStart(8, "0")}`;
|
|
566
|
+
}
|
|
567
|
+
hashString(str) {
|
|
568
|
+
let hash = 0;
|
|
569
|
+
for (let i = 0; i < str.length; i++) {
|
|
570
|
+
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
571
|
+
hash = hash & hash;
|
|
572
|
+
}
|
|
573
|
+
return Math.abs(hash);
|
|
574
|
+
}
|
|
575
|
+
sortById(items) {
|
|
576
|
+
return [...items].sort((a, b) => a.id.localeCompare(b.id));
|
|
577
|
+
}
|
|
578
|
+
sortEdges(edges) {
|
|
579
|
+
return [...edges].sort((a, b) => {
|
|
580
|
+
const cmp = a.from.localeCompare(b.from);
|
|
581
|
+
return cmp !== 0 ? cmp : a.to.localeCompare(b.to);
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// ../analyzer/dist/exporters/codeql/index.js
|
|
589
|
+
var init_codeql = __esm({
|
|
590
|
+
"../analyzer/dist/exporters/codeql/index.js"() {
|
|
591
|
+
"use strict";
|
|
592
|
+
init_codeql_exporter();
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// ../analyzer/dist/layout/elk-layout.js
|
|
597
|
+
import { createRequire as _createRequire } from "module";
|
|
598
|
+
async function applyElkLayout(artifact) {
|
|
599
|
+
const elkGraph = buildElkGraph(artifact);
|
|
600
|
+
const layoutedGraph = await elk.layout(elkGraph);
|
|
601
|
+
const positions = extractPositions(layoutedGraph);
|
|
602
|
+
return {
|
|
603
|
+
...artifact,
|
|
604
|
+
layout: {
|
|
605
|
+
...artifact.layout,
|
|
606
|
+
positions
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function buildElkGraph(artifact) {
|
|
611
|
+
const children = artifact.nodes.functions.map((fn) => ({
|
|
612
|
+
id: fn.id,
|
|
613
|
+
width: 120,
|
|
614
|
+
height: 40
|
|
615
|
+
}));
|
|
616
|
+
const edges = [
|
|
617
|
+
...artifact.edges.calls.map((edge) => ({
|
|
618
|
+
id: `call-${edge.from}-${edge.to}`,
|
|
619
|
+
sources: [edge.from],
|
|
620
|
+
targets: [edge.to]
|
|
621
|
+
})),
|
|
622
|
+
...artifact.edges.dataflow.map((edge) => ({
|
|
623
|
+
id: `df-${edge.from}-${edge.to}`,
|
|
624
|
+
sources: [edge.from],
|
|
625
|
+
targets: [edge.to]
|
|
626
|
+
}))
|
|
627
|
+
];
|
|
628
|
+
return {
|
|
629
|
+
id: "root",
|
|
630
|
+
layoutOptions: {
|
|
631
|
+
"elk.algorithm": "layered",
|
|
632
|
+
"elk.direction": "RIGHT",
|
|
633
|
+
"elk.spacing.nodeNode": "50",
|
|
634
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": "100",
|
|
635
|
+
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX"
|
|
636
|
+
},
|
|
637
|
+
children,
|
|
638
|
+
edges
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function extractPositions(layoutedGraph) {
|
|
642
|
+
const positions = {};
|
|
643
|
+
if (layoutedGraph.children) {
|
|
644
|
+
for (const child of layoutedGraph.children) {
|
|
645
|
+
if (child.x !== void 0 && child.y !== void 0) {
|
|
646
|
+
positions[child.id] = {
|
|
647
|
+
x: child.x,
|
|
648
|
+
y: child.y
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return positions;
|
|
654
|
+
}
|
|
655
|
+
var __require, ELKModule, ELKConstructor, elk;
|
|
656
|
+
var init_elk_layout = __esm({
|
|
657
|
+
"../analyzer/dist/layout/elk-layout.js"() {
|
|
658
|
+
"use strict";
|
|
659
|
+
__require = _createRequire(import.meta.url);
|
|
660
|
+
ELKModule = __require("elkjs");
|
|
661
|
+
ELKConstructor = ELKModule.default;
|
|
662
|
+
elk = new ELKConstructor();
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// ../analyzer/dist/commands/analyze.js
|
|
667
|
+
import fs4 from "fs";
|
|
668
|
+
import path4 from "path";
|
|
669
|
+
async function analyzeCommand(targetPath, options) {
|
|
670
|
+
console.log(`Analyzing: ${path4.resolve(targetPath)}`);
|
|
671
|
+
console.log(`Exporter: ${options.exporter}`);
|
|
672
|
+
console.log(`Output: ${options.output}`);
|
|
673
|
+
console.log("");
|
|
674
|
+
const absolutePath = path4.resolve(targetPath);
|
|
675
|
+
if (!fs4.existsSync(absolutePath)) {
|
|
676
|
+
console.error(`Error: Path does not exist: ${absolutePath}`);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
let artifact;
|
|
680
|
+
if (options.exporter === "mock") {
|
|
681
|
+
console.log("Using mock exporter...");
|
|
682
|
+
const exporter = new MockExporter(absolutePath);
|
|
683
|
+
await exporter.prepare();
|
|
684
|
+
await exporter.analyze();
|
|
685
|
+
artifact = await exporter.export();
|
|
686
|
+
await exporter.cleanup();
|
|
687
|
+
} else if (options.exporter === "codeql-ts") {
|
|
688
|
+
console.log("Using CodeQL exporter for TypeScript...");
|
|
689
|
+
const exporter = new CodeQLExporter(absolutePath);
|
|
690
|
+
await exporter.prepare();
|
|
691
|
+
await exporter.analyze();
|
|
692
|
+
artifact = await exporter.export();
|
|
693
|
+
await exporter.cleanup();
|
|
694
|
+
} else {
|
|
695
|
+
console.error(`Unknown exporter: ${options.exporter}`);
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
console.log("Applying ELK layout...");
|
|
699
|
+
artifact = await applyElkLayout(artifact);
|
|
700
|
+
const outputDir = path4.dirname(path4.resolve(options.output));
|
|
701
|
+
if (!fs4.existsSync(outputDir)) {
|
|
702
|
+
fs4.mkdirSync(outputDir, { recursive: true });
|
|
703
|
+
}
|
|
704
|
+
const outputPath = path4.resolve(options.output);
|
|
705
|
+
const jsonContent = JSON.stringify(artifact, null, 2);
|
|
706
|
+
fs4.writeFileSync(outputPath, jsonContent, "utf-8");
|
|
707
|
+
console.log("");
|
|
708
|
+
console.log(`Artifact written to: ${outputPath}`);
|
|
709
|
+
console.log(` Functions: ${artifact.nodes.functions.length}`);
|
|
710
|
+
console.log(` Modules: ${artifact.nodes.modules.length}`);
|
|
711
|
+
console.log(` Call edges: ${artifact.edges.calls.length}`);
|
|
712
|
+
console.log(` Dataflow edges: ${artifact.edges.dataflow.length}`);
|
|
713
|
+
}
|
|
714
|
+
var init_analyze = __esm({
|
|
715
|
+
"../analyzer/dist/commands/analyze.js"() {
|
|
716
|
+
"use strict";
|
|
717
|
+
init_mock_exporter();
|
|
718
|
+
init_codeql();
|
|
719
|
+
init_elk_layout();
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
// ../analyzer/dist/commands/serve.js
|
|
724
|
+
import fs5 from "fs";
|
|
725
|
+
import path5 from "path";
|
|
726
|
+
import { spawn } from "child_process";
|
|
727
|
+
async function serveCommand(options) {
|
|
728
|
+
const artifactPath = path5.resolve(options.artifact);
|
|
729
|
+
if (!fs5.existsSync(artifactPath)) {
|
|
730
|
+
console.error(`Error: Artifact not found: ${artifactPath}`);
|
|
731
|
+
console.error('Run "codeflow analyze" first to generate the artifact.');
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
console.log(`Starting CodeFlowMap UI...`);
|
|
735
|
+
console.log(`Artifact: ${artifactPath}`);
|
|
736
|
+
console.log(`Port: ${options.port}`);
|
|
737
|
+
console.log("");
|
|
738
|
+
const uiPackagePath = path5.resolve(__dirname, "../../ui");
|
|
739
|
+
if (!fs5.existsSync(uiPackagePath)) {
|
|
740
|
+
const monorepoUiPath = path5.resolve(__dirname, "../../../ui");
|
|
741
|
+
if (fs5.existsSync(monorepoUiPath)) {
|
|
742
|
+
startVite(monorepoUiPath, options.port);
|
|
743
|
+
} else {
|
|
744
|
+
console.error("UI package not found. Run from the monorepo root.");
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
startVite(uiPackagePath, options.port);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
function startVite(uiPath, port) {
|
|
752
|
+
console.log(`Starting Vite dev server at ${uiPath}...`);
|
|
753
|
+
console.log(`Open http://localhost:${port} in your browser`);
|
|
754
|
+
console.log("");
|
|
755
|
+
const vite = spawn("npx", ["vite", "--port", port], {
|
|
756
|
+
cwd: uiPath,
|
|
757
|
+
stdio: "inherit",
|
|
758
|
+
shell: true
|
|
759
|
+
});
|
|
760
|
+
vite.on("error", (err) => {
|
|
761
|
+
console.error("Failed to start Vite:", err);
|
|
762
|
+
process.exit(1);
|
|
763
|
+
});
|
|
764
|
+
vite.on("close", (code) => {
|
|
765
|
+
process.exit(code ?? 0);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
var init_serve = __esm({
|
|
769
|
+
"../analyzer/dist/commands/serve.js"() {
|
|
770
|
+
"use strict";
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
// ../analyzer/dist/commands/diff.js
|
|
775
|
+
import fs6 from "fs";
|
|
776
|
+
import path6 from "path";
|
|
777
|
+
async function diffCommand(baselinePath, currentPath, options) {
|
|
778
|
+
const baselineArtifact = loadArtifact(baselinePath, "Baseline");
|
|
779
|
+
if (!baselineArtifact) {
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
const currentArtifact = loadArtifact(currentPath, "Current");
|
|
783
|
+
if (!currentArtifact) {
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
const diff = calculateDiff(baselineArtifact, currentArtifact);
|
|
787
|
+
if (options.format === "json") {
|
|
788
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
789
|
+
} else {
|
|
790
|
+
printTextDiff(diff, baselinePath, currentPath);
|
|
791
|
+
}
|
|
792
|
+
if (options.exitOnChanges && diff.hasChanges) {
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function loadArtifact(filePath, label) {
|
|
797
|
+
const absolutePath = path6.resolve(filePath);
|
|
798
|
+
if (!fs6.existsSync(absolutePath)) {
|
|
799
|
+
console.error(`Error: ${label} artifact not found: ${absolutePath}`);
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
const content = fs6.readFileSync(absolutePath, "utf-8");
|
|
804
|
+
const artifact = JSON.parse(content);
|
|
805
|
+
if (!isCodeFlowArtifact(artifact)) {
|
|
806
|
+
console.error(`Error: ${label} file is not a valid CodeFlow artifact`);
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
return artifact;
|
|
810
|
+
} catch (error) {
|
|
811
|
+
console.error(`Error: Failed to parse ${label} artifact: ${error}`);
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function calculateDiff(baseline, current) {
|
|
816
|
+
const baselineFnIds = new Set(baseline.nodes.functions.map((fn) => fn.id));
|
|
817
|
+
const currentFnIds = new Set(current.nodes.functions.map((fn) => fn.id));
|
|
818
|
+
const addedFunctions = current.nodes.functions.filter((fn) => !baselineFnIds.has(fn.id)).map((fn) => `${fn.name} (${fn.file}:${fn.startLine})`);
|
|
819
|
+
const removedFunctions = baseline.nodes.functions.filter((fn) => !currentFnIds.has(fn.id)).map((fn) => `${fn.name} (${fn.file}:${fn.startLine})`);
|
|
820
|
+
const baselineModIds = new Set(baseline.nodes.modules.map((m) => m.id));
|
|
821
|
+
const currentModIds = new Set(current.nodes.modules.map((m) => m.id));
|
|
822
|
+
const addedModules = current.nodes.modules.filter((m) => !baselineModIds.has(m.id)).map((m) => m.path);
|
|
823
|
+
const removedModules = baseline.nodes.modules.filter((m) => !currentModIds.has(m.id)).map((m) => m.path);
|
|
824
|
+
const edgeKey = (e) => `${e.from}->${e.to}`;
|
|
825
|
+
const baselineCallEdges = new Set(baseline.edges.calls.map(edgeKey));
|
|
826
|
+
const currentCallEdges = new Set(current.edges.calls.map(edgeKey));
|
|
827
|
+
const addedCallEdges = current.edges.calls.filter((e) => !baselineCallEdges.has(edgeKey(e))).map((e) => formatEdge(e, current));
|
|
828
|
+
const removedCallEdges = baseline.edges.calls.filter((e) => !currentCallEdges.has(edgeKey(e))).map((e) => formatEdge(e, baseline));
|
|
829
|
+
const baselineDataflowEdges = new Set(baseline.edges.dataflow.map(edgeKey));
|
|
830
|
+
const currentDataflowEdges = new Set(current.edges.dataflow.map(edgeKey));
|
|
831
|
+
const addedDataflowEdges = current.edges.dataflow.filter((e) => !baselineDataflowEdges.has(edgeKey(e))).map((e) => formatEdge(e, current));
|
|
832
|
+
const removedDataflowEdges = baseline.edges.dataflow.filter((e) => !currentDataflowEdges.has(edgeKey(e))).map((e) => formatEdge(e, baseline));
|
|
833
|
+
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;
|
|
834
|
+
return {
|
|
835
|
+
hasChanges,
|
|
836
|
+
summary: {
|
|
837
|
+
functionsAdded: addedFunctions.length,
|
|
838
|
+
functionsRemoved: removedFunctions.length,
|
|
839
|
+
modulesAdded: addedModules.length,
|
|
840
|
+
modulesRemoved: removedModules.length,
|
|
841
|
+
callEdgesAdded: addedCallEdges.length,
|
|
842
|
+
callEdgesRemoved: removedCallEdges.length,
|
|
843
|
+
dataflowEdgesAdded: addedDataflowEdges.length,
|
|
844
|
+
dataflowEdgesRemoved: removedDataflowEdges.length
|
|
845
|
+
},
|
|
846
|
+
details: {
|
|
847
|
+
addedFunctions,
|
|
848
|
+
removedFunctions,
|
|
849
|
+
addedModules,
|
|
850
|
+
removedModules,
|
|
851
|
+
addedCallEdges,
|
|
852
|
+
removedCallEdges,
|
|
853
|
+
addedDataflowEdges,
|
|
854
|
+
removedDataflowEdges
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
function formatEdge(edge, artifact) {
|
|
859
|
+
const fromFn = artifact.nodes.functions.find((fn) => fn.id === edge.from);
|
|
860
|
+
const toFn = artifact.nodes.functions.find((fn) => fn.id === edge.to);
|
|
861
|
+
const fromName = fromFn ? fromFn.name : edge.from;
|
|
862
|
+
const toName = toFn ? toFn.name : edge.to;
|
|
863
|
+
return `${fromName} -> ${toName}`;
|
|
864
|
+
}
|
|
865
|
+
function printTextDiff(diff, baselinePath, currentPath) {
|
|
866
|
+
console.log("CodeFlow Artifact Diff");
|
|
867
|
+
console.log("======================");
|
|
868
|
+
console.log(`Baseline: ${baselinePath}`);
|
|
869
|
+
console.log(`Current: ${currentPath}`);
|
|
870
|
+
console.log("");
|
|
871
|
+
if (!diff.hasChanges) {
|
|
872
|
+
console.log("No changes detected.");
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
console.log("Summary:");
|
|
876
|
+
console.log(` Functions: +${diff.summary.functionsAdded} / -${diff.summary.functionsRemoved}`);
|
|
877
|
+
console.log(` Modules: +${diff.summary.modulesAdded} / -${diff.summary.modulesRemoved}`);
|
|
878
|
+
console.log(` Call Edges: +${diff.summary.callEdgesAdded} / -${diff.summary.callEdgesRemoved}`);
|
|
879
|
+
console.log(` Dataflow Edges: +${diff.summary.dataflowEdgesAdded} / -${diff.summary.dataflowEdgesRemoved}`);
|
|
880
|
+
console.log("");
|
|
881
|
+
if (diff.details.addedFunctions.length > 0) {
|
|
882
|
+
console.log("Added Functions:");
|
|
883
|
+
for (const fn of diff.details.addedFunctions) {
|
|
884
|
+
console.log(` + ${fn}`);
|
|
885
|
+
}
|
|
886
|
+
console.log("");
|
|
887
|
+
}
|
|
888
|
+
if (diff.details.removedFunctions.length > 0) {
|
|
889
|
+
console.log("Removed Functions:");
|
|
890
|
+
for (const fn of diff.details.removedFunctions) {
|
|
891
|
+
console.log(` - ${fn}`);
|
|
892
|
+
}
|
|
893
|
+
console.log("");
|
|
894
|
+
}
|
|
895
|
+
if (diff.details.addedModules.length > 0) {
|
|
896
|
+
console.log("Added Modules:");
|
|
897
|
+
for (const mod of diff.details.addedModules) {
|
|
898
|
+
console.log(` + ${mod}`);
|
|
899
|
+
}
|
|
900
|
+
console.log("");
|
|
901
|
+
}
|
|
902
|
+
if (diff.details.removedModules.length > 0) {
|
|
903
|
+
console.log("Removed Modules:");
|
|
904
|
+
for (const mod of diff.details.removedModules) {
|
|
905
|
+
console.log(` - ${mod}`);
|
|
906
|
+
}
|
|
907
|
+
console.log("");
|
|
908
|
+
}
|
|
909
|
+
if (diff.details.addedCallEdges.length > 0) {
|
|
910
|
+
console.log("Added Call Edges:");
|
|
911
|
+
for (const edge of diff.details.addedCallEdges) {
|
|
912
|
+
console.log(` + ${edge}`);
|
|
913
|
+
}
|
|
914
|
+
console.log("");
|
|
915
|
+
}
|
|
916
|
+
if (diff.details.removedCallEdges.length > 0) {
|
|
917
|
+
console.log("Removed Call Edges:");
|
|
918
|
+
for (const edge of diff.details.removedCallEdges) {
|
|
919
|
+
console.log(` - ${edge}`);
|
|
920
|
+
}
|
|
921
|
+
console.log("");
|
|
922
|
+
}
|
|
923
|
+
if (diff.details.addedDataflowEdges.length > 0) {
|
|
924
|
+
console.log("Added Dataflow Edges:");
|
|
925
|
+
for (const edge of diff.details.addedDataflowEdges) {
|
|
926
|
+
console.log(` + ${edge}`);
|
|
927
|
+
}
|
|
928
|
+
console.log("");
|
|
929
|
+
}
|
|
930
|
+
if (diff.details.removedDataflowEdges.length > 0) {
|
|
931
|
+
console.log("Removed Dataflow Edges:");
|
|
932
|
+
for (const edge of diff.details.removedDataflowEdges) {
|
|
933
|
+
console.log(` - ${edge}`);
|
|
934
|
+
}
|
|
935
|
+
console.log("");
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
var init_diff = __esm({
|
|
939
|
+
"../analyzer/dist/commands/diff.js"() {
|
|
940
|
+
"use strict";
|
|
941
|
+
init_dist();
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// ../analyzer/dist/index.js
|
|
946
|
+
var dist_exports = {};
|
|
947
|
+
__export(dist_exports, {
|
|
948
|
+
CodeQLExporter: () => CodeQLExporter,
|
|
949
|
+
MockExporter: () => MockExporter,
|
|
950
|
+
analyzeCommand: () => analyzeCommand,
|
|
951
|
+
applyElkLayout: () => applyElkLayout,
|
|
952
|
+
diffCommand: () => diffCommand,
|
|
953
|
+
serveCommand: () => serveCommand
|
|
954
|
+
});
|
|
955
|
+
var init_dist2 = __esm({
|
|
956
|
+
"../analyzer/dist/index.js"() {
|
|
957
|
+
"use strict";
|
|
958
|
+
init_analyze();
|
|
959
|
+
init_serve();
|
|
960
|
+
init_diff();
|
|
961
|
+
init_elk_layout();
|
|
962
|
+
init_mock_exporter();
|
|
963
|
+
init_codeql_exporter();
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
// src/commands/analyze.ts
|
|
968
|
+
init_dist2();
|
|
969
|
+
import fs7 from "fs";
|
|
970
|
+
import path7 from "path";
|
|
971
|
+
import ora from "ora";
|
|
972
|
+
|
|
973
|
+
// src/utils/output.ts
|
|
974
|
+
import chalk from "chalk";
|
|
975
|
+
var log = {
|
|
976
|
+
info: (message) => console.log(chalk.blue("\u2139"), message),
|
|
977
|
+
success: (message) => console.log(chalk.green("\u2713"), message),
|
|
978
|
+
warning: (message) => console.log(chalk.yellow("\u26A0"), message),
|
|
979
|
+
error: (message) => console.error(chalk.red("\u2717"), message),
|
|
980
|
+
title: (message) => console.log(chalk.bold.cyan(`
|
|
981
|
+
${message}
|
|
982
|
+
`)),
|
|
983
|
+
dim: (message) => console.log(chalk.dim(message))
|
|
984
|
+
};
|
|
985
|
+
function formatNumber(n) {
|
|
986
|
+
return n.toLocaleString();
|
|
987
|
+
}
|
|
988
|
+
function formatPath(p) {
|
|
989
|
+
return chalk.underline(p);
|
|
990
|
+
}
|
|
991
|
+
function formatDiff(added, removed) {
|
|
992
|
+
const parts = [];
|
|
993
|
+
if (added > 0) parts.push(chalk.green(`+${added}`));
|
|
994
|
+
if (removed > 0) parts.push(chalk.red(`-${removed}`));
|
|
995
|
+
return parts.join(" / ") || chalk.dim("no changes");
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/commands/analyze.ts
|
|
999
|
+
async function analyzeCommand2(targetPath, options) {
|
|
1000
|
+
const absolutePath = path7.resolve(targetPath);
|
|
1001
|
+
if (!fs7.existsSync(absolutePath)) {
|
|
1002
|
+
log.error(`Path does not exist: ${formatPath(absolutePath)}`);
|
|
1003
|
+
process.exit(1);
|
|
1004
|
+
}
|
|
1005
|
+
log.title("Monarch Analysis");
|
|
1006
|
+
log.info(`Target: ${formatPath(absolutePath)}`);
|
|
1007
|
+
log.info(`Exporter: ${options.exporter}`);
|
|
1008
|
+
log.info(`Output: ${formatPath(options.output)}`);
|
|
1009
|
+
const spinner = ora("Analyzing codebase...").start();
|
|
1010
|
+
try {
|
|
1011
|
+
let artifact;
|
|
1012
|
+
if (options.exporter === "mock") {
|
|
1013
|
+
const exporter = new MockExporter(absolutePath);
|
|
1014
|
+
await exporter.prepare();
|
|
1015
|
+
spinner.text = "Scanning files...";
|
|
1016
|
+
await exporter.analyze();
|
|
1017
|
+
spinner.text = "Generating artifact...";
|
|
1018
|
+
artifact = await exporter.export();
|
|
1019
|
+
await exporter.cleanup();
|
|
1020
|
+
} else if (options.exporter === "codeql-ts") {
|
|
1021
|
+
spinner.text = "Creating CodeQL database...";
|
|
1022
|
+
const { CodeQLExporter: CodeQLExporter2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports));
|
|
1023
|
+
const exporter = new CodeQLExporter2(absolutePath);
|
|
1024
|
+
await exporter.prepare();
|
|
1025
|
+
spinner.text = "Running CodeQL queries...";
|
|
1026
|
+
await exporter.analyze();
|
|
1027
|
+
spinner.text = "Generating artifact...";
|
|
1028
|
+
artifact = await exporter.export();
|
|
1029
|
+
await exporter.cleanup();
|
|
1030
|
+
} else {
|
|
1031
|
+
spinner.fail(`Unknown exporter: ${options.exporter}`);
|
|
1032
|
+
process.exit(1);
|
|
1033
|
+
}
|
|
1034
|
+
spinner.text = "Computing layout...";
|
|
1035
|
+
artifact = await applyElkLayout(artifact);
|
|
1036
|
+
const outputDir = path7.dirname(path7.resolve(options.output));
|
|
1037
|
+
if (!fs7.existsSync(outputDir)) {
|
|
1038
|
+
fs7.mkdirSync(outputDir, { recursive: true });
|
|
1039
|
+
}
|
|
1040
|
+
const outputPath = path7.resolve(options.output);
|
|
1041
|
+
fs7.writeFileSync(outputPath, JSON.stringify(artifact, null, 2));
|
|
1042
|
+
spinner.succeed("Analysis complete!");
|
|
1043
|
+
console.log("");
|
|
1044
|
+
log.success(`Graph saved to ${formatPath(outputPath)}`);
|
|
1045
|
+
console.log("");
|
|
1046
|
+
log.info(`Functions: ${formatNumber(artifact.nodes.functions.length)}`);
|
|
1047
|
+
log.info(`Modules: ${formatNumber(artifact.nodes.modules.length)}`);
|
|
1048
|
+
log.info(`Components: ${formatNumber(artifact.nodes.components.length)}`);
|
|
1049
|
+
log.info(`Call edges: ${formatNumber(artifact.edges.calls.length)}`);
|
|
1050
|
+
log.info(`Dataflow: ${formatNumber(artifact.edges.dataflow.length)}`);
|
|
1051
|
+
if (options.watch) {
|
|
1052
|
+
log.info("\nWatching for changes... (press Ctrl+C to stop)");
|
|
1053
|
+
}
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
spinner.fail("Analysis failed");
|
|
1056
|
+
log.error(String(error));
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/commands/diff.ts
|
|
1062
|
+
init_dist();
|
|
1063
|
+
import fs8 from "fs";
|
|
1064
|
+
import path8 from "path";
|
|
1065
|
+
import chalk2 from "chalk";
|
|
1066
|
+
async function diffCommand2(baselinePath, currentPath, options) {
|
|
1067
|
+
const baseline = loadArtifact2(baselinePath, "Baseline");
|
|
1068
|
+
const current = loadArtifact2(currentPath, "Current");
|
|
1069
|
+
if (!baseline || !current) {
|
|
1070
|
+
process.exit(1);
|
|
1071
|
+
}
|
|
1072
|
+
const diff = calculateDiff2(baseline, current);
|
|
1073
|
+
switch (options.format) {
|
|
1074
|
+
case "json":
|
|
1075
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
1076
|
+
break;
|
|
1077
|
+
case "markdown":
|
|
1078
|
+
printMarkdownDiff(diff, baselinePath, currentPath, options.only);
|
|
1079
|
+
break;
|
|
1080
|
+
default:
|
|
1081
|
+
printTextDiff2(diff, baselinePath, currentPath, options.only);
|
|
1082
|
+
}
|
|
1083
|
+
if (options.exitOnChanges && diff.hasChanges) {
|
|
1084
|
+
process.exit(1);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
function loadArtifact2(filePath, label) {
|
|
1088
|
+
const absolutePath = path8.resolve(filePath);
|
|
1089
|
+
if (!fs8.existsSync(absolutePath)) {
|
|
1090
|
+
log.error(`${label} artifact not found: ${formatPath(absolutePath)}`);
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
const content = fs8.readFileSync(absolutePath, "utf-8");
|
|
1095
|
+
const artifact = JSON.parse(content);
|
|
1096
|
+
if (!isCodeFlowArtifact(artifact)) {
|
|
1097
|
+
log.error(`${label} file is not a valid Monarch artifact`);
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
return artifact;
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
log.error(`Failed to parse ${label} artifact: ${error}`);
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
function calculateDiff2(baseline, current) {
|
|
1107
|
+
const baselineFnIds = new Set(baseline.nodes.functions.map((fn) => fn.id));
|
|
1108
|
+
const currentFnIds = new Set(current.nodes.functions.map((fn) => fn.id));
|
|
1109
|
+
const addedFunctions = current.nodes.functions.filter((fn) => !baselineFnIds.has(fn.id)).map((fn) => `${fn.name} (${fn.file}:${fn.startLine})`);
|
|
1110
|
+
const removedFunctions = baseline.nodes.functions.filter((fn) => !currentFnIds.has(fn.id)).map((fn) => `${fn.name} (${fn.file}:${fn.startLine})`);
|
|
1111
|
+
const baselineModIds = new Set(baseline.nodes.modules.map((m) => m.id));
|
|
1112
|
+
const currentModIds = new Set(current.nodes.modules.map((m) => m.id));
|
|
1113
|
+
const addedModules = current.nodes.modules.filter((m) => !baselineModIds.has(m.id)).map((m) => m.path);
|
|
1114
|
+
const removedModules = baseline.nodes.modules.filter((m) => !currentModIds.has(m.id)).map((m) => m.path);
|
|
1115
|
+
const edgeKey = (e) => `${e.from}->${e.to}`;
|
|
1116
|
+
const baselineCallEdges = new Set(baseline.edges.calls.map(edgeKey));
|
|
1117
|
+
const currentCallEdges = new Set(current.edges.calls.map(edgeKey));
|
|
1118
|
+
const addedCallEdges = current.edges.calls.filter((e) => !baselineCallEdges.has(edgeKey(e))).map((e) => formatEdge2(e, current));
|
|
1119
|
+
const removedCallEdges = baseline.edges.calls.filter((e) => !currentCallEdges.has(edgeKey(e))).map((e) => formatEdge2(e, baseline));
|
|
1120
|
+
const baselineDataflowEdges = new Set(baseline.edges.dataflow.map(edgeKey));
|
|
1121
|
+
const currentDataflowEdges = new Set(current.edges.dataflow.map(edgeKey));
|
|
1122
|
+
const addedDataflowEdges = current.edges.dataflow.filter((e) => !baselineDataflowEdges.has(edgeKey(e))).map((e) => formatEdge2(e, current));
|
|
1123
|
+
const removedDataflowEdges = baseline.edges.dataflow.filter((e) => !currentDataflowEdges.has(edgeKey(e))).map((e) => formatEdge2(e, baseline));
|
|
1124
|
+
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;
|
|
1125
|
+
return {
|
|
1126
|
+
hasChanges,
|
|
1127
|
+
summary: {
|
|
1128
|
+
functionsAdded: addedFunctions.length,
|
|
1129
|
+
functionsRemoved: removedFunctions.length,
|
|
1130
|
+
modulesAdded: addedModules.length,
|
|
1131
|
+
modulesRemoved: removedModules.length,
|
|
1132
|
+
callEdgesAdded: addedCallEdges.length,
|
|
1133
|
+
callEdgesRemoved: removedCallEdges.length,
|
|
1134
|
+
dataflowEdgesAdded: addedDataflowEdges.length,
|
|
1135
|
+
dataflowEdgesRemoved: removedDataflowEdges.length
|
|
1136
|
+
},
|
|
1137
|
+
details: {
|
|
1138
|
+
addedFunctions,
|
|
1139
|
+
removedFunctions,
|
|
1140
|
+
addedModules,
|
|
1141
|
+
removedModules,
|
|
1142
|
+
addedCallEdges,
|
|
1143
|
+
removedCallEdges,
|
|
1144
|
+
addedDataflowEdges,
|
|
1145
|
+
removedDataflowEdges
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function formatEdge2(edge, artifact) {
|
|
1150
|
+
const fromFn = artifact.nodes.functions.find((fn) => fn.id === edge.from);
|
|
1151
|
+
const toFn = artifact.nodes.functions.find((fn) => fn.id === edge.to);
|
|
1152
|
+
return `${fromFn?.name || edge.from} \u2192 ${toFn?.name || edge.to}`;
|
|
1153
|
+
}
|
|
1154
|
+
function printTextDiff2(diff, baselinePath, currentPath, only) {
|
|
1155
|
+
log.title("Monarch Graph Diff");
|
|
1156
|
+
console.log(chalk2.dim(`Baseline: ${baselinePath}`));
|
|
1157
|
+
console.log(chalk2.dim(`Current: ${currentPath}`));
|
|
1158
|
+
console.log("");
|
|
1159
|
+
if (!diff.hasChanges) {
|
|
1160
|
+
log.success("No changes detected");
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
console.log(chalk2.bold("Summary:"));
|
|
1164
|
+
if (!only || only === "functions") {
|
|
1165
|
+
console.log(` Functions: ${formatDiff(diff.summary.functionsAdded, diff.summary.functionsRemoved)}`);
|
|
1166
|
+
}
|
|
1167
|
+
if (!only || only === "modules") {
|
|
1168
|
+
console.log(` Modules: ${formatDiff(diff.summary.modulesAdded, diff.summary.modulesRemoved)}`);
|
|
1169
|
+
}
|
|
1170
|
+
if (!only || only === "edges") {
|
|
1171
|
+
console.log(` Call Edges: ${formatDiff(diff.summary.callEdgesAdded, diff.summary.callEdgesRemoved)}`);
|
|
1172
|
+
console.log(` Dataflow Edges: ${formatDiff(diff.summary.dataflowEdgesAdded, diff.summary.dataflowEdgesRemoved)}`);
|
|
1173
|
+
}
|
|
1174
|
+
console.log("");
|
|
1175
|
+
if ((!only || only === "functions") && diff.details.addedFunctions.length > 0) {
|
|
1176
|
+
console.log(chalk2.green.bold("Added Functions:"));
|
|
1177
|
+
diff.details.addedFunctions.forEach((fn) => console.log(chalk2.green(` + ${fn}`)));
|
|
1178
|
+
console.log("");
|
|
1179
|
+
}
|
|
1180
|
+
if ((!only || only === "functions") && diff.details.removedFunctions.length > 0) {
|
|
1181
|
+
console.log(chalk2.red.bold("Removed Functions:"));
|
|
1182
|
+
diff.details.removedFunctions.forEach((fn) => console.log(chalk2.red(` - ${fn}`)));
|
|
1183
|
+
console.log("");
|
|
1184
|
+
}
|
|
1185
|
+
if ((!only || only === "modules") && diff.details.addedModules.length > 0) {
|
|
1186
|
+
console.log(chalk2.green.bold("Added Modules:"));
|
|
1187
|
+
diff.details.addedModules.forEach((m) => console.log(chalk2.green(` + ${m}`)));
|
|
1188
|
+
console.log("");
|
|
1189
|
+
}
|
|
1190
|
+
if ((!only || only === "modules") && diff.details.removedModules.length > 0) {
|
|
1191
|
+
console.log(chalk2.red.bold("Removed Modules:"));
|
|
1192
|
+
diff.details.removedModules.forEach((m) => console.log(chalk2.red(` - ${m}`)));
|
|
1193
|
+
console.log("");
|
|
1194
|
+
}
|
|
1195
|
+
if ((!only || only === "edges") && diff.details.addedCallEdges.length > 0) {
|
|
1196
|
+
console.log(chalk2.green.bold("Added Call Edges:"));
|
|
1197
|
+
diff.details.addedCallEdges.forEach((e) => console.log(chalk2.green(` + ${e}`)));
|
|
1198
|
+
console.log("");
|
|
1199
|
+
}
|
|
1200
|
+
if ((!only || only === "edges") && diff.details.removedCallEdges.length > 0) {
|
|
1201
|
+
console.log(chalk2.red.bold("Removed Call Edges:"));
|
|
1202
|
+
diff.details.removedCallEdges.forEach((e) => console.log(chalk2.red(` - ${e}`)));
|
|
1203
|
+
console.log("");
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
function printMarkdownDiff(diff, baselinePath, currentPath, only) {
|
|
1207
|
+
console.log("# Monarch Graph Diff\n");
|
|
1208
|
+
console.log(`- **Baseline:** \`${baselinePath}\``);
|
|
1209
|
+
console.log(`- **Current:** \`${currentPath}\`
|
|
1210
|
+
`);
|
|
1211
|
+
if (!diff.hasChanges) {
|
|
1212
|
+
console.log("\u2705 No changes detected\n");
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
console.log("## Summary\n");
|
|
1216
|
+
console.log("| Category | Added | Removed |");
|
|
1217
|
+
console.log("|----------|-------|---------|");
|
|
1218
|
+
if (!only || only === "functions") {
|
|
1219
|
+
console.log(`| Functions | ${diff.summary.functionsAdded} | ${diff.summary.functionsRemoved} |`);
|
|
1220
|
+
}
|
|
1221
|
+
if (!only || only === "modules") {
|
|
1222
|
+
console.log(`| Modules | ${diff.summary.modulesAdded} | ${diff.summary.modulesRemoved} |`);
|
|
1223
|
+
}
|
|
1224
|
+
if (!only || only === "edges") {
|
|
1225
|
+
console.log(`| Call Edges | ${diff.summary.callEdgesAdded} | ${diff.summary.callEdgesRemoved} |`);
|
|
1226
|
+
console.log(`| Dataflow Edges | ${diff.summary.dataflowEdgesAdded} | ${diff.summary.dataflowEdgesRemoved} |`);
|
|
1227
|
+
}
|
|
1228
|
+
console.log("");
|
|
1229
|
+
if ((!only || only === "functions") && diff.details.addedFunctions.length > 0) {
|
|
1230
|
+
console.log("### Added Functions\n");
|
|
1231
|
+
diff.details.addedFunctions.forEach((fn) => console.log(`- \`${fn}\``));
|
|
1232
|
+
console.log("");
|
|
1233
|
+
}
|
|
1234
|
+
if ((!only || only === "functions") && diff.details.removedFunctions.length > 0) {
|
|
1235
|
+
console.log("### Removed Functions\n");
|
|
1236
|
+
diff.details.removedFunctions.forEach((fn) => console.log(`- \`${fn}\``));
|
|
1237
|
+
console.log("");
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// src/commands/serve.ts
|
|
1242
|
+
init_dist();
|
|
1243
|
+
import fs9 from "fs";
|
|
1244
|
+
import path9 from "path";
|
|
1245
|
+
import { createServer } from "http";
|
|
1246
|
+
async function serveCommand2(options) {
|
|
1247
|
+
const artifactPath = path9.resolve(options.artifact);
|
|
1248
|
+
const port = parseInt(options.port, 10);
|
|
1249
|
+
if (!fs9.existsSync(artifactPath)) {
|
|
1250
|
+
log.error(`Artifact not found: ${formatPath(artifactPath)}`);
|
|
1251
|
+
log.info("Run `monarch analyze` first to generate a graph");
|
|
1252
|
+
process.exit(1);
|
|
1253
|
+
}
|
|
1254
|
+
try {
|
|
1255
|
+
const content = fs9.readFileSync(artifactPath, "utf-8");
|
|
1256
|
+
const artifact = JSON.parse(content);
|
|
1257
|
+
if (!isCodeFlowArtifact(artifact)) {
|
|
1258
|
+
log.error("Invalid artifact file");
|
|
1259
|
+
process.exit(1);
|
|
1260
|
+
}
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
log.error(`Failed to read artifact: ${error}`);
|
|
1263
|
+
process.exit(1);
|
|
1264
|
+
}
|
|
1265
|
+
const server = createServer((req, res) => {
|
|
1266
|
+
const url = req.url || "/";
|
|
1267
|
+
if (url === "/api/artifact") {
|
|
1268
|
+
res.setHeader("Content-Type", "application/json");
|
|
1269
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1270
|
+
const content = fs9.readFileSync(artifactPath, "utf-8");
|
|
1271
|
+
res.end(content);
|
|
1272
|
+
} else if (url === "/") {
|
|
1273
|
+
res.setHeader("Content-Type", "text/html");
|
|
1274
|
+
res.end(getIndexHtml(port));
|
|
1275
|
+
} else {
|
|
1276
|
+
res.statusCode = 404;
|
|
1277
|
+
res.end("Not Found");
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
server.listen(port, () => {
|
|
1281
|
+
log.title("Monarch Visualization Server");
|
|
1282
|
+
log.success(`Server running at http://localhost:${port}`);
|
|
1283
|
+
log.info(`Serving artifact: ${formatPath(artifactPath)}`);
|
|
1284
|
+
log.dim("\nPress Ctrl+C to stop");
|
|
1285
|
+
if (options.open) {
|
|
1286
|
+
const url = `http://localhost:${port}`;
|
|
1287
|
+
import("child_process").then(({ exec }) => {
|
|
1288
|
+
const cmd = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
1289
|
+
exec(`${cmd} ${url}`);
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1293
|
+
process.on("SIGINT", () => {
|
|
1294
|
+
log.info("\nShutting down server...");
|
|
1295
|
+
server.close(() => {
|
|
1296
|
+
process.exit(0);
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
function getIndexHtml(port) {
|
|
1301
|
+
return `<!DOCTYPE html>
|
|
1302
|
+
<html lang="en">
|
|
1303
|
+
<head>
|
|
1304
|
+
<meta charset="UTF-8">
|
|
1305
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1306
|
+
<title>Monarch - Graph Visualization</title>
|
|
1307
|
+
<style>
|
|
1308
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1309
|
+
body {
|
|
1310
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1311
|
+
background: #16213e;
|
|
1312
|
+
color: #fff;
|
|
1313
|
+
min-height: 100vh;
|
|
1314
|
+
display: flex;
|
|
1315
|
+
align-items: center;
|
|
1316
|
+
justify-content: center;
|
|
1317
|
+
}
|
|
1318
|
+
.container {
|
|
1319
|
+
text-align: center;
|
|
1320
|
+
padding: 2rem;
|
|
1321
|
+
}
|
|
1322
|
+
h1 {
|
|
1323
|
+
font-size: 2.5rem;
|
|
1324
|
+
margin-bottom: 1rem;
|
|
1325
|
+
color: #4a90d9;
|
|
1326
|
+
}
|
|
1327
|
+
p {
|
|
1328
|
+
color: #888;
|
|
1329
|
+
margin-bottom: 2rem;
|
|
1330
|
+
}
|
|
1331
|
+
.info {
|
|
1332
|
+
background: #1a1a2e;
|
|
1333
|
+
padding: 1.5rem;
|
|
1334
|
+
border-radius: 8px;
|
|
1335
|
+
margin-bottom: 2rem;
|
|
1336
|
+
}
|
|
1337
|
+
.info code {
|
|
1338
|
+
background: #2d2d44;
|
|
1339
|
+
padding: 0.25rem 0.5rem;
|
|
1340
|
+
border-radius: 4px;
|
|
1341
|
+
font-family: monospace;
|
|
1342
|
+
}
|
|
1343
|
+
a {
|
|
1344
|
+
color: #4a90d9;
|
|
1345
|
+
text-decoration: none;
|
|
1346
|
+
}
|
|
1347
|
+
a:hover {
|
|
1348
|
+
text-decoration: underline;
|
|
1349
|
+
}
|
|
1350
|
+
</style>
|
|
1351
|
+
</head>
|
|
1352
|
+
<body>
|
|
1353
|
+
<div class="container">
|
|
1354
|
+
<h1>\u{1F52E} Monarch</h1>
|
|
1355
|
+
<p>Static Codebase Flow Mapping</p>
|
|
1356
|
+
<div class="info">
|
|
1357
|
+
<p>API Endpoint: <code>http://localhost:${port}/api/artifact</code></p>
|
|
1358
|
+
</div>
|
|
1359
|
+
<p>
|
|
1360
|
+
To view the full visualization, run the UI package:<br>
|
|
1361
|
+
<code>pnpm --filter @codeflow/ui dev</code>
|
|
1362
|
+
</p>
|
|
1363
|
+
</div>
|
|
1364
|
+
</body>
|
|
1365
|
+
</html>`;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/commands/init.ts
|
|
1369
|
+
import fs10 from "fs";
|
|
1370
|
+
import path10 from "path";
|
|
1371
|
+
var DEFAULT_CONFIG = {
|
|
1372
|
+
version: "1.0",
|
|
1373
|
+
exporter: "mock",
|
|
1374
|
+
output: "monarch-graph.json",
|
|
1375
|
+
include: ["src/**/*.ts", "src/**/*.tsx"],
|
|
1376
|
+
exclude: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"]
|
|
1377
|
+
};
|
|
1378
|
+
async function initCommand(options) {
|
|
1379
|
+
const configPath = path10.resolve("monarch.json");
|
|
1380
|
+
if (fs10.existsSync(configPath) && !options.force) {
|
|
1381
|
+
log.error(`Configuration file already exists: ${formatPath(configPath)}`);
|
|
1382
|
+
log.info("Use --force to overwrite");
|
|
1383
|
+
process.exit(1);
|
|
1384
|
+
}
|
|
1385
|
+
fs10.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
1386
|
+
log.success(`Created configuration file: ${formatPath(configPath)}`);
|
|
1387
|
+
const gitignorePath = path10.resolve(".gitignore");
|
|
1388
|
+
const gitignoreEntry = "\n# Monarch\nmonarch-graph.json\n.monarch/\n";
|
|
1389
|
+
if (fs10.existsSync(gitignorePath)) {
|
|
1390
|
+
const content = fs10.readFileSync(gitignorePath, "utf-8");
|
|
1391
|
+
if (!content.includes("monarch-graph.json")) {
|
|
1392
|
+
log.info(`Consider adding to ${formatPath(".gitignore")}:`);
|
|
1393
|
+
log.dim(gitignoreEntry.trim());
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
log.title("Monarch initialized successfully!");
|
|
1397
|
+
log.info("Next steps:");
|
|
1398
|
+
log.dim(" 1. Run `monarch analyze` to generate your first graph");
|
|
1399
|
+
log.dim(" 2. Run `monarch serve` to visualize the graph");
|
|
1400
|
+
log.dim(" 3. Run `monarch diff <old> <new>` to compare graphs");
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// src/commands/list.ts
|
|
1404
|
+
init_dist();
|
|
1405
|
+
import fs11 from "fs";
|
|
1406
|
+
import path11 from "path";
|
|
1407
|
+
import chalk3 from "chalk";
|
|
1408
|
+
async function listCommand(artifactPath, options) {
|
|
1409
|
+
const absolutePath = path11.resolve(artifactPath);
|
|
1410
|
+
if (!fs11.existsSync(absolutePath)) {
|
|
1411
|
+
log.error(`Artifact not found: ${formatPath(absolutePath)}`);
|
|
1412
|
+
log.info("Run `monarch analyze` first to generate a graph");
|
|
1413
|
+
process.exit(1);
|
|
1414
|
+
}
|
|
1415
|
+
let artifact;
|
|
1416
|
+
try {
|
|
1417
|
+
const content = fs11.readFileSync(absolutePath, "utf-8");
|
|
1418
|
+
artifact = JSON.parse(content);
|
|
1419
|
+
if (!isCodeFlowArtifact(artifact)) {
|
|
1420
|
+
log.error("Invalid artifact file");
|
|
1421
|
+
process.exit(1);
|
|
1422
|
+
}
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
log.error(`Failed to read artifact: ${error}`);
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
}
|
|
1427
|
+
if (options.json) {
|
|
1428
|
+
printJsonOutput(artifact, options.type);
|
|
1429
|
+
} else {
|
|
1430
|
+
printTextOutput(artifact, options.type);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
function printJsonOutput(artifact, type) {
|
|
1434
|
+
let output;
|
|
1435
|
+
switch (type) {
|
|
1436
|
+
case "functions":
|
|
1437
|
+
output = artifact.nodes.functions;
|
|
1438
|
+
break;
|
|
1439
|
+
case "modules":
|
|
1440
|
+
output = artifact.nodes.modules;
|
|
1441
|
+
break;
|
|
1442
|
+
case "components":
|
|
1443
|
+
output = artifact.nodes.components;
|
|
1444
|
+
break;
|
|
1445
|
+
case "edges":
|
|
1446
|
+
output = {
|
|
1447
|
+
calls: artifact.edges.calls,
|
|
1448
|
+
dataflow: artifact.edges.dataflow
|
|
1449
|
+
};
|
|
1450
|
+
break;
|
|
1451
|
+
default:
|
|
1452
|
+
output = {
|
|
1453
|
+
metadata: artifact.metadata,
|
|
1454
|
+
summary: {
|
|
1455
|
+
functions: artifact.nodes.functions.length,
|
|
1456
|
+
modules: artifact.nodes.modules.length,
|
|
1457
|
+
components: artifact.nodes.components.length,
|
|
1458
|
+
callEdges: artifact.edges.calls.length,
|
|
1459
|
+
dataflowEdges: artifact.edges.dataflow.length
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1464
|
+
}
|
|
1465
|
+
function printTextOutput(artifact, type) {
|
|
1466
|
+
log.title("Monarch Graph Contents");
|
|
1467
|
+
console.log(chalk3.bold("Metadata:"));
|
|
1468
|
+
console.log(` Version: ${artifact.metadata.analyzerVersion}`);
|
|
1469
|
+
console.log(` Language: ${artifact.metadata.language}`);
|
|
1470
|
+
console.log(` Commit: ${artifact.metadata.repoCommit}`);
|
|
1471
|
+
console.log(` Generated: ${artifact.metadata.generatedAt}`);
|
|
1472
|
+
console.log("");
|
|
1473
|
+
console.log(chalk3.bold("Summary:"));
|
|
1474
|
+
console.log(` Functions: ${formatNumber(artifact.nodes.functions.length)}`);
|
|
1475
|
+
console.log(` Modules: ${formatNumber(artifact.nodes.modules.length)}`);
|
|
1476
|
+
console.log(` Components: ${formatNumber(artifact.nodes.components.length)}`);
|
|
1477
|
+
console.log(` Call Edges: ${formatNumber(artifact.edges.calls.length)}`);
|
|
1478
|
+
console.log(` Dataflow Edges: ${formatNumber(artifact.edges.dataflow.length)}`);
|
|
1479
|
+
console.log("");
|
|
1480
|
+
if (!type || type === "functions") {
|
|
1481
|
+
console.log(chalk3.bold("Functions:"));
|
|
1482
|
+
const grouped = groupByFile(artifact.nodes.functions);
|
|
1483
|
+
for (const [file, fns] of Object.entries(grouped)) {
|
|
1484
|
+
console.log(chalk3.cyan(` ${file}`));
|
|
1485
|
+
for (const fn of fns) {
|
|
1486
|
+
console.log(` ${fn.name} ${chalk3.dim(`(${fn.startLine}-${fn.endLine})`)}`);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
console.log("");
|
|
1490
|
+
}
|
|
1491
|
+
if (!type || type === "modules") {
|
|
1492
|
+
console.log(chalk3.bold("Modules:"));
|
|
1493
|
+
for (const mod of artifact.nodes.modules) {
|
|
1494
|
+
const fnCount = mod.functionIds?.length || 0;
|
|
1495
|
+
console.log(` ${mod.path} ${chalk3.dim(`(${fnCount} functions)`)}`);
|
|
1496
|
+
}
|
|
1497
|
+
console.log("");
|
|
1498
|
+
}
|
|
1499
|
+
if (!type || type === "components") {
|
|
1500
|
+
console.log(chalk3.bold("Components:"));
|
|
1501
|
+
for (const comp of artifact.nodes.components) {
|
|
1502
|
+
const modCount = comp.moduleIds?.length || 0;
|
|
1503
|
+
console.log(` ${comp.name} ${chalk3.dim(`(${modCount} modules)`)}`);
|
|
1504
|
+
}
|
|
1505
|
+
console.log("");
|
|
1506
|
+
}
|
|
1507
|
+
if (!type || type === "edges") {
|
|
1508
|
+
if (artifact.edges.calls.length > 0) {
|
|
1509
|
+
console.log(chalk3.bold("Call Edges:"));
|
|
1510
|
+
for (const edge of artifact.edges.calls.slice(0, 10)) {
|
|
1511
|
+
const from = artifact.nodes.functions.find((f) => f.id === edge.from);
|
|
1512
|
+
const to = artifact.nodes.functions.find((f) => f.id === edge.to);
|
|
1513
|
+
console.log(` ${from?.name || edge.from} ${chalk3.dim("\u2192")} ${to?.name || edge.to}`);
|
|
1514
|
+
}
|
|
1515
|
+
if (artifact.edges.calls.length > 10) {
|
|
1516
|
+
console.log(chalk3.dim(` ... and ${artifact.edges.calls.length - 10} more`));
|
|
1517
|
+
}
|
|
1518
|
+
console.log("");
|
|
1519
|
+
}
|
|
1520
|
+
if (artifact.edges.dataflow.length > 0) {
|
|
1521
|
+
console.log(chalk3.bold("Dataflow Edges:"));
|
|
1522
|
+
for (const edge of artifact.edges.dataflow.slice(0, 10)) {
|
|
1523
|
+
const from = artifact.nodes.functions.find((f) => f.id === edge.from);
|
|
1524
|
+
const to = artifact.nodes.functions.find((f) => f.id === edge.to);
|
|
1525
|
+
console.log(` ${from?.name || edge.from} ${chalk3.yellow("\u27FF")} ${to?.name || edge.to}`);
|
|
1526
|
+
if (edge.label) {
|
|
1527
|
+
console.log(chalk3.dim(` ${edge.label}`));
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
if (artifact.edges.dataflow.length > 10) {
|
|
1531
|
+
console.log(chalk3.dim(` ... and ${artifact.edges.dataflow.length - 10} more`));
|
|
1532
|
+
}
|
|
1533
|
+
console.log("");
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
function groupByFile(items) {
|
|
1538
|
+
return items.reduce((acc, item) => {
|
|
1539
|
+
if (!acc[item.file]) {
|
|
1540
|
+
acc[item.file] = [];
|
|
1541
|
+
}
|
|
1542
|
+
acc[item.file].push(item);
|
|
1543
|
+
return acc;
|
|
1544
|
+
}, {});
|
|
1545
|
+
}
|
|
1546
|
+
export {
|
|
1547
|
+
analyzeCommand2 as analyzeCommand,
|
|
1548
|
+
diffCommand2 as diffCommand,
|
|
1549
|
+
initCommand,
|
|
1550
|
+
listCommand,
|
|
1551
|
+
serveCommand2 as serveCommand
|
|
1552
|
+
};
|