@mgamil/mapx 0.2.4
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/LICENSE +194 -0
- package/README.md +488 -0
- package/VERSION +1 -0
- package/dist/agents/generator.d.ts +74 -0
- package/dist/agents/generator.js +375 -0
- package/dist/agents/templates.d.ts +29 -0
- package/dist/agents/templates.js +459 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +1835 -0
- package/dist/core/cluster-engine.d.ts +32 -0
- package/dist/core/cluster-engine.js +314 -0
- package/dist/core/config.d.ts +29 -0
- package/dist/core/config.js +178 -0
- package/dist/core/context-builder.d.ts +61 -0
- package/dist/core/context-builder.js +252 -0
- package/dist/core/flow-tracer.d.ts +63 -0
- package/dist/core/flow-tracer.js +366 -0
- package/dist/core/git-tracker.d.ts +20 -0
- package/dist/core/git-tracker.js +159 -0
- package/dist/core/graph.d.ts +42 -0
- package/dist/core/graph.js +186 -0
- package/dist/core/metrics.d.ts +24 -0
- package/dist/core/metrics.js +87 -0
- package/dist/core/scanner.d.ts +53 -0
- package/dist/core/scanner.js +949 -0
- package/dist/core/store-bun.d.ts +13 -0
- package/dist/core/store-bun.js +34 -0
- package/dist/core/store-interface.d.ts +15 -0
- package/dist/core/store-interface.js +7 -0
- package/dist/core/store-node.d.ts +13 -0
- package/dist/core/store-node.js +35 -0
- package/dist/core/store.d.ts +132 -0
- package/dist/core/store.js +614 -0
- package/dist/core/workspace-manager.d.ts +9 -0
- package/dist/core/workspace-manager.js +64 -0
- package/dist/exporters/dot-exporter.d.ts +16 -0
- package/dist/exporters/dot-exporter.js +179 -0
- package/dist/exporters/graph-exporter.d.ts +14 -0
- package/dist/exporters/graph-exporter.js +85 -0
- package/dist/exporters/index.d.ts +9 -0
- package/dist/exporters/index.js +12 -0
- package/dist/exporters/llm-exporter.d.ts +18 -0
- package/dist/exporters/llm-exporter.js +224 -0
- package/dist/exporters/svg-exporter.d.ts +19 -0
- package/dist/exporters/svg-exporter.js +319 -0
- package/dist/exporters/toon-exporter.d.ts +16 -0
- package/dist/exporters/toon-exporter.js +246 -0
- package/dist/frameworks/detectors/aspnet.d.ts +11 -0
- package/dist/frameworks/detectors/aspnet.js +52 -0
- package/dist/frameworks/detectors/django.d.ts +14 -0
- package/dist/frameworks/detectors/django.js +135 -0
- package/dist/frameworks/detectors/drupal.d.ts +13 -0
- package/dist/frameworks/detectors/drupal.js +94 -0
- package/dist/frameworks/detectors/express.d.ts +12 -0
- package/dist/frameworks/detectors/express.js +234 -0
- package/dist/frameworks/detectors/fastapi.d.ts +12 -0
- package/dist/frameworks/detectors/fastapi.js +203 -0
- package/dist/frameworks/detectors/flask.d.ts +12 -0
- package/dist/frameworks/detectors/flask.js +244 -0
- package/dist/frameworks/detectors/go.d.ts +11 -0
- package/dist/frameworks/detectors/go.js +75 -0
- package/dist/frameworks/detectors/laravel.d.ts +11 -0
- package/dist/frameworks/detectors/laravel.js +462 -0
- package/dist/frameworks/detectors/nestjs.d.ts +12 -0
- package/dist/frameworks/detectors/nestjs.js +155 -0
- package/dist/frameworks/detectors/nextjs.d.ts +11 -0
- package/dist/frameworks/detectors/nextjs.js +118 -0
- package/dist/frameworks/detectors/rails.d.ts +12 -0
- package/dist/frameworks/detectors/rails.js +76 -0
- package/dist/frameworks/detectors/react-router.d.ts +11 -0
- package/dist/frameworks/detectors/react-router.js +115 -0
- package/dist/frameworks/detectors/rust.d.ts +11 -0
- package/dist/frameworks/detectors/rust.js +59 -0
- package/dist/frameworks/detectors/spring.d.ts +11 -0
- package/dist/frameworks/detectors/spring.js +56 -0
- package/dist/frameworks/detectors/sveltekit.d.ts +11 -0
- package/dist/frameworks/detectors/sveltekit.js +154 -0
- package/dist/frameworks/detectors/symfony.d.ts +13 -0
- package/dist/frameworks/detectors/symfony.js +175 -0
- package/dist/frameworks/detectors/tanstack-router.d.ts +12 -0
- package/dist/frameworks/detectors/tanstack-router.js +80 -0
- package/dist/frameworks/detectors/vapor.d.ts +11 -0
- package/dist/frameworks/detectors/vapor.js +52 -0
- package/dist/frameworks/detectors/vue-router.d.ts +12 -0
- package/dist/frameworks/detectors/vue-router.js +237 -0
- package/dist/frameworks/detectors/wordpress.d.ts +13 -0
- package/dist/frameworks/detectors/wordpress.js +141 -0
- package/dist/frameworks/detectors/yii.d.ts +11 -0
- package/dist/frameworks/detectors/yii.js +131 -0
- package/dist/frameworks/framework-registry.d.ts +13 -0
- package/dist/frameworks/framework-registry.js +77 -0
- package/dist/frameworks/route-registry.d.ts +26 -0
- package/dist/frameworks/route-registry.js +102 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +30 -0
- package/dist/languages/index.d.ts +2 -0
- package/dist/languages/index.js +7 -0
- package/dist/languages/installer.d.ts +13 -0
- package/dist/languages/installer.js +103 -0
- package/dist/languages/registry.d.ts +19 -0
- package/dist/languages/registry.js +427 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +20 -0
- package/dist/mcp.d.ts +11 -0
- package/dist/mcp.js +1699 -0
- package/dist/parsers/common-methods.d.ts +3 -0
- package/dist/parsers/common-methods.js +33 -0
- package/dist/parsers/fallback-parser.d.ts +10 -0
- package/dist/parsers/fallback-parser.js +18 -0
- package/dist/parsers/generic-wasm-parser.d.ts +23 -0
- package/dist/parsers/generic-wasm-parser.js +168 -0
- package/dist/parsers/ignored-symbols.d.ts +26 -0
- package/dist/parsers/ignored-symbols.js +77 -0
- package/dist/parsers/index.d.ts +9 -0
- package/dist/parsers/index.js +13 -0
- package/dist/parsers/languages/javascript.d.ts +11 -0
- package/dist/parsers/languages/javascript.js +28 -0
- package/dist/parsers/languages/php.d.ts +15 -0
- package/dist/parsers/languages/php.js +648 -0
- package/dist/parsers/languages/typescript.d.ts +10 -0
- package/dist/parsers/languages/typescript.js +9 -0
- package/dist/parsers/languages/vue.d.ts +13 -0
- package/dist/parsers/languages/vue.js +63 -0
- package/dist/parsers/parse-worker.d.ts +2 -0
- package/dist/parsers/parse-worker.js +185 -0
- package/dist/parsers/parser-interface.d.ts +9 -0
- package/dist/parsers/parser-interface.js +0 -0
- package/dist/parsers/parser-registry.d.ts +8 -0
- package/dist/parsers/parser-registry.js +52 -0
- package/dist/parsers/wasm-parser.d.ts +16 -0
- package/dist/parsers/wasm-parser.js +110 -0
- package/dist/types.d.ts +172 -0
- package/dist/types.js +0 -0
- package/dist/ui/index.html +270 -0
- package/dist/ui/main.js +581 -0
- package/dist/ui/main.js.map +7 -0
- package/dist/ui/styles.css +573 -0
- package/dist/ui-events.d.ts +36 -0
- package/dist/ui-events.js +61 -0
- package/dist/ui-server.d.ts +12 -0
- package/dist/ui-server.js +504 -0
- package/package.json +179 -0
- package/queries/bash/references.scm +22 -0
- package/queries/bash/symbols.scm +15 -0
- package/queries/c/references.scm +14 -0
- package/queries/c/symbols.scm +30 -0
- package/queries/c-sharp/references.scm +26 -0
- package/queries/c-sharp/symbols.scm +57 -0
- package/queries/cpp/references.scm +21 -0
- package/queries/cpp/symbols.scm +44 -0
- package/queries/dart/references.scm +33 -0
- package/queries/dart/symbols.scm +38 -0
- package/queries/elixir/references.scm +45 -0
- package/queries/elixir/symbols.scm +41 -0
- package/queries/go/references.scm +22 -0
- package/queries/go/symbols.scm +53 -0
- package/queries/java/references.scm +32 -0
- package/queries/java/symbols.scm +41 -0
- package/queries/javascript/references.scm +14 -0
- package/queries/javascript/symbols.scm +23 -0
- package/queries/kotlin/references.scm +31 -0
- package/queries/kotlin/symbols.scm +24 -0
- package/queries/lua/references.scm +19 -0
- package/queries/lua/symbols.scm +29 -0
- package/queries/pascal/references.scm +29 -0
- package/queries/pascal/symbols.scm +45 -0
- package/queries/php/references.scm +109 -0
- package/queries/php/symbols.scm +33 -0
- package/queries/python/references.scm +50 -0
- package/queries/python/symbols.scm +21 -0
- package/queries/ruby/references.scm +48 -0
- package/queries/ruby/symbols.scm +24 -0
- package/queries/rust/references.scm +31 -0
- package/queries/rust/symbols.scm +35 -0
- package/queries/scala/references.scm +30 -0
- package/queries/scala/symbols.scm +35 -0
- package/queries/svelte/references.scm +20 -0
- package/queries/svelte/symbols.scm +30 -0
- package/queries/swift/references.scm +22 -0
- package/queries/swift/symbols.scm +37 -0
- package/queries/typescript/references.scm +25 -0
- package/queries/typescript/symbols.scm +35 -0
- package/queries/vue/references.scm +20 -0
- package/queries/vue/symbols.scm +28 -0
- package/queries/zig/references.scm +20 -0
- package/queries/zig/symbols.scm +22 -0
- package/wasm/tree-sitter-c.wasm +0 -0
- package/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/wasm/tree-sitter-cpp.wasm +0 -0
- package/wasm/tree-sitter-dart.wasm +0 -0
- package/wasm/tree-sitter-go.wasm +0 -0
- package/wasm/tree-sitter-java.wasm +0 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-kotlin.wasm +0 -0
- package/wasm/tree-sitter-php.wasm +0 -0
- package/wasm/tree-sitter-python.wasm +0 -0
- package/wasm/tree-sitter-ruby.wasm +0 -0
- package/wasm/tree-sitter-rust.wasm +0 -0
- package/wasm/tree-sitter-scala.wasm +0 -0
- package/wasm/tree-sitter-swift.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
- package/wasm/tree-sitter-vue.wasm +0 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1835 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { resolve, join, dirname, relative, basename } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, readdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import * as readline from "node:readline";
|
|
7
|
+
import { Store } from "./core/store.js";
|
|
8
|
+
import { MapxGraph } from "./core/graph.js";
|
|
9
|
+
import { Scanner, buildMatcher } from "./core/scanner.js";
|
|
10
|
+
import { Config } from "./core/config.js";
|
|
11
|
+
import { FlowTracer } from "./core/flow-tracer.js";
|
|
12
|
+
import { AgentGenerator } from "./agents/generator.js";
|
|
13
|
+
import { WorkspaceManager } from "./core/workspace-manager.js";
|
|
14
|
+
import { LLMExporter } from "./exporters/llm-exporter.js";
|
|
15
|
+
import { GraphExporter } from "./exporters/graph-exporter.js";
|
|
16
|
+
import { DotExporter } from "./exporters/dot-exporter.js";
|
|
17
|
+
import { SvgExporter } from "./exporters/svg-exporter.js";
|
|
18
|
+
import { ToonExporter } from "./exporters/toon-exporter.js";
|
|
19
|
+
import { calculateMetrics } from "./core/metrics.js";
|
|
20
|
+
import { getChangedFiles, isGitRepo } from "./core/git-tracker.js";
|
|
21
|
+
import { getBuiltinLanguages } from "./languages/registry.js";
|
|
22
|
+
import { isLanguageInstalled, installLanguage, uninstallLanguage } from "./languages/installer.js";
|
|
23
|
+
import { RouteRegistry } from "./frameworks/route-registry.js";
|
|
24
|
+
const dynamicRequire = createRequire(import.meta.url);
|
|
25
|
+
function readVersion() {
|
|
26
|
+
const base = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
for (const candidate of [join(base, "VERSION"), join(base, "..", "VERSION")]) {
|
|
28
|
+
if (existsSync(candidate)) return readFileSync(candidate, "utf-8").trim();
|
|
29
|
+
}
|
|
30
|
+
return "0.1.0";
|
|
31
|
+
}
|
|
32
|
+
function collectPatterns(val, memo) {
|
|
33
|
+
return memo.concat(val.split(",").map((s) => s.trim()));
|
|
34
|
+
}
|
|
35
|
+
function resolveDir(cmdOpts, programOpts) {
|
|
36
|
+
const raw = cmdOpts.dir || programOpts.dir || process.cwd();
|
|
37
|
+
return resolve(raw);
|
|
38
|
+
}
|
|
39
|
+
const PHASE_LABELS = {
|
|
40
|
+
discover: { active: "Discovering files", done: "Discovered files" },
|
|
41
|
+
index: { active: "Indexing files", done: "Indexed files" },
|
|
42
|
+
parse: { active: "Parsing files", done: "Parsed files" },
|
|
43
|
+
resolve: { active: "Resolving references", done: "Resolved references" },
|
|
44
|
+
detect: { active: "Detecting changes", done: "Detected changes" },
|
|
45
|
+
cluster: { active: "Detecting clusters", done: "Detected clusters" }
|
|
46
|
+
};
|
|
47
|
+
const SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2E26", "\u28BC", "\u28F4", "\u28F7", "\u28EF", "\u28DF", "\u287F", "\u28BF", "\u28FB", "\u28FD", "\u28FE"];
|
|
48
|
+
let spinnerIdx = 0;
|
|
49
|
+
function createProgressRenderer() {
|
|
50
|
+
let lastPhase = null;
|
|
51
|
+
let lastLineLen = 0;
|
|
52
|
+
const writeLine = (line) => {
|
|
53
|
+
const clear = lastLineLen > 0 ? "\r" + " ".repeat(lastLineLen) + "\r" : "\r";
|
|
54
|
+
process.stderr.write(clear + line);
|
|
55
|
+
lastLineLen = line.length;
|
|
56
|
+
};
|
|
57
|
+
const renderBar = (current, total, width = 20) => {
|
|
58
|
+
if (total === 0) {
|
|
59
|
+
const frame = SPINNER_FRAMES[spinnerIdx++ % SPINNER_FRAMES.length];
|
|
60
|
+
return `${frame} `;
|
|
61
|
+
}
|
|
62
|
+
const filled = Math.min(width, Math.max(0, Math.round(current / total * width)));
|
|
63
|
+
const empty = width - filled;
|
|
64
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
65
|
+
const pct = Math.round(current / total * 100);
|
|
66
|
+
return `${bar} ${pct}%`;
|
|
67
|
+
};
|
|
68
|
+
return (progress) => {
|
|
69
|
+
const { phase, current, total, file } = progress;
|
|
70
|
+
const label = PHASE_LABELS[phase];
|
|
71
|
+
const isNewPhase = phase !== lastPhase;
|
|
72
|
+
if (isNewPhase && lastPhase !== null) {
|
|
73
|
+
const prevLabel = PHASE_LABELS[lastPhase];
|
|
74
|
+
writeLine(` \u2714 ${prevLabel.done}
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
lastPhase = phase;
|
|
78
|
+
const bar = renderBar(current, total);
|
|
79
|
+
const counter = total > 0 ? `${current}/${total}` : `${current}`;
|
|
80
|
+
let line = ` ${label.active} ${bar} ${counter}`;
|
|
81
|
+
if (file) {
|
|
82
|
+
const maxFileLen = Math.max(0, 60 - line.length);
|
|
83
|
+
const displayFile = file.length > maxFileLen && maxFileLen > 3 ? "\u2026" + file.slice(-(maxFileLen - 1)) : file.length <= maxFileLen ? file : "";
|
|
84
|
+
if (displayFile) {
|
|
85
|
+
line += ` ${displayFile}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
writeLine(line);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const MAPX_MARKER_START = "<!-- mapx -->";
|
|
92
|
+
const MAPX_MARKER_END = "<!-- /mapx -->";
|
|
93
|
+
function readStubContent() {
|
|
94
|
+
try {
|
|
95
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
96
|
+
const stubPath = resolve(thisDir, "agents.stub.md");
|
|
97
|
+
return readFileSync(stubPath, "utf-8");
|
|
98
|
+
} catch {
|
|
99
|
+
return [
|
|
100
|
+
"# MapxGraph - LLM Integration Guide",
|
|
101
|
+
"",
|
|
102
|
+
"This project uses **MapxGraph** \u2014 a local code graph memory system that provides persistent, structured understanding of the codebase across LLM sessions.",
|
|
103
|
+
"",
|
|
104
|
+
"## Commands",
|
|
105
|
+
"",
|
|
106
|
+
"All commands accept a target directory. Three ways to specify:",
|
|
107
|
+
"",
|
|
108
|
+
"```bash",
|
|
109
|
+
"# 1. Positional path argument",
|
|
110
|
+
"mapx scan /path/to/project",
|
|
111
|
+
"",
|
|
112
|
+
"# 2. --dir / -d flag",
|
|
113
|
+
"mapx scan --dir /path/to/project",
|
|
114
|
+
'mapx query "MyClass" -d /path/to/project',
|
|
115
|
+
"",
|
|
116
|
+
"# 3. Global flag (works with any subcommand)",
|
|
117
|
+
"mapx -d /path/to/project scan",
|
|
118
|
+
"```",
|
|
119
|
+
"",
|
|
120
|
+
"```bash",
|
|
121
|
+
"mapx init [/path] # First-time setup",
|
|
122
|
+
"mapx uninit [/path] # Reverse installation",
|
|
123
|
+
"mapx scan [/path] # Full scan (survives Ctrl+C)",
|
|
124
|
+
"mapx update [/path] # Incremental update",
|
|
125
|
+
"mapx export [--dir /path] # LLM summary (8K tokens)",
|
|
126
|
+
"mapx export --format=json # Full JSON graph",
|
|
127
|
+
"mapx export --format=dot # GraphViz DOT",
|
|
128
|
+
"mapx export --format=svg # SVG visualization",
|
|
129
|
+
"mapx export -o summary.txt # Export to file",
|
|
130
|
+
"mapx export --format=svg -o graph.svg # SVG to file",
|
|
131
|
+
"mapx query <term> # Search symbols",
|
|
132
|
+
"mapx deps <file> # File dependencies",
|
|
133
|
+
"mapx summary [/path] # Project summary",
|
|
134
|
+
"mapx serve --dir /path # Start MCP server (stdio)",
|
|
135
|
+
"mapx serve --sse --port 3456 --dir /path # SSE (HTTP) transport",
|
|
136
|
+
"```",
|
|
137
|
+
"",
|
|
138
|
+
"## MCP Tools",
|
|
139
|
+
"",
|
|
140
|
+
"- `mapx_scan` \u2014 Scan/update the code graph",
|
|
141
|
+
"- `mapx_query` \u2014 Search symbols by name",
|
|
142
|
+
"- `mapx_dependencies` \u2014 Get deps for a file",
|
|
143
|
+
"- `mapx_export` \u2014 Export graph (llm, json, dot, svg)",
|
|
144
|
+
"- `mapx_status` \u2014 Check scan status",
|
|
145
|
+
"",
|
|
146
|
+
"## When to Use",
|
|
147
|
+
"",
|
|
148
|
+
"1. Start of session: `mapx export`",
|
|
149
|
+
"2. Find something: `mapx query <term>`",
|
|
150
|
+
"3. Understand a file: `mapx deps <file>`",
|
|
151
|
+
"4. Files changed: `mapx update`",
|
|
152
|
+
"5. Major changes: `mapx scan`",
|
|
153
|
+
"6. Visual overview: `mapx export --format=svg -o graph.svg`",
|
|
154
|
+
"",
|
|
155
|
+
"## Supported Languages",
|
|
156
|
+
"",
|
|
157
|
+
"- **PHP**: classes, methods, functions, interfaces, traits, enums, constants",
|
|
158
|
+
"- **JavaScript**: classes, methods, functions, arrow functions",
|
|
159
|
+
"- **TypeScript**: classes, methods, functions, interfaces, enums, type aliases, properties"
|
|
160
|
+
].join("\n");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function generateAgentsBlock() {
|
|
164
|
+
const content = readStubContent();
|
|
165
|
+
return `${MAPX_MARKER_START}
|
|
166
|
+
${content}
|
|
167
|
+
${MAPX_MARKER_END}`;
|
|
168
|
+
}
|
|
169
|
+
function hasMarkers(content) {
|
|
170
|
+
return content.includes(MAPX_MARKER_START) && content.includes(MAPX_MARKER_END);
|
|
171
|
+
}
|
|
172
|
+
function replaceBetweenMarkers(existing, block) {
|
|
173
|
+
const startIdx = existing.indexOf(MAPX_MARKER_START);
|
|
174
|
+
const endIdx = existing.indexOf(MAPX_MARKER_END);
|
|
175
|
+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
|
|
176
|
+
return existing;
|
|
177
|
+
}
|
|
178
|
+
return existing.slice(0, startIdx) + block + existing.slice(endIdx + MAPX_MARKER_END.length);
|
|
179
|
+
}
|
|
180
|
+
function prompt(question, options) {
|
|
181
|
+
return new Promise((res) => {
|
|
182
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
183
|
+
const labels = options.map((o, i) => ` ${i + 1}) ${o}`);
|
|
184
|
+
process.stderr.write(question + "\n" + labels.join("\n") + "\n> ");
|
|
185
|
+
rl.question("", (answer) => {
|
|
186
|
+
rl.close();
|
|
187
|
+
const num = parseInt(answer.trim(), 10);
|
|
188
|
+
res(num >= 1 && num <= options.length ? num - 1 : options.length - 1);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function askQuestion(query) {
|
|
193
|
+
const rl = readline.createInterface({
|
|
194
|
+
input: process.stdin,
|
|
195
|
+
output: process.stdout
|
|
196
|
+
});
|
|
197
|
+
return new Promise((resolve2) => rl.question(query, (ans) => {
|
|
198
|
+
rl.close();
|
|
199
|
+
resolve2(ans);
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
async function selectProvidersInteractive() {
|
|
203
|
+
const generator = new AgentGenerator();
|
|
204
|
+
const providers = generator.listProviders();
|
|
205
|
+
console.log("\nWhich LLM/agent tools do you use in this project?");
|
|
206
|
+
console.log('Enter numbers separated by commas (e.g. 1,3), type "all" for all, or press Enter for default [1 (generic)]:');
|
|
207
|
+
providers.forEach((p, idx) => {
|
|
208
|
+
console.log(` [${idx + 1}] ${p}`);
|
|
209
|
+
});
|
|
210
|
+
const answer = await askQuestion("\nSelection: ");
|
|
211
|
+
const input = answer.trim().toLowerCase();
|
|
212
|
+
if (!input) {
|
|
213
|
+
return ["generic"];
|
|
214
|
+
}
|
|
215
|
+
if (input === "all") {
|
|
216
|
+
return providers;
|
|
217
|
+
}
|
|
218
|
+
const parts = input.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n) && n >= 1 && n <= providers.length);
|
|
219
|
+
const selected = parts.map((n) => providers[n - 1]);
|
|
220
|
+
return selected.length === 0 ? ["generic"] : selected;
|
|
221
|
+
}
|
|
222
|
+
function buildCLI() {
|
|
223
|
+
const program = new Command();
|
|
224
|
+
program.name("mapx").description("Multi-language code graph memory system for LLMs").version(readVersion()).option("-d, --dir <path>", "Target project directory (default: current directory)");
|
|
225
|
+
function detectLaravel(workspaceRoot) {
|
|
226
|
+
const composerPath = join(workspaceRoot, "composer.json");
|
|
227
|
+
if (existsSync(composerPath)) {
|
|
228
|
+
try {
|
|
229
|
+
const content = readFileSync(composerPath, "utf-8");
|
|
230
|
+
const composer = JSON.parse(content);
|
|
231
|
+
if (composer.require && composer.require["laravel/framework"]) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (existsSync(join(workspaceRoot, "artisan"))) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
if (existsSync(join(workspaceRoot, "app", "Http", "Kernel.php"))) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
if (existsSync(join(workspaceRoot, "app")) && existsSync(join(workspaceRoot, "routes")) && existsSync(join(workspaceRoot, "config")) && existsSync(join(workspaceRoot, "database"))) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
async function confirmLaravelExcludes(noSuggestions) {
|
|
249
|
+
if (noSuggestions) return false;
|
|
250
|
+
if (!process.stdin.isTTY) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
console.log("\nDetected Laravel project.");
|
|
254
|
+
console.log("\nSuggested exclusions (recommended):");
|
|
255
|
+
console.log(" \u2713 database/migrations/** (schema DDL \u2014 no app logic)");
|
|
256
|
+
console.log(" \u2713 database/seeders/** (data fixtures)");
|
|
257
|
+
console.log(" \u2713 database/factories/** (test data)");
|
|
258
|
+
console.log(" \u2713 storage/** (runtime-generated)");
|
|
259
|
+
console.log(" \u2713 bootstrap/cache/** (artisan-generated cache)");
|
|
260
|
+
console.log(" \u2713 public/** (web assets)");
|
|
261
|
+
console.log(" \u2713 resources/views/** (Blade templates \u2014 not yet supported)");
|
|
262
|
+
console.log(" \u2713 **/*.blade.php (Blade files)");
|
|
263
|
+
const answer = await askQuestion("\nAdd these to .mapx/config.json? [Y/n] ");
|
|
264
|
+
return answer.toLowerCase() !== "n";
|
|
265
|
+
}
|
|
266
|
+
program.command("init").description("Initialize mapx for a project").argument("[path]", "Target directory").option("--name <name>", "Repository name").option("--no-agents", "Skip AGENTS.md creation").option("--no-suggestions", "Skip interactive framework suggestions").option("--no-mcp-configs", "Skip auto-generating MCP config files for detected agent tools").action(async (path, opts) => {
|
|
267
|
+
const dir = path ? resolve(path) : resolveDir(opts, program.opts());
|
|
268
|
+
const isLaravel = detectLaravel(dir);
|
|
269
|
+
let shouldAddLaravelExcludes = false;
|
|
270
|
+
if (isLaravel) {
|
|
271
|
+
shouldAddLaravelExcludes = await confirmLaravelExcludes(opts.suggestions === false);
|
|
272
|
+
}
|
|
273
|
+
const config = await Config.init(dir, opts.name, isLaravel, shouldAddLaravelExcludes);
|
|
274
|
+
if (opts.agents !== false) {
|
|
275
|
+
if (process.stdin.isTTY && opts.suggestions !== false) {
|
|
276
|
+
const selected = await selectProvidersInteractive();
|
|
277
|
+
console.log(`Generating integration files for: ${selected.join(", ")}...`);
|
|
278
|
+
const generator = new AgentGenerator();
|
|
279
|
+
const actions = generator.plan(selected, { dir });
|
|
280
|
+
for (const action of actions) {
|
|
281
|
+
generator.execute(action);
|
|
282
|
+
console.log(` \u2713 Generated ${action.filename} (${action.status})`);
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
const generator = new AgentGenerator();
|
|
286
|
+
const actions = generator.plan(["generic"], { dir });
|
|
287
|
+
for (const action of actions) {
|
|
288
|
+
generator.execute(action);
|
|
289
|
+
console.log(` \u2713 Generated ${action.filename} (${action.status})`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (opts.mcpConfigs !== false) {
|
|
294
|
+
const generator = new AgentGenerator();
|
|
295
|
+
const detected = generator.detectAgentTools(dir);
|
|
296
|
+
if (detected.length > 0) {
|
|
297
|
+
const mcpActions = generator.generateMcpConfigs(detected, { dir });
|
|
298
|
+
for (const action of mcpActions) {
|
|
299
|
+
if (action.status === "up_to_date") continue;
|
|
300
|
+
generator.executeMcpConfig(action);
|
|
301
|
+
const verb = action.status === "merge" ? "merged into" : action.status === "create" ? "created" : "updated";
|
|
302
|
+
console.log(` \u2713 MCP config ${verb} ${action.filename} (${action.tool})`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const gitignorePath = join(dir, ".gitignore");
|
|
307
|
+
const hasGitignore = existsSync(gitignorePath);
|
|
308
|
+
const isGit = isGitRepo(dir);
|
|
309
|
+
if (hasGitignore || isGit) {
|
|
310
|
+
const content = hasGitignore ? readFileSync(gitignorePath, "utf-8") : "";
|
|
311
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
312
|
+
if (!lines.includes(".mapx/") && !lines.includes(".mapx")) {
|
|
313
|
+
const entry = content.length > 0 && !content.endsWith("\n") ? "\n.mapx/\n" : ".mapx/\n";
|
|
314
|
+
writeFileSync(gitignorePath, content + entry);
|
|
315
|
+
console.log(` \u2713 Added .mapx/ to .gitignore`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
console.log(`Initialized mapx in ${dir}/.mapx/`);
|
|
319
|
+
console.log(`Repo: ${config.repo.name}`);
|
|
320
|
+
});
|
|
321
|
+
program.command("uninit").description("Remove .mapx/ directory and reverse project integration changes").argument("[path]", "Target directory").option("-f, --force", "Skip confirmation prompt").action(async (path, opts) => {
|
|
322
|
+
const dir = path ? resolve(path) : resolveDir(opts, program.opts());
|
|
323
|
+
const hasMapx = existsSync(join(dir, ".mapx"));
|
|
324
|
+
if (!opts.force && process.stdin.isTTY) {
|
|
325
|
+
const answer = await askQuestion(`Are you sure you want to remove .mapx/ and reverse all mapx integrations in ${dir}? [y/N] `);
|
|
326
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
327
|
+
console.log("Aborted.");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const generator = new AgentGenerator();
|
|
332
|
+
generator.revert({ dir });
|
|
333
|
+
generator.revertMcpConfigs({ dir });
|
|
334
|
+
const gitignorePath = join(dir, ".gitignore");
|
|
335
|
+
if (existsSync(gitignorePath)) {
|
|
336
|
+
try {
|
|
337
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
338
|
+
const lines = content.split("\n");
|
|
339
|
+
let removed = false;
|
|
340
|
+
const filteredLines = lines.filter((line) => {
|
|
341
|
+
const trimmed = line.trim();
|
|
342
|
+
if (trimmed === ".mapx" || trimmed === ".mapx/") {
|
|
343
|
+
removed = true;
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
return true;
|
|
347
|
+
});
|
|
348
|
+
if (removed) {
|
|
349
|
+
writeFileSync(gitignorePath, filteredLines.join("\n"), "utf-8");
|
|
350
|
+
console.log(` \u2713 Removed .mapx/ from .gitignore`);
|
|
351
|
+
}
|
|
352
|
+
} catch (err) {
|
|
353
|
+
console.error(` \u2717 Failed to update .gitignore: ${err.message}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (hasMapx) {
|
|
357
|
+
try {
|
|
358
|
+
rmSync(join(dir, ".mapx"), { recursive: true, force: true });
|
|
359
|
+
console.log(` \u2713 Removed .mapx/ directory`);
|
|
360
|
+
} catch (err) {
|
|
361
|
+
console.error(` \u2717 Failed to remove .mapx/ directory: ${err.message}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
console.log(`Successfully uninitialized mapx for project: ${dir}`);
|
|
365
|
+
});
|
|
366
|
+
program.command("scan").description("Full scan: parse all files, build graph").argument("[path]", "Target directory").option("--exclude <glob>", "Exclude glob pattern(s)", collectPatterns, []).option("--include <glob>", "Include glob pattern(s)", collectPatterns, []).option("--repo <name>", "Scan only a specific registered repository").option("--all", "Scan all registered repositories").option("--force", "Force re-parsing of all files (bypass cache)", false).action(async (path, opts) => {
|
|
367
|
+
const dir = path ? resolve(path) : resolveDir({}, program.opts());
|
|
368
|
+
const { config, store, graph } = await loadContext(dir);
|
|
369
|
+
const onProgress = createProgressRenderer();
|
|
370
|
+
const scanner = new Scanner(store, config, graph, onProgress, {
|
|
371
|
+
excludes: opts.exclude,
|
|
372
|
+
includes: opts.include
|
|
373
|
+
});
|
|
374
|
+
const onSigInt = () => {
|
|
375
|
+
scanner.abort();
|
|
376
|
+
process.stderr.write("\n");
|
|
377
|
+
};
|
|
378
|
+
process.once("SIGINT", onSigInt);
|
|
379
|
+
let repoNames = void 0;
|
|
380
|
+
if (opts.repo) {
|
|
381
|
+
repoNames = [opts.repo];
|
|
382
|
+
} else if (opts.all) {
|
|
383
|
+
repoNames = ["all"];
|
|
384
|
+
}
|
|
385
|
+
const result = await scanner.scanFull(repoNames, { force: !!opts.force }).catch((err) => {
|
|
386
|
+
if (err.message.includes("Another scan is already running")) {
|
|
387
|
+
console.error(`Error: ${err.message}`);
|
|
388
|
+
process.exit(1);
|
|
389
|
+
}
|
|
390
|
+
throw err;
|
|
391
|
+
});
|
|
392
|
+
process.removeListener("SIGINT", onSigInt);
|
|
393
|
+
process.stderr.write("\r" + " ".repeat(80) + "\r");
|
|
394
|
+
if (result.interrupted) {
|
|
395
|
+
console.log(`Scan interrupted after ${result.filesScanned}/${result.totalFiles} files. Progress saved \u2014 run \`scan\` again to resume.`);
|
|
396
|
+
} else {
|
|
397
|
+
console.log(`Scanned ${result.filesScanned} files in ${result.durationMs}ms`);
|
|
398
|
+
}
|
|
399
|
+
console.log(`Languages: ${Object.entries(result.languageBreakdown).map(([l, c]) => `${l}: ${c}`).join(", ")}`);
|
|
400
|
+
console.log(`Found ${result.symbolsFound} symbols, ${result.edgesFound} edges`);
|
|
401
|
+
});
|
|
402
|
+
program.command("update").alias("sync").description("Incremental scan: re-scan only changed files").argument("[path]", "Target directory").option("--exclude <glob>", "Exclude glob pattern(s)", collectPatterns, []).option("--include <glob>", "Include glob pattern(s)", collectPatterns, []).option("--repo <name>", "Update only a specific registered repository").option("--all", "Update all registered repositories").action(async (path, opts) => {
|
|
403
|
+
const dir = path ? resolve(path) : resolveDir({}, program.opts());
|
|
404
|
+
const { config, store, graph } = await loadContext(dir);
|
|
405
|
+
const onProgress = createProgressRenderer();
|
|
406
|
+
const handleLockError = (err) => {
|
|
407
|
+
if (err.message.includes("Another scan is already running")) {
|
|
408
|
+
console.error(`Error: ${err.message}`);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
throw err;
|
|
412
|
+
};
|
|
413
|
+
let repoNames = void 0;
|
|
414
|
+
if (opts.repo) {
|
|
415
|
+
repoNames = [opts.repo];
|
|
416
|
+
} else if (opts.all) {
|
|
417
|
+
repoNames = ["all"];
|
|
418
|
+
}
|
|
419
|
+
const scanner = new Scanner(store, config, graph, onProgress, {
|
|
420
|
+
excludes: opts.exclude,
|
|
421
|
+
includes: opts.include
|
|
422
|
+
});
|
|
423
|
+
process.once("SIGINT", () => scanner.abort());
|
|
424
|
+
const result = await scanner.scanIncremental(repoNames).catch(handleLockError);
|
|
425
|
+
process.stderr.write("\r" + " ".repeat(80) + "\r");
|
|
426
|
+
if (result.interrupted) {
|
|
427
|
+
console.log(`Update interrupted after ${result.filesScanned} files.`);
|
|
428
|
+
} else {
|
|
429
|
+
console.log(`Updated ${result.filesScanned} files in ${result.durationMs}ms`);
|
|
430
|
+
console.log(`${result.symbolsFound} symbols updated, ${result.edgesFound} edges updated`);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
program.command("status").description("Show scan status, collected metrics, and changed files").argument("[path]", "Target directory").option("--exclude <glob>", "Exclude glob pattern(s)", collectPatterns, []).option("--include <glob>", "Include glob pattern(s)", collectPatterns, []).action(async (path, opts) => {
|
|
434
|
+
const dir = path ? resolve(path) : resolveDir({}, program.opts());
|
|
435
|
+
const { config, store, graph } = await loadContext(dir);
|
|
436
|
+
const lastScan = store.getMeta("last_scan_time:" + config.repo.name) || store.getMeta("last_scan_time");
|
|
437
|
+
const lastCommit = store.getMeta("last_scan_commit:" + config.repo.name) || store.getMeta("last_scan_commit");
|
|
438
|
+
const schemaVer = store.getMeta("schema_version");
|
|
439
|
+
const dbPath = resolve(dir, ".mapx", "mapx.db");
|
|
440
|
+
const activeExcludes = [
|
|
441
|
+
...config.settings.excludePatterns ?? [],
|
|
442
|
+
...opts.exclude ?? []
|
|
443
|
+
];
|
|
444
|
+
const activeIncludes = [
|
|
445
|
+
...config.settings.includePatterns ?? [],
|
|
446
|
+
...opts.include ?? []
|
|
447
|
+
];
|
|
448
|
+
console.log("\n\u2500\u2500 Scan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
449
|
+
console.log(` Project: ${config.repo.name}`);
|
|
450
|
+
console.log(` Framework: ${config.repo.framework || "generic"}`);
|
|
451
|
+
console.log(` Directory: ${dir}`);
|
|
452
|
+
console.log(` Last scan: ${lastScan || "never"}`);
|
|
453
|
+
console.log(` Last commit: ${lastCommit || "none"}`);
|
|
454
|
+
console.log(` Schema: v${schemaVer || "?"}`);
|
|
455
|
+
console.log(` Excludes: [${activeExcludes.join(", ")}]`);
|
|
456
|
+
console.log(` Includes: [${activeIncludes.join(", ")}]`);
|
|
457
|
+
const fileCount = store.getFileCount();
|
|
458
|
+
const symbolCount = store.getSymbolCount();
|
|
459
|
+
const edgeCount = store.getEdgeCount();
|
|
460
|
+
const breakdown = store.getLanguageBreakdown();
|
|
461
|
+
const verifiedEdgeCount = store.raw.prepare("SELECT COUNT(*) as cnt FROM edges WHERE verifiability = 'verified'").get()?.cnt || 0;
|
|
462
|
+
const inferredEdgeCount = store.raw.prepare("SELECT COUNT(*) as cnt FROM edges WHERE verifiability = 'inferred'").get()?.cnt || 0;
|
|
463
|
+
console.log("\n\u2500\u2500 Collected data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
464
|
+
console.log(` Files: ${fileCount}`);
|
|
465
|
+
console.log(` Symbols: ${symbolCount}`);
|
|
466
|
+
console.log(` Edges: ${edgeCount} (verified: ${verifiedEdgeCount}, inferred: ${inferredEdgeCount})`);
|
|
467
|
+
const langs = Object.entries(breakdown).sort((a, b) => b[1] - a[1]);
|
|
468
|
+
if (langs.length > 0) {
|
|
469
|
+
console.log(` Languages:`);
|
|
470
|
+
for (const [lang, cnt] of langs) {
|
|
471
|
+
console.log(` ${lang.padEnd(14)} ${cnt} files`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const kindRows = store.raw.prepare(
|
|
475
|
+
"SELECT kind, COUNT(*) as cnt FROM symbols GROUP BY kind ORDER BY cnt DESC"
|
|
476
|
+
).all();
|
|
477
|
+
if (kindRows.length > 0) {
|
|
478
|
+
console.log(` Symbol kinds:`);
|
|
479
|
+
for (const row of kindRows) {
|
|
480
|
+
console.log(` ${row.kind.padEnd(14)} ${row.cnt}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const edgeTypeRows = store.raw.prepare(
|
|
484
|
+
"SELECT edge_type, COUNT(*) as cnt FROM edges GROUP BY edge_type ORDER BY cnt DESC"
|
|
485
|
+
).all();
|
|
486
|
+
if (edgeTypeRows.length > 0) {
|
|
487
|
+
console.log(` Edge types:`);
|
|
488
|
+
for (const row of edgeTypeRows) {
|
|
489
|
+
console.log(` ${row.edge_type.padEnd(14)} ${row.cnt}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (fileCount > 0) {
|
|
493
|
+
const densityNum = edgeCount / Math.max(fileCount * (fileCount - 1), 1);
|
|
494
|
+
const density = (densityNum * 100).toFixed(2);
|
|
495
|
+
const connRows = store.raw.prepare(`
|
|
496
|
+
SELECT source_file, COUNT(*) as cnt FROM edges
|
|
497
|
+
GROUP BY source_file ORDER BY cnt DESC LIMIT 5
|
|
498
|
+
`).all();
|
|
499
|
+
console.log("\n\u2500\u2500 Graph metrics \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
500
|
+
console.log(` Density: ${density}%`);
|
|
501
|
+
const avgEdges = fileCount > 0 ? (edgeCount / fileCount).toFixed(1) : "0";
|
|
502
|
+
console.log(` Avg edges/file: ${avgEdges}`);
|
|
503
|
+
if (connRows.length > 0) {
|
|
504
|
+
console.log(` Most connected files:`);
|
|
505
|
+
for (const row of connRows) {
|
|
506
|
+
const rel = row.source_file.replace(dir + "/", "");
|
|
507
|
+
console.log(` ${String(row.cnt).padStart(4)} edges ${rel}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
try {
|
|
512
|
+
const { statSync } = await import("node:fs");
|
|
513
|
+
const dbSize = statSync(dbPath).size;
|
|
514
|
+
const kb = (dbSize / 1024).toFixed(1);
|
|
515
|
+
console.log("\n\u2500\u2500 Storage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
516
|
+
console.log(` Database: ${kb} KB (${dbPath})`);
|
|
517
|
+
} catch {
|
|
518
|
+
}
|
|
519
|
+
console.log("\n\u2500\u2500 PageRank Importance \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
520
|
+
const topFiles = store.getTopFilesByPageRank(graph, 5);
|
|
521
|
+
const topSymbols = store.getTopSymbolsByPageRank(graph, 5);
|
|
522
|
+
if (topFiles.length > 0) {
|
|
523
|
+
console.log(" Top files by PageRank:");
|
|
524
|
+
for (const tf of topFiles) {
|
|
525
|
+
console.log(` ${tf.pagerank.toFixed(6)} ${tf.path}`);
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
console.log(" No ranked files (run a scan first)");
|
|
529
|
+
}
|
|
530
|
+
if (topSymbols.length > 0) {
|
|
531
|
+
console.log("\n Top symbols by PageRank:");
|
|
532
|
+
for (const ts of topSymbols) {
|
|
533
|
+
const scope = ts.scope ? `${ts.scope}::` : "";
|
|
534
|
+
console.log(` ${ts.pagerank.toFixed(6)} ${scope}${ts.name} (${ts.filePath})`);
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
console.log("\n No ranked symbols (run a scan first)");
|
|
538
|
+
}
|
|
539
|
+
const repoRoot = resolve(dir, config.repo.path);
|
|
540
|
+
let isStale = false;
|
|
541
|
+
if (!isGitRepo(repoRoot)) {
|
|
542
|
+
console.log("\n\u2500\u2500 Git \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
543
|
+
console.log(" Not a git repository");
|
|
544
|
+
} else {
|
|
545
|
+
const changes = getChangedFiles(repoRoot, lastCommit || void 0);
|
|
546
|
+
console.log("\n\u2500\u2500 Git changes since last scan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
547
|
+
if (changes.length === 0) {
|
|
548
|
+
console.log(" No changes since last scan (\u2713 index is current)");
|
|
549
|
+
} else {
|
|
550
|
+
isStale = true;
|
|
551
|
+
const byStatus = { added: 0, modified: 0, removed: 0, renamed: 0, unchanged: 0 };
|
|
552
|
+
for (const c of changes) byStatus[c.status] = (byStatus[c.status] || 0) + 1;
|
|
553
|
+
const summary = Object.entries(byStatus).filter(([, n]) => n > 0).map(([s, n]) => `${n} ${s}`).join(", ");
|
|
554
|
+
console.log(` ${changes.length} changed files (${summary}) (\u26A0 stale)`);
|
|
555
|
+
const icon = { added: "+", modified: "~", removed: "-", renamed: ">", unchanged: "=" };
|
|
556
|
+
for (const change of changes) {
|
|
557
|
+
console.log(` ${icon[change.status]} ${change.path}`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
console.log("\n\u2500\u2500 Recommendations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
562
|
+
if (isStale) {
|
|
563
|
+
console.log(" \u26A0 Index is stale. Run `mapx sync` or `mapx update` to bring it up to date.");
|
|
564
|
+
} else {
|
|
565
|
+
console.log(" \u2713 Index is up to date.");
|
|
566
|
+
}
|
|
567
|
+
console.log("");
|
|
568
|
+
});
|
|
569
|
+
program.command("query <term>").description("Search for symbols by name").option("-d, --dir <path>", "Target directory").action(async (term, opts) => {
|
|
570
|
+
const dir = resolveDir(opts, program.opts());
|
|
571
|
+
const { store } = await loadContext(dir);
|
|
572
|
+
const results = store.searchSymbols(term);
|
|
573
|
+
if (results.length === 0) {
|
|
574
|
+
console.log(`No symbols matching "${term}"`);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
for (const sym of results) {
|
|
578
|
+
const scope = sym.scope ? `${sym.scope}::` : "";
|
|
579
|
+
console.log(` ${sym.kind} ${scope}${sym.name}`);
|
|
580
|
+
console.log(` @ ${sym.file_path}:${sym.start_line}`);
|
|
581
|
+
if (sym.signature && sym.signature !== sym.name) {
|
|
582
|
+
console.log(` signature: ${sym.signature}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
program.command("search <term>").description("Symbol search with kind/file/exact filters").option("-d, --dir <path>", "Target directory").option("--kind <kind>", "Filter by symbol kind (e.g. class, method)").option("--file <prefix>", "Filter by file path prefix").option("--exact", "Only match exact name", false).option("--limit <limit>", "Max results to return", "20").action(async (term, opts) => {
|
|
587
|
+
const dir = resolveDir(opts, program.opts());
|
|
588
|
+
const { store, graph } = await loadContext(dir);
|
|
589
|
+
const results = store.searchSymbolsFiltered({
|
|
590
|
+
term,
|
|
591
|
+
kind: opts.kind,
|
|
592
|
+
filePrefix: opts.file,
|
|
593
|
+
exact: !!opts.exact,
|
|
594
|
+
limit: parseInt(opts.limit, 10)
|
|
595
|
+
});
|
|
596
|
+
if (results.length === 0) {
|
|
597
|
+
console.log(`No symbols matching "${term}"`);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const rankedAll = graph.getRankedSymbols();
|
|
601
|
+
const rankMap = /* @__PURE__ */ new Map();
|
|
602
|
+
for (const item of rankedAll) {
|
|
603
|
+
rankMap.set(`${item.filePath}::${item.name}`, item.pagerank);
|
|
604
|
+
}
|
|
605
|
+
for (const sym of results) {
|
|
606
|
+
const scope = sym.scope ? `${sym.scope}::` : "";
|
|
607
|
+
const key = `${sym.file_path}::${sym.name}`;
|
|
608
|
+
const pagerankVal = rankMap.get(key) || 0;
|
|
609
|
+
console.log(` ${sym.kind} ${scope}${sym.name} [pagerank: ${pagerankVal.toFixed(6)}]`);
|
|
610
|
+
console.log(` @ ${sym.file_path}:${sym.start_line}`);
|
|
611
|
+
if (sym.signature && sym.signature !== sym.name) {
|
|
612
|
+
console.log(` signature: ${sym.signature}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
program.command("callers <symbol>").description("Show callers of a symbol").option("-d, --dir <path>", "Target directory").option("--depth <depth>", "Traversal depth", "1").action(async (symbol, opts) => {
|
|
617
|
+
const dir = resolveDir(opts, program.opts());
|
|
618
|
+
const { store } = await loadContext(dir);
|
|
619
|
+
const maxDepth = parseInt(opts.depth, 10);
|
|
620
|
+
const queue = [{ symName: symbol, depth: 0 }];
|
|
621
|
+
const visited = /* @__PURE__ */ new Set([symbol]);
|
|
622
|
+
const results = [];
|
|
623
|
+
while (queue.length > 0) {
|
|
624
|
+
const { symName, depth } = queue.shift();
|
|
625
|
+
if (depth >= maxDepth) continue;
|
|
626
|
+
const callers = store.getCallersOfSymbol(symName);
|
|
627
|
+
for (const edge of callers) {
|
|
628
|
+
const callerName = edge.source_symbol ? `${edge.source_symbol}` : "<top-level>";
|
|
629
|
+
const calleeName = edge.target_symbol || symName;
|
|
630
|
+
const meta = edge.metadata ? JSON.parse(edge.metadata) : {};
|
|
631
|
+
results.push({
|
|
632
|
+
caller: callerName,
|
|
633
|
+
callee: calleeName,
|
|
634
|
+
file: edge.source_file,
|
|
635
|
+
line: meta.startLine || 1,
|
|
636
|
+
depth: depth + 1
|
|
637
|
+
});
|
|
638
|
+
const nextSym = edge.source_symbol;
|
|
639
|
+
if (nextSym && !visited.has(nextSym)) {
|
|
640
|
+
visited.add(nextSym);
|
|
641
|
+
queue.push({ symName: nextSym, depth: depth + 1 });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (results.length === 0) {
|
|
646
|
+
console.log(`No callers found for "${symbol}"`);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
console.log(`Callers of "${symbol}":`);
|
|
650
|
+
for (const res of results) {
|
|
651
|
+
const indent = " ".repeat(res.depth);
|
|
652
|
+
console.log(`${indent}\u2190 ${res.caller} (calls ${res.callee})`);
|
|
653
|
+
console.log(`${indent} @ ${res.file}:${res.line}`);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
program.command("callees <symbol>").description("Show callees of a symbol").option("-d, --dir <path>", "Target directory").option("--depth <depth>", "Traversal depth", "1").action(async (symbol, opts) => {
|
|
657
|
+
const dir = resolveDir(opts, program.opts());
|
|
658
|
+
const { store } = await loadContext(dir);
|
|
659
|
+
const maxDepth = parseInt(opts.depth, 10);
|
|
660
|
+
const queue = [{ symName: symbol, depth: 0 }];
|
|
661
|
+
const visited = /* @__PURE__ */ new Set([symbol]);
|
|
662
|
+
const results = [];
|
|
663
|
+
while (queue.length > 0) {
|
|
664
|
+
const { symName, depth } = queue.shift();
|
|
665
|
+
if (depth >= maxDepth) continue;
|
|
666
|
+
const callees = store.getCalleesOfSymbol(symName);
|
|
667
|
+
for (const edge of callees) {
|
|
668
|
+
const calleeName = edge.target_symbol || "<unknown>";
|
|
669
|
+
const callerName = edge.source_symbol || symName;
|
|
670
|
+
const meta = edge.metadata ? JSON.parse(edge.metadata) : {};
|
|
671
|
+
results.push({
|
|
672
|
+
caller: callerName,
|
|
673
|
+
callee: calleeName,
|
|
674
|
+
file: edge.target_file,
|
|
675
|
+
line: meta.startLine || 1,
|
|
676
|
+
depth: depth + 1
|
|
677
|
+
});
|
|
678
|
+
if (edge.target_symbol && !visited.has(edge.target_symbol)) {
|
|
679
|
+
visited.add(edge.target_symbol);
|
|
680
|
+
queue.push({ symName: edge.target_symbol, depth: depth + 1 });
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (results.length === 0) {
|
|
685
|
+
console.log(`No callees found for "${symbol}"`);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
console.log(`Callees of "${symbol}":`);
|
|
689
|
+
for (const res of results) {
|
|
690
|
+
const indent = " ".repeat(res.depth);
|
|
691
|
+
console.log(`${indent}\u2192 ${res.callee} (called by ${res.caller})`);
|
|
692
|
+
console.log(`${indent} @ ${res.file}:${res.line}`);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
program.command("impact <symbol>").description("Show transitive blast-radius of changing a symbol").option("-d, --dir <path>", "Target directory").option("--depth <depth>", "Traversal depth", "3").option("--format <format>", "text | json", "text").action(async (symbol, opts) => {
|
|
696
|
+
const dir = resolveDir(opts, program.opts());
|
|
697
|
+
const { store } = await loadContext(dir);
|
|
698
|
+
const maxDepth = parseInt(opts.depth, 10);
|
|
699
|
+
const queue = [{ symName: symbol, depth: 0 }];
|
|
700
|
+
const visited = /* @__PURE__ */ new Set([symbol]);
|
|
701
|
+
const items = [];
|
|
702
|
+
while (queue.length > 0) {
|
|
703
|
+
const { symName, depth } = queue.shift();
|
|
704
|
+
if (depth >= maxDepth) continue;
|
|
705
|
+
const callers = store.getCallersOfSymbol(symName);
|
|
706
|
+
for (const edge of callers) {
|
|
707
|
+
const callerName = edge.source_symbol || "<top-level>";
|
|
708
|
+
const key = `${edge.source_file}::${callerName}`;
|
|
709
|
+
if (visited.has(key)) continue;
|
|
710
|
+
visited.add(key);
|
|
711
|
+
let risk = "LOW";
|
|
712
|
+
const isStructural = ["import", "require", "extends", "implements"].includes(edge.edge_type);
|
|
713
|
+
const curDepth = depth + 1;
|
|
714
|
+
if (curDepth === 1) {
|
|
715
|
+
risk = isStructural ? "MEDIUM" : "HIGH";
|
|
716
|
+
} else if (curDepth === 2) {
|
|
717
|
+
risk = isStructural ? "LOW" : "MEDIUM";
|
|
718
|
+
} else {
|
|
719
|
+
risk = "LOW";
|
|
720
|
+
}
|
|
721
|
+
items.push({
|
|
722
|
+
symbol: callerName,
|
|
723
|
+
file: edge.source_file,
|
|
724
|
+
depth: curDepth,
|
|
725
|
+
edgeType: edge.edge_type,
|
|
726
|
+
risk
|
|
727
|
+
});
|
|
728
|
+
if (edge.source_symbol) {
|
|
729
|
+
queue.push({ symName: edge.source_symbol, depth: curDepth });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
let recommendation = "No callers found \u2014 safe to change";
|
|
734
|
+
if (items.some((x) => x.risk === "HIGH")) {
|
|
735
|
+
recommendation = "Treat as BREAKING CHANGE \u2014 update all HIGH-risk callers";
|
|
736
|
+
} else if (items.length > 0) {
|
|
737
|
+
recommendation = "Low blast radius \u2014 proceed with caution";
|
|
738
|
+
}
|
|
739
|
+
if (opts.format === "json") {
|
|
740
|
+
console.log(JSON.stringify({
|
|
741
|
+
affected: items,
|
|
742
|
+
summary: {
|
|
743
|
+
high: items.filter((x) => x.risk === "HIGH").length,
|
|
744
|
+
medium: items.filter((x) => x.risk === "MEDIUM").length,
|
|
745
|
+
low: items.filter((x) => x.risk === "LOW").length
|
|
746
|
+
},
|
|
747
|
+
recommendation
|
|
748
|
+
}, null, 2));
|
|
749
|
+
} else {
|
|
750
|
+
if (items.length === 0) {
|
|
751
|
+
console.log(`No callers affected by changing "${symbol}"`);
|
|
752
|
+
} else {
|
|
753
|
+
console.log(`Impact analysis for "${symbol}":`);
|
|
754
|
+
for (const item of items) {
|
|
755
|
+
console.log(` [${item.risk}] ${item.symbol} (${item.file}) [depth: ${item.depth}, type: ${item.edgeType}]`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
console.log(`
|
|
759
|
+
Recommendation: ${recommendation}`);
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
program.command("node <symbol>").description("Show full symbol details and optional source code").option("-d, --dir <path>", "Target directory").option("--source", "Extract and display source code", false).action(async (symbol, opts) => {
|
|
763
|
+
const dir = resolveDir(opts, program.opts());
|
|
764
|
+
const { store } = await loadContext(dir);
|
|
765
|
+
const { readFileSync: readFileSync2 } = await import("node:fs");
|
|
766
|
+
const sym = store.getSymbolByName(symbol);
|
|
767
|
+
if (!sym) {
|
|
768
|
+
console.error(`Error: Symbol "${symbol}" not found`);
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
const callers = store.getCallersOfSymbol(symbol);
|
|
772
|
+
const callees = store.getCalleesOfSymbol(symbol);
|
|
773
|
+
console.log(`Symbol: ${sym.scope ? `${sym.scope}::` : ""}${sym.name}`);
|
|
774
|
+
console.log(`Kind: ${sym.kind}`);
|
|
775
|
+
console.log(`File: ${sym.file_path}`);
|
|
776
|
+
console.log(`Lines: ${sym.start_line}-${sym.end_line}`);
|
|
777
|
+
console.log(`Signature: ${sym.signature}`);
|
|
778
|
+
console.log(`Callers: ${callers.length}`);
|
|
779
|
+
console.log(`Callees: ${callees.length}`);
|
|
780
|
+
if (opts.source) {
|
|
781
|
+
try {
|
|
782
|
+
const absolutePath = resolve(dir, sym.file_path);
|
|
783
|
+
const content = readFileSync2(absolutePath, "utf8");
|
|
784
|
+
const lines = content.split("\n");
|
|
785
|
+
const start = sym.start_line - 1;
|
|
786
|
+
const end = sym.end_line;
|
|
787
|
+
const sliced = lines.slice(start, end).join("\n");
|
|
788
|
+
console.log("\nSource Code:");
|
|
789
|
+
console.log("----------------------------------------");
|
|
790
|
+
console.log(sliced);
|
|
791
|
+
console.log("----------------------------------------");
|
|
792
|
+
} catch (err) {
|
|
793
|
+
console.error(`Failed to read source code: ${err.message}`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
program.command("files").description("List indexed files with prefix/lang/sort filters").option("-d, --dir <path>", "Target directory").option("--path <prefix>", "Filter by path prefix").option("--lang <lang>", "Filter by language").option("--sort <sort>", "lines | path", "path").option("--limit <limit>", "Max files to return", "50").action(async (opts) => {
|
|
798
|
+
const dir = resolveDir(opts, program.opts());
|
|
799
|
+
const { store } = await loadContext(dir);
|
|
800
|
+
const results = store.getFilesFiltered({
|
|
801
|
+
pathPrefix: opts.path,
|
|
802
|
+
lang: opts.lang,
|
|
803
|
+
sort: opts.sort,
|
|
804
|
+
limit: parseInt(opts.limit, 10)
|
|
805
|
+
});
|
|
806
|
+
if (results.length === 0) {
|
|
807
|
+
console.log("No files found matching filters");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
for (const file of results) {
|
|
811
|
+
console.log(` ${file.path} (${file.language}, ${file.lines} lines, ${file.size_bytes} bytes)`);
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
program.command("deps <file>").description("Show dependencies for a file").option("-d, --dir <path>", "Target directory").action(async (file, opts) => {
|
|
815
|
+
const dir = resolveDir(opts, program.opts());
|
|
816
|
+
const { store, graph } = await loadContext(dir);
|
|
817
|
+
const deps = graph.getDependencies(file);
|
|
818
|
+
const rdeps = graph.getReverseDependencies(file);
|
|
819
|
+
if (deps.length > 0) {
|
|
820
|
+
console.log("Dependencies:");
|
|
821
|
+
for (const dep of deps) {
|
|
822
|
+
console.log(` \u2192 ${dep.target} (${dep.type})`);
|
|
823
|
+
}
|
|
824
|
+
} else {
|
|
825
|
+
console.log("No dependencies found");
|
|
826
|
+
}
|
|
827
|
+
if (rdeps.length > 0) {
|
|
828
|
+
console.log("\nDepended on by:");
|
|
829
|
+
for (const rdep of rdeps) {
|
|
830
|
+
console.log(` \u2190 ${rdep.source} (${rdep.type})`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
program.command("trace [symbol-or-file]").description("Trace data flow paths from a starting symbol or file").option("-d, --dir <path>", "Target directory").option("--direction <dir>", "up | down | both", "both").option("--depth <n>", "Maximum traversal depth", "3").option("--max-depth <n>", "Maximum traversal depth (alias for --depth)").option("--format <fmt>", "text | dot | json", "text").option("--include-structural", "Include import/extends edges in trace", false).option("--sources", "Show entry points", false).option("--sinks", "Show terminal consumers", false).option("--to <target>", "Find the shortest path to target symbol/file").action(async (start, opts) => {
|
|
835
|
+
const dir = resolveDir(opts, program.opts());
|
|
836
|
+
const { config, store } = await loadContext(dir);
|
|
837
|
+
const tracer = new FlowTracer(store);
|
|
838
|
+
if (opts.sources) {
|
|
839
|
+
const sources = tracer.findSources(config.repo.name);
|
|
840
|
+
console.log(`
|
|
841
|
+
Entry points (data sources) \u2014 ${sources.length} found:`);
|
|
842
|
+
for (const s of sources) {
|
|
843
|
+
let extra = "[no incoming data edges]";
|
|
844
|
+
if (s.file.includes("routes/")) {
|
|
845
|
+
const routes = store.getEdgesForFile(s.file).filter((e) => e.edge_type === "route");
|
|
846
|
+
extra = `[route file \u2014 ${routes.length} controller endpoints]`;
|
|
847
|
+
} else if (s.file.includes("app/Jobs/")) {
|
|
848
|
+
extra = "[dispatched externally \u2014 queue worker]";
|
|
849
|
+
} else if (s.file.includes("app/Listeners/")) {
|
|
850
|
+
extra = "[event listener \u2014 external trigger]";
|
|
851
|
+
} else if (s.file.includes("app/Http/Middleware/")) {
|
|
852
|
+
extra = "[middleware \u2014 filter chain entry]";
|
|
853
|
+
}
|
|
854
|
+
console.log(` ${s.file.padEnd(40)} ${extra}`);
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (opts.sinks) {
|
|
859
|
+
const sinks = tracer.findSinks(config.repo.name);
|
|
860
|
+
console.log(`
|
|
861
|
+
Terminal consumers (data sinks) \u2014 ${sinks.length} found:`);
|
|
862
|
+
for (const s of sinks) {
|
|
863
|
+
const inEdges = store.getReverseEdges(s.file).filter((e) => [
|
|
864
|
+
"call",
|
|
865
|
+
"instantiation",
|
|
866
|
+
"param_type",
|
|
867
|
+
"return_type",
|
|
868
|
+
"relation",
|
|
869
|
+
"dispatch",
|
|
870
|
+
"notify",
|
|
871
|
+
"route"
|
|
872
|
+
].includes(e.edge_type));
|
|
873
|
+
let extra = `[terminal \u2014 no outgoing data edges]`;
|
|
874
|
+
if (s.file.includes("DatabaseManager") || s.file.includes("database")) {
|
|
875
|
+
extra = `[DB facade \u2192 raw SQL \u2014 ${inEdges.length} in-edges]`;
|
|
876
|
+
} else if (s.file.includes("CacheManager") || s.file.includes("cache")) {
|
|
877
|
+
extra = `[Cache facade \u2192 Redis/Memcache \u2014 ${inEdges.length} in-edges]`;
|
|
878
|
+
} else if (s.file.includes("Mailer") || s.file.includes("mail")) {
|
|
879
|
+
extra = `[Mail facade \u2192 SMTP \u2014 ${inEdges.length} in-edges]`;
|
|
880
|
+
} else if (s.file.includes("QueueManager") || s.file.includes("queue")) {
|
|
881
|
+
extra = `[Queue::push \u2014 ${inEdges.length} in-edges]`;
|
|
882
|
+
}
|
|
883
|
+
console.log(` ${s.file.padEnd(40)} ${extra}`);
|
|
884
|
+
}
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (!start) {
|
|
888
|
+
console.error("Error: start symbol or file is required unless --sources or --sinks is specified.");
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
if (opts.to) {
|
|
892
|
+
const path = tracer.findCriticalPath(start, opts.to, config.repo.name);
|
|
893
|
+
if (!path) {
|
|
894
|
+
console.log(`No path found from "${start}" to "${opts.to}"`);
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
console.log(`
|
|
898
|
+
Critical data path: ${start} \u2192 ${opts.to}`);
|
|
899
|
+
console.log(`Length: ${path.nodes.length - 1} hops
|
|
900
|
+
`);
|
|
901
|
+
for (let i = 0; i < path.nodes.length; i++) {
|
|
902
|
+
const node = path.nodes[i];
|
|
903
|
+
const indent = " ".repeat(i);
|
|
904
|
+
const prefix = i === 0 ? "" : `\u2514\u2500[${node.incomingEdgeType}]\u2500\u2192 `;
|
|
905
|
+
const suffix = i === path.nodes.length - 1 ? " \u2297" : "";
|
|
906
|
+
const name = node.symbol ? node.symbol : node.file;
|
|
907
|
+
console.log(`${indent}${prefix}${name}${suffix}`);
|
|
908
|
+
}
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const requestedDepth = opts.maxDepth !== void 0 ? opts.maxDepth : opts.depth;
|
|
912
|
+
const parsedDepth = parseInt(requestedDepth, 10);
|
|
913
|
+
const result = tracer.trace({
|
|
914
|
+
startSymbol: start,
|
|
915
|
+
direction: opts.direction,
|
|
916
|
+
maxDepth: parsedDepth,
|
|
917
|
+
includeStructural: !!opts.includeStructural,
|
|
918
|
+
repo: config.repo.name
|
|
919
|
+
});
|
|
920
|
+
if (opts.format === "json") {
|
|
921
|
+
const jsonOutput = {
|
|
922
|
+
start: result.start,
|
|
923
|
+
direction: result.direction,
|
|
924
|
+
maxDepth: parsedDepth,
|
|
925
|
+
nodeCount: result.nodeCount,
|
|
926
|
+
edgeCount: result.edgeCount,
|
|
927
|
+
maxDepthReached: result.maxDepthReached,
|
|
928
|
+
sources: result.sources.map((s) => ({ file: s.file, symbol: s.symbol })),
|
|
929
|
+
sinks: result.sinks.map((s) => ({ file: s.file, symbol: s.symbol })),
|
|
930
|
+
cycles: result.cycles,
|
|
931
|
+
nodes: Array.from(new Map(result.paths.flatMap((p) => p.nodes).map((n) => [`${n.file}::${n.symbol || ""}`, n])).values()).map((n) => ({
|
|
932
|
+
file: n.file,
|
|
933
|
+
symbol: n.symbol,
|
|
934
|
+
depth: n.depth,
|
|
935
|
+
incomingEdgeType: n.incomingEdgeType
|
|
936
|
+
})),
|
|
937
|
+
edges: Array.from(new Set(result.paths.flatMap((p) => {
|
|
938
|
+
const arr = [];
|
|
939
|
+
for (let i = 1; i < p.nodes.length; i++) {
|
|
940
|
+
arr.push(JSON.stringify({
|
|
941
|
+
from: p.nodes[i - 1].file,
|
|
942
|
+
to: p.nodes[i].file,
|
|
943
|
+
edgeType: p.nodes[i].incomingEdgeType,
|
|
944
|
+
fromSymbol: p.nodes[i - 1].symbol,
|
|
945
|
+
toSymbol: p.nodes[i].symbol
|
|
946
|
+
}));
|
|
947
|
+
}
|
|
948
|
+
return arr;
|
|
949
|
+
}))).map((s) => JSON.parse(s))
|
|
950
|
+
};
|
|
951
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (opts.format === "dot") {
|
|
955
|
+
const lines = [];
|
|
956
|
+
const safeStartName = (result.start.symbol || result.start.file).replace(/[^a-zA-Z0-9]/g, "_");
|
|
957
|
+
lines.push(`digraph Trace_${safeStartName} {`);
|
|
958
|
+
lines.push(" rankdir=TB;");
|
|
959
|
+
lines.push(` label="Trace: ${result.start.symbol || result.start.file} (${result.direction}stream, depth\u2264${parsedDepth})";`);
|
|
960
|
+
lines.push(" fontsize=12;");
|
|
961
|
+
lines.push(" node [shape=box, style=filled, fontsize=10];");
|
|
962
|
+
lines.push("");
|
|
963
|
+
const uniqueNodes = /* @__PURE__ */ new Map();
|
|
964
|
+
const edgesSet = /* @__PURE__ */ new Set();
|
|
965
|
+
for (const p of result.paths) {
|
|
966
|
+
for (let i = 0; i < p.nodes.length; i++) {
|
|
967
|
+
const n = p.nodes[i];
|
|
968
|
+
const key = `${n.file}::${n.symbol || ""}`;
|
|
969
|
+
if (!uniqueNodes.has(key)) {
|
|
970
|
+
let shape = "box";
|
|
971
|
+
let color = "#E8F4FD";
|
|
972
|
+
const isStart = n.file === result.start.file && n.symbol === result.start.symbol;
|
|
973
|
+
const isSink = result.sinks.some((s) => s.file === n.file && s.symbol === n.symbol);
|
|
974
|
+
const isSource = result.sources.some((s) => s.file === n.file && s.symbol === n.symbol);
|
|
975
|
+
if (isStart) {
|
|
976
|
+
shape = "diamond";
|
|
977
|
+
color = "#FFE0B2";
|
|
978
|
+
} else if (isSink) {
|
|
979
|
+
shape = "octagon";
|
|
980
|
+
color = "#FFEBEE";
|
|
981
|
+
} else if (isSource) {
|
|
982
|
+
shape = "ellipse";
|
|
983
|
+
color = "#E8F5E9";
|
|
984
|
+
}
|
|
985
|
+
uniqueNodes.set(key, { file: n.file, symbol: n.symbol, shape, color });
|
|
986
|
+
}
|
|
987
|
+
if (i > 0) {
|
|
988
|
+
const fromNode = p.nodes[i - 1];
|
|
989
|
+
const toNode = p.nodes[i];
|
|
990
|
+
edgesSet.add(JSON.stringify({
|
|
991
|
+
from: `${fromNode.file}::${fromNode.symbol || ""}`,
|
|
992
|
+
to: `${toNode.file}::${toNode.symbol || ""}`,
|
|
993
|
+
type: toNode.incomingEdgeType
|
|
994
|
+
}));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
for (const [key, n] of uniqueNodes.entries()) {
|
|
999
|
+
const label = n.symbol || n.file.split("/").pop() || n.file;
|
|
1000
|
+
lines.push(` "${key}" [label="${label}", fillcolor="${n.color}", shape=${n.shape}];`);
|
|
1001
|
+
}
|
|
1002
|
+
lines.push("");
|
|
1003
|
+
for (const edgeStr of edgesSet) {
|
|
1004
|
+
const e = JSON.parse(edgeStr);
|
|
1005
|
+
lines.push(` "${e.from}" -> "${e.to}" [label="${e.type}"];`);
|
|
1006
|
+
}
|
|
1007
|
+
lines.push("}");
|
|
1008
|
+
console.log(lines.join("\n"));
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const dirSymbol = result.direction === "down" ? "\u2193 downstream" : result.direction === "up" ? "\u2191 upstream" : "\u2195 bidirectional";
|
|
1012
|
+
console.log(`
|
|
1013
|
+
Trace: ${start} ${dirSymbol} depth\u2264${parsedDepth}`);
|
|
1014
|
+
console.log("\u2500".repeat(53));
|
|
1015
|
+
console.log("");
|
|
1016
|
+
const printNode = (node, indentLevel) => {
|
|
1017
|
+
const indent = " ".repeat(indentLevel);
|
|
1018
|
+
const prefix = indentLevel === 0 ? "" : `\u2514\u2500[${node.incomingEdgeType}]\u2500\u2192 `;
|
|
1019
|
+
const displayName = node.symbol || node.file;
|
|
1020
|
+
const filePart = node.symbol ? ` (${node.file})` : "";
|
|
1021
|
+
const isSink = result.sinks.some((s) => s.file === node.file && s.symbol === node.symbol);
|
|
1022
|
+
const sinkStr = isSink ? " \u2297 sink" : "";
|
|
1023
|
+
const cycle = result.cycles.find((c) => c.fromFile === node.file && c.fromSymbol === node.symbol);
|
|
1024
|
+
const cycleStr = cycle ? " \u21BB cycle" : "";
|
|
1025
|
+
console.log(`${indent}${prefix}${displayName}${filePart}${sinkStr}${cycleStr}`);
|
|
1026
|
+
if (!cycle) {
|
|
1027
|
+
const children = [];
|
|
1028
|
+
const seenChildKeys = /* @__PURE__ */ new Set();
|
|
1029
|
+
for (const path of result.paths) {
|
|
1030
|
+
const idx = path.nodes.findIndex((n) => n.file === node.file && n.symbol === node.symbol && n.depth === node.depth);
|
|
1031
|
+
if (idx !== -1 && idx + 1 < path.nodes.length) {
|
|
1032
|
+
const nextNode = path.nodes[idx + 1];
|
|
1033
|
+
const key = `${nextNode.file}::${nextNode.symbol || ""}::${nextNode.depth}`;
|
|
1034
|
+
if (!seenChildKeys.has(key)) {
|
|
1035
|
+
seenChildKeys.add(key);
|
|
1036
|
+
children.push(nextNode);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
for (const child of children) {
|
|
1041
|
+
printNode(child, indentLevel + 1);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
const startNode = {
|
|
1046
|
+
file: result.start.file,
|
|
1047
|
+
symbol: result.start.symbol,
|
|
1048
|
+
depth: 0,
|
|
1049
|
+
incomingEdgeType: "start"
|
|
1050
|
+
};
|
|
1051
|
+
printNode(startNode, 0);
|
|
1052
|
+
console.log("");
|
|
1053
|
+
const cyclesStr = result.cycles.length > 0 ? ` Cycles: ${result.cycles.length}` : "";
|
|
1054
|
+
console.log(`Nodes: ${result.nodeCount} Edges: ${result.edgeCount} Max depth: ${parsedDepth}${cyclesStr}`);
|
|
1055
|
+
if (result.sinks.length > 0) {
|
|
1056
|
+
const sinkNames = result.sinks.map((s) => s.symbol || s.file.split("/").pop() || s.file);
|
|
1057
|
+
console.log(`Sinks: ${sinkNames.join(", ")}`);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
program.command("export").description("Export code graph for LLM consumption").option("-d, --dir <path>", "Target directory").option("--format <format>", "Output format: llm, json, dot, svg, toon", "llm").option("--tokens <budget>", "Token budget for LLM export", "8192").option("--repo <name>", "Filter by repo name").option("-o, --output <file>", "Write output to file instead of stdout").option("--exclude <glob>", "Exclude glob pattern(s)", collectPatterns, []).option("--include <glob>", "Include glob pattern(s)", collectPatterns, []).option("--delimiter <delimiter>", "Delimiter for TOON format: comma, tab, pipe", "comma").option("--key-folding", "Collapse single-key chains into dotted paths for TOON", false).option("--cluster <mode>", "Cluster rendering mode for DOT/SVG: none, auto", "none").option("--depth <n>", "Maximum cluster nesting depth for DOT/SVG export", "3").option("--fallback-grid", "Force using fallback grid SVG export", false).action(async (opts) => {
|
|
1061
|
+
const dir = resolveDir(opts, program.opts());
|
|
1062
|
+
const { config, store, graph } = await loadContext(dir);
|
|
1063
|
+
const format = opts.format;
|
|
1064
|
+
const tokenBudget = parseInt(opts.tokens, 10) || 8192;
|
|
1065
|
+
const outputPath = opts.output;
|
|
1066
|
+
const delimiter = opts.delimiter;
|
|
1067
|
+
const keyFolding = !!opts.keyFolding;
|
|
1068
|
+
const clusterMode = opts.cluster === "none" ? "none" : "auto";
|
|
1069
|
+
const clusterDepth = opts.depth ? parseInt(opts.depth, 10) : 3;
|
|
1070
|
+
const fallbackGrid = !!opts.fallbackGrid;
|
|
1071
|
+
const clusterOpts = { cluster: clusterMode, depth: clusterDepth, forceFallback: fallbackGrid };
|
|
1072
|
+
if (outputPath) {
|
|
1073
|
+
const outputDir = resolve(outputPath, "..");
|
|
1074
|
+
if (!existsSync(outputDir)) {
|
|
1075
|
+
console.error(`Error: output directory does not exist: ${outputDir}`);
|
|
1076
|
+
process.exit(1);
|
|
1077
|
+
}
|
|
1078
|
+
try {
|
|
1079
|
+
writeFileSync(outputPath, "", "utf-8");
|
|
1080
|
+
} catch {
|
|
1081
|
+
console.error(`Error: cannot write to: ${resolve(outputPath)}`);
|
|
1082
|
+
process.exit(1);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const excludes = [
|
|
1086
|
+
...config.settings.excludePatterns ?? [],
|
|
1087
|
+
...opts.exclude ?? []
|
|
1088
|
+
];
|
|
1089
|
+
const includes = [
|
|
1090
|
+
...config.settings.includePatterns ?? [],
|
|
1091
|
+
...opts.include ?? []
|
|
1092
|
+
];
|
|
1093
|
+
const matcher = buildMatcher(excludes, includes);
|
|
1094
|
+
const allFiles = store.getAllFiles(opts.repo).map((f) => f.path);
|
|
1095
|
+
const filteredFiles = allFiles.filter((f) => matcher(f));
|
|
1096
|
+
let output;
|
|
1097
|
+
switch (format) {
|
|
1098
|
+
case "json": {
|
|
1099
|
+
const exporter = new GraphExporter(store, graph);
|
|
1100
|
+
output = exporter.exportAsJSONString(opts.repo, filteredFiles);
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
case "dot": {
|
|
1104
|
+
const exporter = new DotExporter(store, graph);
|
|
1105
|
+
output = exporter.export(opts.repo, filteredFiles, clusterOpts);
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
case "svg": {
|
|
1109
|
+
const exporter = new SvgExporter(store, graph);
|
|
1110
|
+
output = exporter.export(opts.repo, filteredFiles, clusterOpts);
|
|
1111
|
+
break;
|
|
1112
|
+
}
|
|
1113
|
+
case "toon": {
|
|
1114
|
+
const exporter = new ToonExporter(store, graph);
|
|
1115
|
+
output = exporter.export({
|
|
1116
|
+
format: "toon",
|
|
1117
|
+
tokenBudget,
|
|
1118
|
+
repo: opts.repo,
|
|
1119
|
+
files: filteredFiles,
|
|
1120
|
+
delimiter,
|
|
1121
|
+
keyFolding
|
|
1122
|
+
});
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
case "llm":
|
|
1126
|
+
default: {
|
|
1127
|
+
const exporter = new LLMExporter(store, graph);
|
|
1128
|
+
output = exporter.export({
|
|
1129
|
+
format: "llm",
|
|
1130
|
+
tokenBudget,
|
|
1131
|
+
repo: opts.repo,
|
|
1132
|
+
files: filteredFiles
|
|
1133
|
+
});
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (outputPath) {
|
|
1138
|
+
writeFileSync(resolve(outputPath), output, "utf-8");
|
|
1139
|
+
console.log(`Exported ${format} to ${resolve(outputPath)} (${Buffer.byteLength(output, "utf-8")} bytes)`);
|
|
1140
|
+
} else {
|
|
1141
|
+
console.log(output);
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
program.command("summary").description("Show project summary").argument("[path]", "Target directory").action(async (path) => {
|
|
1145
|
+
const dir = path ? resolve(path) : resolveDir({}, program.opts());
|
|
1146
|
+
const { store, graph, config } = await loadContext(dir);
|
|
1147
|
+
const fileCount = store.getFileCount();
|
|
1148
|
+
const symbolCount = store.getSymbolCount();
|
|
1149
|
+
const edgeCount = store.getEdgeCount();
|
|
1150
|
+
const breakdown = store.getLanguageBreakdown();
|
|
1151
|
+
console.log(`Project: ${config.repo.name} (${dir})`);
|
|
1152
|
+
console.log(`Files: ${fileCount}`);
|
|
1153
|
+
console.log(`Symbols: ${symbolCount}`);
|
|
1154
|
+
console.log(`Dependencies: ${edgeCount}`);
|
|
1155
|
+
console.log(`Languages: ${Object.entries(breakdown).map(([l, c]) => `${l} (${c})`).join(", ")}`);
|
|
1156
|
+
});
|
|
1157
|
+
const langCmd = program.command("lang").description("Manage language grammars and configuration");
|
|
1158
|
+
langCmd.command("list").description("List all supported languages, their extensions, tier, and status").action(() => {
|
|
1159
|
+
const langs = getBuiltinLanguages();
|
|
1160
|
+
console.log("Supported languages:");
|
|
1161
|
+
for (const [name, def] of Object.entries(langs)) {
|
|
1162
|
+
const installed = isLanguageInstalled(name) ? "Installed" : "Not Installed";
|
|
1163
|
+
console.log(` - ${name} (${def.extensions.join(", ")} | tier: ${def.tier} | status: ${installed})`);
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
langCmd.command("install <lang>").description("Install grammar and query files for an installable language").action(async (lang) => {
|
|
1167
|
+
try {
|
|
1168
|
+
console.log(`Installing language '${lang}'...`);
|
|
1169
|
+
await installLanguage(lang);
|
|
1170
|
+
console.log(`Successfully installed language '${lang}'.`);
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
console.error(`Error installing language '${lang}':`, err.message);
|
|
1173
|
+
process.exit(1);
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
langCmd.command("uninstall <lang>").description("Uninstall grammar and query files for an installable language").action(async (lang) => {
|
|
1177
|
+
try {
|
|
1178
|
+
console.log(`Uninstalling language '${lang}'...`);
|
|
1179
|
+
await uninstallLanguage(lang);
|
|
1180
|
+
console.log(`Successfully uninstalled language '${lang}'.`);
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
console.error(`Error uninstalling language '${lang}':`, err.message);
|
|
1183
|
+
process.exit(1);
|
|
1184
|
+
}
|
|
1185
|
+
});
|
|
1186
|
+
program.command("serve").description("Start MCP server (stdio transport)").option("-d, --dir <path>", "Default target directory for MCP tools").option("--port <port>", "Port for SSE transport (default: 45123)", "45123").option("--sse", "Enable SSE transport instead of stdio").option("--ui", "Enable UI dashboard alongside MCP server").option("--ui-port <port>", "Port to run UI on (default: 45124)", "45124").option("--ui-host <host>", "Host to run UI on (default: 127.0.0.1)", "127.0.0.1").option("--ui-token <token>", "Bearer token for authorization").option("--debug", "Enable verbose debug logging of MCP calls to stderr").action(async (opts) => {
|
|
1187
|
+
const defaultDir = resolveDir(opts, program.opts());
|
|
1188
|
+
const { startMcpServer } = await import("./mcp.js");
|
|
1189
|
+
if (opts.ui) {
|
|
1190
|
+
const { startUiServer } = await import("./ui-server.js");
|
|
1191
|
+
const uiPort = parseInt(opts.uiPort, 10) || 45124;
|
|
1192
|
+
const uiHost = opts.uiHost || "127.0.0.1";
|
|
1193
|
+
const uiToken = opts.uiToken;
|
|
1194
|
+
startUiServer({ port: uiPort, host: uiHost, token: uiToken, dir: defaultDir });
|
|
1195
|
+
}
|
|
1196
|
+
await startMcpServer(defaultDir, {
|
|
1197
|
+
sse: opts.sse,
|
|
1198
|
+
port: parseInt(opts.port, 10) || 45123,
|
|
1199
|
+
debug: opts.debug
|
|
1200
|
+
});
|
|
1201
|
+
});
|
|
1202
|
+
program.command("ui").description("Start the Web Dashboard").argument("[path]", "Target directory").option("-d, --dir <path>", "Target directory").option("-p, --port <port>", "Port to run UI on (default: 45124)", "45124").option("--host <host>", "Host to run UI on (default: 127.0.0.1)", "127.0.0.1").option("--token <token>", "Bearer token for authorization").option("--no-open", "Do not open the dashboard in the browser automatically").action(async (path, opts) => {
|
|
1203
|
+
const dir = path ? resolve(path) : resolveDir(opts, program.opts());
|
|
1204
|
+
const port = parseInt(opts.port, 10) || 45124;
|
|
1205
|
+
const host = opts.host || "127.0.0.1";
|
|
1206
|
+
const token = opts.token;
|
|
1207
|
+
const { startUiServer } = await import("./ui-server.js");
|
|
1208
|
+
startUiServer({ port, host, token, dir });
|
|
1209
|
+
const url = `http://${host}:${port}`;
|
|
1210
|
+
console.log(`Mapx Web Dashboard started at ${url}`);
|
|
1211
|
+
if (opts.open !== false) {
|
|
1212
|
+
const { exec } = await import("node:child_process");
|
|
1213
|
+
const openCmd = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
1214
|
+
exec(`${openCmd} ${url}`).unref();
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
program.command("metrics").description("Show coupling and instability metrics for files").argument("[path]", "Target directory").option("-d, --dir <path>", "Target directory").option("--lang <language>", "Filter metrics by language").option("--verified-only", "Only compute metrics using verified edges").action(async (path, opts) => {
|
|
1218
|
+
const dir = path ? resolve(path) : resolveDir(opts, program.opts());
|
|
1219
|
+
const { config, store } = await loadContext(dir);
|
|
1220
|
+
const metrics = calculateMetrics(store, {
|
|
1221
|
+
repo: config.repo.name,
|
|
1222
|
+
language: opts.lang,
|
|
1223
|
+
verifiedOnly: !!opts.verifiedOnly
|
|
1224
|
+
});
|
|
1225
|
+
if (metrics.length === 0) {
|
|
1226
|
+
console.log("No metrics found.");
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
console.log("\n\u2500\u2500 Coupling & Instability Metrics \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1230
|
+
console.log(`${"File Path".padEnd(45)} | ${"Lang".padEnd(10)} | ${"Ca".padStart(4)} | ${"Ce".padStart(4)} | ${"Instability".padStart(11)}`);
|
|
1231
|
+
console.log("-".repeat(85));
|
|
1232
|
+
for (const m of metrics) {
|
|
1233
|
+
const pathTrunc = m.path.length > 45 ? "..." + m.path.substring(m.path.length - 42) : m.path;
|
|
1234
|
+
console.log(`${pathTrunc.padEnd(45)} | ${m.language.padEnd(10)} | ${String(m.afferent).padStart(4)} | ${String(m.efferent).padStart(4)} | ${m.instability.toFixed(4).padStart(11)}`);
|
|
1235
|
+
}
|
|
1236
|
+
console.log("");
|
|
1237
|
+
});
|
|
1238
|
+
program.command("clusters").description("List detected code clusters/modules").argument("[clusterOrPath]", "Target directory or a specific cluster name to inspect").option("-d, --dir <path>", "Target directory").option("--source <source>", "Filter by cluster source: namespace, directory, community, or all", "all").option("--json", "Output results as JSON").action(async (clusterOrPath, opts) => {
|
|
1239
|
+
let dir = resolveDir(opts, program.opts());
|
|
1240
|
+
let clusterQuery = void 0;
|
|
1241
|
+
if (clusterOrPath) {
|
|
1242
|
+
const resolvedPath = resolve(clusterOrPath);
|
|
1243
|
+
if (existsSync(resolvedPath)) {
|
|
1244
|
+
dir = resolvedPath;
|
|
1245
|
+
} else {
|
|
1246
|
+
clusterQuery = clusterOrPath;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
const { config, store } = await loadContext(dir);
|
|
1250
|
+
const source = opts.source;
|
|
1251
|
+
const json = !!opts.json;
|
|
1252
|
+
const clusters = store.getClusters(config.repo.name);
|
|
1253
|
+
let filtered = clusters;
|
|
1254
|
+
if (source && source !== "all") {
|
|
1255
|
+
filtered = clusters.filter((c) => c.source === source);
|
|
1256
|
+
}
|
|
1257
|
+
if (clusterQuery) {
|
|
1258
|
+
const targetCluster = clusters.find((c) => c.name === clusterQuery);
|
|
1259
|
+
if (!targetCluster) {
|
|
1260
|
+
console.error(`Cluster "${clusterQuery}" not found.`);
|
|
1261
|
+
process.exit(1);
|
|
1262
|
+
}
|
|
1263
|
+
const files = store.getClusterFiles(targetCluster.name, config.repo.name);
|
|
1264
|
+
const clusterEdges = store.getClusterEdges(targetCluster.name, config.repo.name);
|
|
1265
|
+
if (json) {
|
|
1266
|
+
console.log(JSON.stringify({
|
|
1267
|
+
cluster: targetCluster,
|
|
1268
|
+
files,
|
|
1269
|
+
edges: clusterEdges
|
|
1270
|
+
}, null, 2));
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
console.log(`
|
|
1274
|
+
${targetCluster.name} [${targetCluster.source}] ${targetCluster.file_count} files`);
|
|
1275
|
+
for (const f of files) {
|
|
1276
|
+
console.log(` ${f}`);
|
|
1277
|
+
}
|
|
1278
|
+
const dependsOn = clusterEdges.filter((e) => e.sourceCluster === targetCluster.name);
|
|
1279
|
+
console.log("\nDepends on:");
|
|
1280
|
+
if (dependsOn.length === 0) {
|
|
1281
|
+
console.log(" (none)");
|
|
1282
|
+
} else {
|
|
1283
|
+
for (const dep of dependsOn) {
|
|
1284
|
+
console.log(` ${dep.targetCluster.padEnd(25)} [${dep.edgeCount} edges \u2014 dominant: ${dep.dominantType}]`);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const dependedOnBy = clusterEdges.filter((e) => e.targetCluster === targetCluster.name);
|
|
1288
|
+
console.log("\nDepended on by:");
|
|
1289
|
+
if (dependedOnBy.length === 0) {
|
|
1290
|
+
console.log(" (none)");
|
|
1291
|
+
} else {
|
|
1292
|
+
for (const dep of dependedOnBy) {
|
|
1293
|
+
console.log(` ${dep.sourceCluster.padEnd(25)} [${dep.edgeCount} edges \u2014 dominant: ${dep.dominantType}]`);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
console.log("");
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
if (json) {
|
|
1300
|
+
console.log(JSON.stringify({ clusters: filtered }, null, 2));
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
const roots = [];
|
|
1304
|
+
const childrenMap = /* @__PURE__ */ new Map();
|
|
1305
|
+
for (const c of filtered) {
|
|
1306
|
+
if (!c.parent_name) {
|
|
1307
|
+
roots.push(c);
|
|
1308
|
+
} else {
|
|
1309
|
+
const parentName = c.parent_name;
|
|
1310
|
+
if (!childrenMap.has(parentName)) {
|
|
1311
|
+
childrenMap.set(parentName, []);
|
|
1312
|
+
}
|
|
1313
|
+
childrenMap.get(parentName).push(c);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
for (const list of childrenMap.values()) {
|
|
1317
|
+
list.sort((a, b) => a.name.localeCompare(b.name));
|
|
1318
|
+
}
|
|
1319
|
+
roots.sort((a, b) => a.name.localeCompare(b.name));
|
|
1320
|
+
const printTree = (node, indent) => {
|
|
1321
|
+
const padding = " ".repeat(indent);
|
|
1322
|
+
const namePart = node.name;
|
|
1323
|
+
const sourcePart = `(${node.source})`;
|
|
1324
|
+
const filesPart = `[${node.file_count} files]`;
|
|
1325
|
+
const formatted = `${padding}${namePart.padEnd(35 - indent * 2)}${sourcePart.padEnd(15)} ${filesPart}`;
|
|
1326
|
+
console.log(formatted);
|
|
1327
|
+
const children = childrenMap.get(node.name) || [];
|
|
1328
|
+
for (const child of children) {
|
|
1329
|
+
printTree(child, indent + 1);
|
|
1330
|
+
}
|
|
1331
|
+
};
|
|
1332
|
+
console.log("");
|
|
1333
|
+
for (const root of roots) {
|
|
1334
|
+
printTree(root, 0);
|
|
1335
|
+
}
|
|
1336
|
+
const nsCount = filtered.filter((c) => c.source === "namespace").length;
|
|
1337
|
+
const dirCount = filtered.filter((c) => c.source === "directory").length;
|
|
1338
|
+
const commCount = filtered.filter((c) => c.source === "community").length;
|
|
1339
|
+
console.log(`
|
|
1340
|
+
${filtered.length} clusters detected (${nsCount} namespace, ${dirCount} directory, ${commCount} community)
|
|
1341
|
+
`);
|
|
1342
|
+
});
|
|
1343
|
+
program.command("edges").description("Granular query of dependency edges").argument("[path]", "Target directory").option("-d, --dir <path>", "Target directory").option("--type <type>", "Filter edges by type").option("--from <file>", "Filter edges originating from a file pattern").option("--to <file>", "Filter edges targeting a file pattern").action(async (path, opts) => {
|
|
1344
|
+
const dir = path ? resolve(path) : resolveDir(opts, program.opts());
|
|
1345
|
+
const { config, store } = await loadContext(dir);
|
|
1346
|
+
const edges = store.queryEdges({
|
|
1347
|
+
repo: config.repo.name,
|
|
1348
|
+
type: opts.type,
|
|
1349
|
+
from: opts.from,
|
|
1350
|
+
to: opts.to
|
|
1351
|
+
});
|
|
1352
|
+
if (edges.length === 0) {
|
|
1353
|
+
console.log("No matching edges found.");
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
console.log(`
|
|
1357
|
+
Found ${edges.length} matching edges:`);
|
|
1358
|
+
for (const e of edges) {
|
|
1359
|
+
const srcSym = e.source_symbol ? `#${e.source_symbol}` : "";
|
|
1360
|
+
const tgtSym = e.target_symbol ? `#${e.target_symbol}` : "";
|
|
1361
|
+
const infSuffix = e.verifiability === "inferred" ? " [inferred]" : "";
|
|
1362
|
+
console.log(`- ${e.source_file}${srcSym} \u2192 ${e.target_file}${tgtSym} (${e.edge_type})${infSuffix}`);
|
|
1363
|
+
}
|
|
1364
|
+
console.log("");
|
|
1365
|
+
});
|
|
1366
|
+
program.command("routes").description("Show routes from all detected frameworks").argument("[path]", "Target directory").option("-d, --dir <path>", "Target directory").option("--framework <name>", "Filter by framework name").option("--method <verb>", "Filter by HTTP method (GET, POST, etc.)").option("--path-pattern <pattern>", "Filter by route path pattern").option("--json", "Output routes as JSON").action(async (path, opts) => {
|
|
1367
|
+
const dir = path ? resolve(path) : resolveDir(opts, program.opts());
|
|
1368
|
+
const routeRegistry = new RouteRegistry();
|
|
1369
|
+
await routeRegistry.load(dir);
|
|
1370
|
+
const routes = routeRegistry.queryRoutes({
|
|
1371
|
+
framework: opts.framework,
|
|
1372
|
+
method: opts.method,
|
|
1373
|
+
path: opts.pathPattern
|
|
1374
|
+
});
|
|
1375
|
+
if (opts.json) {
|
|
1376
|
+
console.log(JSON.stringify(routes, null, 2));
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (routes.length === 0) {
|
|
1380
|
+
console.log("No routes found.");
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
console.log(`
|
|
1384
|
+
Detected Routes (${routes.length}):`);
|
|
1385
|
+
console.log("".padEnd(80, "-"));
|
|
1386
|
+
console.log(`${"Framework".padEnd(12)} | ${"Method".padEnd(8)} | ${"Path".padEnd(30)} | ${"Handler"}`);
|
|
1387
|
+
console.log("".padEnd(80, "-"));
|
|
1388
|
+
for (const r of routes) {
|
|
1389
|
+
const handler = r.handlerSymbol || r.handlerFile;
|
|
1390
|
+
console.log(`${r.framework.padEnd(12)} | ${r.method.toUpperCase().padEnd(8)} | ${r.path.padEnd(30)} | ${handler}`);
|
|
1391
|
+
}
|
|
1392
|
+
console.log("".padEnd(80, "-"));
|
|
1393
|
+
console.log("");
|
|
1394
|
+
});
|
|
1395
|
+
program.command("hooks").description("Show hooks from all detected frameworks").argument("[path]", "Target directory").option("-d, --dir <path>", "Target directory").option("--framework <name>", "Filter by framework name").option("--type <type>", "Filter by hook type").option("--name <pattern>", "Filter by hook name pattern").option("--json", "Output hooks as JSON").action(async (path, opts) => {
|
|
1396
|
+
const dir = path ? resolve(path) : resolveDir(opts, program.opts());
|
|
1397
|
+
const routeRegistry = new RouteRegistry();
|
|
1398
|
+
await routeRegistry.load(dir);
|
|
1399
|
+
const hooks = routeRegistry.queryHooks({
|
|
1400
|
+
framework: opts.framework,
|
|
1401
|
+
hookType: opts.type,
|
|
1402
|
+
hookName: opts.name
|
|
1403
|
+
});
|
|
1404
|
+
if (opts.json) {
|
|
1405
|
+
console.log(JSON.stringify(hooks, null, 2));
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (hooks.length === 0) {
|
|
1409
|
+
console.log("No hooks found.");
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
console.log(`
|
|
1413
|
+
Detected Hooks (${hooks.length}):`);
|
|
1414
|
+
console.log("".padEnd(80, "-"));
|
|
1415
|
+
console.log(`${"Framework".padEnd(12)} | ${"Type".padEnd(15)} | ${"Hook Name".padEnd(25)} | ${"Handler"}`);
|
|
1416
|
+
console.log("".padEnd(80, "-"));
|
|
1417
|
+
for (const h of hooks) {
|
|
1418
|
+
const handler = h.handlerSymbol || h.handlerFile;
|
|
1419
|
+
console.log(`${h.framework.padEnd(12)} | ${h.hookType.padEnd(15)} | ${h.hookName.padEnd(25)} | ${handler}`);
|
|
1420
|
+
}
|
|
1421
|
+
console.log("".padEnd(80, "-"));
|
|
1422
|
+
console.log("");
|
|
1423
|
+
});
|
|
1424
|
+
const agentsCmd = program.command("agents").description("Manage LLM agent integration files");
|
|
1425
|
+
agentsCmd.command("list").description("List all supported LLM integration providers").action(() => {
|
|
1426
|
+
const generator = new AgentGenerator();
|
|
1427
|
+
const providers = generator.listProviders();
|
|
1428
|
+
console.log("\nSupported LLM integration providers:");
|
|
1429
|
+
for (const p of providers) {
|
|
1430
|
+
const temp = generator.getTemplate(p);
|
|
1431
|
+
const appendStr = temp?.isAppend ? " (append-mode)" : "";
|
|
1432
|
+
console.log(` - ${p.padEnd(12)} -> ${temp?.filename}${appendStr}`);
|
|
1433
|
+
}
|
|
1434
|
+
console.log("");
|
|
1435
|
+
});
|
|
1436
|
+
agentsCmd.command("generate").description("Generate/overwrite LLM integration files").option("--providers <list>", "Comma-separated list of providers to generate").option("--all", "Generate integration files for all supported providers").option("--dry-run", "Show actions without writing files").option("--force", "Force overwrite of existing files without prompt").option("--mcp-port <number>", "Port for the MCP SSE transport server", "3456").action(async (opts) => {
|
|
1437
|
+
const dir = program.opts().dir ? resolve(program.opts().dir) : process.cwd();
|
|
1438
|
+
const generator = new AgentGenerator();
|
|
1439
|
+
const available = generator.listProviders();
|
|
1440
|
+
let targets = [];
|
|
1441
|
+
if (opts.all) {
|
|
1442
|
+
targets = available;
|
|
1443
|
+
} else if (opts.providers) {
|
|
1444
|
+
targets = opts.providers.split(",").map((s) => s.trim().toLowerCase()).filter((p) => available.includes(p));
|
|
1445
|
+
} else {
|
|
1446
|
+
if (process.stdin.isTTY) {
|
|
1447
|
+
targets = await selectProvidersInteractive();
|
|
1448
|
+
} else {
|
|
1449
|
+
targets = ["generic"];
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
if (targets.length === 0) {
|
|
1453
|
+
console.error("No valid providers specified.");
|
|
1454
|
+
process.exit(1);
|
|
1455
|
+
}
|
|
1456
|
+
const actions = generator.plan(targets, { dir, mcpPort: parseInt(opts.mcpPort, 10) });
|
|
1457
|
+
for (const action of actions) {
|
|
1458
|
+
if (action.status === "up_to_date") {
|
|
1459
|
+
console.log(` - ${action.filename}: Up to date. Skipping.`);
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
if (action.status === "update_conflict" || action.status === "no_sentinel") {
|
|
1463
|
+
console.log(`
|
|
1464
|
+
\u26A0\uFE0F Conflict/Modification detected in ${action.filename}:`);
|
|
1465
|
+
if (action.diff) {
|
|
1466
|
+
console.log(action.diff);
|
|
1467
|
+
}
|
|
1468
|
+
if (!opts.force) {
|
|
1469
|
+
const confirm = await askQuestion(`Overwrite ${action.filename}? [y/N] `);
|
|
1470
|
+
if (confirm.trim().toLowerCase() !== "y") {
|
|
1471
|
+
console.log(`Skipped ${action.filename}.`);
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (opts.dryRun) {
|
|
1477
|
+
console.log(`[DRY RUN] Would write to ${action.filepath} (status: ${action.status})`);
|
|
1478
|
+
} else {
|
|
1479
|
+
generator.execute(action);
|
|
1480
|
+
console.log(`\u2713 Wrote to ${action.filename} (status: ${action.status})`);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
agentsCmd.command("update").description("Update existing LLM integration files to the current MapxGraph version").option("--dry-run", "Show updates without writing files").option("--force", "Force overwrite of customized blocks without prompt").option("--mcp-port <number>", "Port for the MCP SSE transport server", "3456").action(async (opts) => {
|
|
1485
|
+
const dir = program.opts().dir ? resolve(program.opts().dir) : process.cwd();
|
|
1486
|
+
const generator = new AgentGenerator();
|
|
1487
|
+
const available = generator.listProviders();
|
|
1488
|
+
const existingProviders = available.filter((p) => {
|
|
1489
|
+
const temp = generator.getTemplate(p);
|
|
1490
|
+
return temp && existsSync(join(dir, temp.filename));
|
|
1491
|
+
});
|
|
1492
|
+
if (existingProviders.length === 0) {
|
|
1493
|
+
console.log("No existing LLM integration files found to update.");
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
const actions = generator.plan(existingProviders, { dir, mcpPort: parseInt(opts.mcpPort, 10) });
|
|
1497
|
+
let updatedCount = 0;
|
|
1498
|
+
for (const action of actions) {
|
|
1499
|
+
if (action.status === "up_to_date") {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (action.status === "update_conflict") {
|
|
1503
|
+
console.log(`
|
|
1504
|
+
\u26A0\uFE0F Customized content detected in ${action.filename}:`);
|
|
1505
|
+
if (action.diff) {
|
|
1506
|
+
console.log(action.diff);
|
|
1507
|
+
}
|
|
1508
|
+
if (!opts.force) {
|
|
1509
|
+
const confirm = await askQuestion(`Overwrite customizations in ${action.filename}? [y/N] `);
|
|
1510
|
+
if (confirm.trim().toLowerCase() !== "y") {
|
|
1511
|
+
console.log(`Skipped ${action.filename}.`);
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
if (opts.dryRun) {
|
|
1517
|
+
console.log(`[DRY RUN] Would update ${action.filepath}`);
|
|
1518
|
+
} else {
|
|
1519
|
+
generator.execute(action);
|
|
1520
|
+
console.log(`\u2713 Updated ${action.filename}`);
|
|
1521
|
+
updatedCount++;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (updatedCount === 0 && !opts.dryRun) {
|
|
1525
|
+
console.log("All integration files are already up to date.");
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
agentsCmd.command("mcp").description("Auto-detect agent tools and generate/update MCP config files").option("--tools <list>", "Comma-separated list of tools to generate configs for (opencode, gemini-cli, cursor-mcp, vscode-mcp)").option("--all", "Generate MCP configs for all supported tools").option("--detect", "Only detect agent tools without writing files").option("--dry-run", "Show actions without writing files").action(async (opts) => {
|
|
1529
|
+
const dir = program.opts().dir ? resolve(program.opts().dir) : process.cwd();
|
|
1530
|
+
const generator = new AgentGenerator();
|
|
1531
|
+
const allConfigs = generator.listMcpConfigs();
|
|
1532
|
+
let targets;
|
|
1533
|
+
if (opts.all) {
|
|
1534
|
+
targets = allConfigs;
|
|
1535
|
+
} else if (opts.tools) {
|
|
1536
|
+
const requested = opts.tools.split(",").map((s) => s.trim().toLowerCase());
|
|
1537
|
+
targets = allConfigs.filter((c) => requested.includes(c.name));
|
|
1538
|
+
} else {
|
|
1539
|
+
targets = generator.detectAgentTools(dir);
|
|
1540
|
+
}
|
|
1541
|
+
if (opts.detect) {
|
|
1542
|
+
if (targets.length === 0) {
|
|
1543
|
+
console.log("No agent tools detected in this project.");
|
|
1544
|
+
} else {
|
|
1545
|
+
console.log(`
|
|
1546
|
+
Detected agent tools (${targets.length}):`);
|
|
1547
|
+
for (const t of targets) {
|
|
1548
|
+
console.log(` \u2713 ${t.name.padEnd(15)} \u2192 ${t.filename}`);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
console.log(`
|
|
1552
|
+
All available targets:`);
|
|
1553
|
+
for (const c of allConfigs) {
|
|
1554
|
+
const detected = targets.includes(c);
|
|
1555
|
+
const icon = detected ? "\u2713" : "\xB7";
|
|
1556
|
+
console.log(` ${icon} ${c.name.padEnd(15)} \u2192 ${c.filename}`);
|
|
1557
|
+
}
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
if (targets.length === 0) {
|
|
1561
|
+
console.log("No agent tools detected. Use --all or --tools to specify targets.");
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
const actions = generator.generateMcpConfigs(targets, { dir });
|
|
1565
|
+
for (const action of actions) {
|
|
1566
|
+
if (action.status === "up_to_date") {
|
|
1567
|
+
console.log(` - ${action.filename}: Up to date.`);
|
|
1568
|
+
continue;
|
|
1569
|
+
}
|
|
1570
|
+
if (opts.dryRun) {
|
|
1571
|
+
console.log(`[DRY RUN] Would ${action.status} ${action.filename} (${action.tool})`);
|
|
1572
|
+
} else {
|
|
1573
|
+
generator.executeMcpConfig(action);
|
|
1574
|
+
const verb = action.status === "merge" ? "merged into" : action.status === "create" ? "created" : "updated";
|
|
1575
|
+
console.log(` \u2713 MCP config ${verb} ${action.filename} (${action.tool})`);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
const workspacesCmd = program.command("workspaces").description("Manage multi-repository workspace contexts");
|
|
1580
|
+
workspacesCmd.command("list").alias("show").description("List registered repositories and other discovered peer/submodule directories").action(async () => {
|
|
1581
|
+
const dir = resolveDir({}, program.opts());
|
|
1582
|
+
const { config } = await loadContext(dir);
|
|
1583
|
+
console.log("\nRegistered repositories:");
|
|
1584
|
+
const registeredPaths = /* @__PURE__ */ new Set();
|
|
1585
|
+
for (const r of config.repos) {
|
|
1586
|
+
const absPath = resolve(dir, r.path);
|
|
1587
|
+
registeredPaths.add(absPath);
|
|
1588
|
+
const fwStr = r.framework ? ` [${r.framework}]` : "";
|
|
1589
|
+
console.log(` - ${r.name.padEnd(15)} -> ${r.path} (active)${fwStr}`);
|
|
1590
|
+
}
|
|
1591
|
+
const submodules = WorkspaceManager.discoverSubmodules(dir);
|
|
1592
|
+
const uninitSubmodules = submodules.filter((s) => !registeredPaths.has(resolve(dir, s.path)));
|
|
1593
|
+
if (uninitSubmodules.length > 0) {
|
|
1594
|
+
console.log("\nDiscovered submodules:");
|
|
1595
|
+
for (const s of uninitSubmodules) {
|
|
1596
|
+
const status = s.isInitialized ? "available" : "uninitialized";
|
|
1597
|
+
console.log(` - ${s.name.padEnd(15)} -> ${s.path} (${status})`);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
const peers = WorkspaceManager.discoverPeerRepos(dir);
|
|
1601
|
+
const uninitPeers = peers.filter((p) => !registeredPaths.has(resolve(dir, p.path)));
|
|
1602
|
+
if (uninitPeers.length > 0) {
|
|
1603
|
+
console.log("\nDiscovered peer repositories:");
|
|
1604
|
+
for (const p of uninitPeers) {
|
|
1605
|
+
console.log(` - ${p.name.padEnd(15)} -> ${p.path} (available)`);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const wsFiles = readdirSync(dir).filter((f) => f.endsWith(".code-workspace"));
|
|
1609
|
+
if (wsFiles.length > 0) {
|
|
1610
|
+
console.log("\nDiscovered VS Code Workspace folders:");
|
|
1611
|
+
for (const f of wsFiles) {
|
|
1612
|
+
const wsFolderRepos = WorkspaceManager.discoverVSCodeWorkspace(join(dir, f), dir);
|
|
1613
|
+
const uninitWs = wsFolderRepos.filter((p) => !registeredPaths.has(resolve(dir, p.path)));
|
|
1614
|
+
for (const p of uninitWs) {
|
|
1615
|
+
console.log(` - ${p.name.padEnd(15)} -> ${p.path} (available)`);
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
console.log("");
|
|
1620
|
+
});
|
|
1621
|
+
workspacesCmd.command("add <path>").description("Register a repository path").option("--name <name>", "Repository name (defaults to folder name)").action(async (repoPath, opts) => {
|
|
1622
|
+
const dir = resolveDir({}, program.opts());
|
|
1623
|
+
const { config, store, graph } = await loadContext(dir);
|
|
1624
|
+
const absPath = resolve(dir, repoPath);
|
|
1625
|
+
if (!existsSync(absPath)) {
|
|
1626
|
+
console.error(`Error: Path ${repoPath} does not exist.`);
|
|
1627
|
+
process.exit(1);
|
|
1628
|
+
}
|
|
1629
|
+
if (!isGitRepo(absPath)) {
|
|
1630
|
+
console.error(`Error: Path ${repoPath} is not a git repository.`);
|
|
1631
|
+
process.exit(1);
|
|
1632
|
+
}
|
|
1633
|
+
const relPath = relative(dir, absPath);
|
|
1634
|
+
const name = opts.name || basename(absPath);
|
|
1635
|
+
if (config.repos.some((r) => r.name === name || r.path === relPath)) {
|
|
1636
|
+
console.log(`Repository already registered: ${name} (${relPath})`);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
config.addRepo(name, relPath);
|
|
1640
|
+
await config.save();
|
|
1641
|
+
console.log(`Registered repository: ${name} -> ${relPath}`);
|
|
1642
|
+
console.log("Running initial full scan for the new repository...");
|
|
1643
|
+
const onProgress = createProgressRenderer();
|
|
1644
|
+
const scanner = new Scanner(store, config, graph, onProgress);
|
|
1645
|
+
const result = await scanner.scanFull([name]);
|
|
1646
|
+
console.log(`Scanned ${result.filesScanned} files, ${result.symbolsFound} symbols, ${result.edgesFound} edges in ${result.durationMs}ms`);
|
|
1647
|
+
});
|
|
1648
|
+
workspacesCmd.command("remove <name>").description("Unregister a repository by name or path").action(async (name) => {
|
|
1649
|
+
const dir = resolveDir({}, program.opts());
|
|
1650
|
+
const { config, store } = await loadContext(dir);
|
|
1651
|
+
const repo = config.repos.find((r) => r.name === name || r.path === name);
|
|
1652
|
+
if (!repo) {
|
|
1653
|
+
console.error(`Error: Repository ${name} is not registered.`);
|
|
1654
|
+
process.exit(1);
|
|
1655
|
+
}
|
|
1656
|
+
const repoName = repo.name;
|
|
1657
|
+
config.removeRepo(name);
|
|
1658
|
+
await config.save();
|
|
1659
|
+
console.log(`Unregistered repository: ${repoName}`);
|
|
1660
|
+
console.log(`Cleaning up stored data for repository: ${repoName}...`);
|
|
1661
|
+
store.deleteRepo(repoName);
|
|
1662
|
+
console.log(`Done.`);
|
|
1663
|
+
});
|
|
1664
|
+
workspacesCmd.command("discover").description("Discover unregistered submodules, peer repos, and VS Code workspace folders (read-only)").action(async () => {
|
|
1665
|
+
const dir = resolveDir({}, program.opts());
|
|
1666
|
+
const { config } = await loadContext(dir);
|
|
1667
|
+
const registeredPaths = /* @__PURE__ */ new Set();
|
|
1668
|
+
for (const r of config.repos) {
|
|
1669
|
+
registeredPaths.add(resolve(dir, r.path));
|
|
1670
|
+
}
|
|
1671
|
+
let found = 0;
|
|
1672
|
+
const submodules = WorkspaceManager.discoverSubmodules(dir);
|
|
1673
|
+
const uninitSubs = submodules.filter((s) => !registeredPaths.has(resolve(dir, s.path)));
|
|
1674
|
+
if (uninitSubs.length > 0) {
|
|
1675
|
+
console.log("\nSubmodules:");
|
|
1676
|
+
for (const s of uninitSubs) {
|
|
1677
|
+
const status = s.isInitialized ? "available" : "uninitialized";
|
|
1678
|
+
console.log(` - ${s.name.padEnd(20)} -> ${s.path} (${status})`);
|
|
1679
|
+
}
|
|
1680
|
+
found += uninitSubs.length;
|
|
1681
|
+
}
|
|
1682
|
+
const peers = WorkspaceManager.discoverPeerRepos(dir);
|
|
1683
|
+
const uninitPeers = peers.filter((p) => !registeredPaths.has(resolve(dir, p.path)));
|
|
1684
|
+
if (uninitPeers.length > 0) {
|
|
1685
|
+
console.log("\nPeer repositories:");
|
|
1686
|
+
for (const p of uninitPeers) {
|
|
1687
|
+
console.log(` - ${p.name.padEnd(20)} -> ${p.path} (available)`);
|
|
1688
|
+
}
|
|
1689
|
+
found += uninitPeers.length;
|
|
1690
|
+
}
|
|
1691
|
+
const wsFiles = readdirSync(dir).filter((f) => f.endsWith(".code-workspace"));
|
|
1692
|
+
const vsEntries = [];
|
|
1693
|
+
for (const f of wsFiles) {
|
|
1694
|
+
const wsFolderRepos = WorkspaceManager.discoverVSCodeWorkspace(join(dir, f), dir);
|
|
1695
|
+
for (const p of wsFolderRepos) {
|
|
1696
|
+
if (!registeredPaths.has(resolve(dir, p.path))) {
|
|
1697
|
+
vsEntries.push({ name: p.name, path: p.path });
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
if (vsEntries.length > 0) {
|
|
1702
|
+
console.log("\nVS Code workspace folders:");
|
|
1703
|
+
for (const p of vsEntries) {
|
|
1704
|
+
console.log(` - ${p.name.padEnd(20)} -> ${p.path} (available)`);
|
|
1705
|
+
}
|
|
1706
|
+
found += vsEntries.length;
|
|
1707
|
+
}
|
|
1708
|
+
if (found === 0) {
|
|
1709
|
+
console.log("No unregistered repositories discovered.");
|
|
1710
|
+
} else {
|
|
1711
|
+
console.log(`
|
|
1712
|
+
${found} unregistered repositor${found === 1 ? "y" : "ies"} discovered. Use \`mapx workspaces add <path>\` to register.`);
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
workspacesCmd.command("sync").description("Sync all discovered submodules, peer repos, and VS Code workspace folders").action(async () => {
|
|
1716
|
+
const dir = resolveDir({}, program.opts());
|
|
1717
|
+
const { config, store, graph } = await loadContext(dir);
|
|
1718
|
+
const registeredPaths = /* @__PURE__ */ new Set();
|
|
1719
|
+
for (const r of config.repos) {
|
|
1720
|
+
registeredPaths.add(resolve(dir, r.path));
|
|
1721
|
+
}
|
|
1722
|
+
const toAdd = [];
|
|
1723
|
+
const submodules = WorkspaceManager.discoverSubmodules(dir);
|
|
1724
|
+
for (const s of submodules) {
|
|
1725
|
+
if (s.isInitialized) {
|
|
1726
|
+
const abs = resolve(dir, s.path);
|
|
1727
|
+
if (!registeredPaths.has(abs)) {
|
|
1728
|
+
toAdd.push({ name: s.name, path: s.path });
|
|
1729
|
+
registeredPaths.add(abs);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
const peers = WorkspaceManager.discoverPeerRepos(dir);
|
|
1734
|
+
for (const p of peers) {
|
|
1735
|
+
const abs = resolve(dir, p.path);
|
|
1736
|
+
if (!registeredPaths.has(abs)) {
|
|
1737
|
+
toAdd.push({ name: p.name, path: p.path });
|
|
1738
|
+
registeredPaths.add(abs);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
const wsFiles = readdirSync(dir).filter((f) => f.endsWith(".code-workspace"));
|
|
1742
|
+
for (const f of wsFiles) {
|
|
1743
|
+
const wsFolderRepos = WorkspaceManager.discoverVSCodeWorkspace(join(dir, f), dir);
|
|
1744
|
+
for (const p of wsFolderRepos) {
|
|
1745
|
+
const abs = resolve(dir, p.path);
|
|
1746
|
+
if (!registeredPaths.has(abs)) {
|
|
1747
|
+
toAdd.push({ name: p.name, path: p.path });
|
|
1748
|
+
registeredPaths.add(abs);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (toAdd.length === 0) {
|
|
1753
|
+
console.log("No new repositories discovered to sync.");
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
console.log(`Syncing ${toAdd.length} newly discovered repositories:`);
|
|
1757
|
+
const scanner = new Scanner(store, config, graph, createProgressRenderer());
|
|
1758
|
+
for (const item of toAdd) {
|
|
1759
|
+
config.addRepo(item.name, item.path);
|
|
1760
|
+
console.log(` + Registered: ${item.name} -> ${item.path}`);
|
|
1761
|
+
}
|
|
1762
|
+
await config.save();
|
|
1763
|
+
console.log("\nRunning initial full scan for new repositories...");
|
|
1764
|
+
const newNames = toAdd.map((item) => item.name);
|
|
1765
|
+
const result = await scanner.scanFull(newNames);
|
|
1766
|
+
console.log(`Scanned ${result.filesScanned} files, ${result.symbolsFound} symbols, ${result.edgesFound} edges in ${result.durationMs}ms`);
|
|
1767
|
+
});
|
|
1768
|
+
return program;
|
|
1769
|
+
}
|
|
1770
|
+
async function loadContext(dir) {
|
|
1771
|
+
const configPath = resolve(dir, ".mapx", "config.json");
|
|
1772
|
+
if (!existsSync(configPath)) {
|
|
1773
|
+
console.error(`MapxGraph not initialized in ${dir}. Run \`mapx init ${dir}\` first.`);
|
|
1774
|
+
process.exit(1);
|
|
1775
|
+
}
|
|
1776
|
+
const config = await Config.load(dir);
|
|
1777
|
+
const dbPath = resolve(dir, ".mapx", "mapx.db");
|
|
1778
|
+
const store = new Store(dbPath);
|
|
1779
|
+
const closeStore = () => {
|
|
1780
|
+
try {
|
|
1781
|
+
store.close();
|
|
1782
|
+
} catch {
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
process.once("exit", closeStore);
|
|
1786
|
+
process.once("SIGINT", () => {
|
|
1787
|
+
closeStore();
|
|
1788
|
+
process.exit(130);
|
|
1789
|
+
});
|
|
1790
|
+
process.once("SIGTERM", () => {
|
|
1791
|
+
closeStore();
|
|
1792
|
+
process.exit(143);
|
|
1793
|
+
});
|
|
1794
|
+
const graph = new MapxGraph(config.repo.name);
|
|
1795
|
+
const files = store.getAllFiles();
|
|
1796
|
+
for (const file of files) {
|
|
1797
|
+
graph.addFileNode(
|
|
1798
|
+
file.path,
|
|
1799
|
+
file.language,
|
|
1800
|
+
file.size_bytes,
|
|
1801
|
+
file.lines
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
const symbols = store.getAllSymbols();
|
|
1805
|
+
for (const sym of symbols) {
|
|
1806
|
+
graph.addSymbolNode(
|
|
1807
|
+
sym.name,
|
|
1808
|
+
sym.file_path,
|
|
1809
|
+
sym.name,
|
|
1810
|
+
sym.kind,
|
|
1811
|
+
sym.start_line,
|
|
1812
|
+
sym.end_line,
|
|
1813
|
+
sym.scope
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
const edges = store.getAllEdges();
|
|
1817
|
+
for (const edge of edges) {
|
|
1818
|
+
graph.addDependencyEdge({
|
|
1819
|
+
sourceFile: edge.source_file,
|
|
1820
|
+
targetFile: edge.target_file,
|
|
1821
|
+
sourceSymbol: edge.source_symbol,
|
|
1822
|
+
targetSymbol: edge.target_symbol,
|
|
1823
|
+
edgeType: edge.edge_type,
|
|
1824
|
+
repo: edge.repo,
|
|
1825
|
+
weight: edge.weight,
|
|
1826
|
+
verifiability: edge.verifiability,
|
|
1827
|
+
targetRepo: edge.target_repo
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
return { config, store, graph };
|
|
1831
|
+
}
|
|
1832
|
+
export {
|
|
1833
|
+
buildCLI,
|
|
1834
|
+
loadContext
|
|
1835
|
+
};
|