@route-intelligence/cli 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry shim: runs dist/cli.js and auto-builds in the monorepo when dist/ is missing.
4
+ */
5
+ import { execSync, spawnSync } from 'node:child_process';
6
+ import { existsSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const packageDir = join(dirname(fileURLToPath(import.meta.url)), '..');
11
+ const distCli = join(packageDir, 'dist', 'cli.js');
12
+ const monorepoRoot = join(packageDir, '..', '..');
13
+ const buildScript = join(monorepoRoot, 'scripts', 'build-packages.mjs');
14
+
15
+ if (!existsSync(distCli)) {
16
+ if (existsSync(buildScript)) {
17
+ console.log('[route-intelligence] Building workspace packages (first run)…');
18
+ execSync(`node "${buildScript}"`, { cwd: monorepoRoot, stdio: 'inherit' });
19
+ }
20
+
21
+ if (!existsSync(distCli)) {
22
+ console.error(
23
+ '[route-intelligence] CLI is not built. Run `npm run build` in the repo root, then retry.',
24
+ );
25
+ process.exit(1);
26
+ }
27
+ }
28
+
29
+ const result = spawnSync(process.execPath, [distCli, ...process.argv.slice(2)], {
30
+ stdio: 'inherit',
31
+ });
32
+
33
+ process.exit(result.status ?? 1);
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { mkdirSync, writeFileSync } from "fs";
5
+ import { createServer } from "http";
6
+ import { join, resolve } from "path";
7
+ import {
8
+ createAnalyzer,
9
+ exportDot,
10
+ exportHtml,
11
+ exportJson,
12
+ exportMarkdown,
13
+ exportMermaid,
14
+ exportPlantUML
15
+ } from "@route-intelligence/core";
16
+ import { NextPlugin } from "@route-intelligence/next";
17
+ import chalk from "chalk";
18
+ import { Command } from "commander";
19
+ import ora from "ora";
20
+ var program = new Command();
21
+ program.name("route-intelligence").description("Routing intelligence platform for React applications").version("0.1.0");
22
+ function getDefaultConfig(root) {
23
+ return {
24
+ root,
25
+ plugins: [NextPlugin()],
26
+ include: ["app/**", "pages/**", "src/**", "middleware.ts"],
27
+ exclude: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/*.test.*", "**/*.spec.*"],
28
+ cache: { enabled: true, directory: ".route-intelligence" },
29
+ output: { formats: ["json", "mermaid"], directory: "ri-output" }
30
+ };
31
+ }
32
+ async function runAnalysis(root) {
33
+ const spinner = ora("Analyzing routes...").start();
34
+ const config = getDefaultConfig(resolve(root));
35
+ const analyzer = createAnalyzer(config);
36
+ const result = await analyzer.analyze();
37
+ const graph = result.graph;
38
+ spinner.succeed(
39
+ chalk.green(
40
+ `Found ${result.metadata.totalRoutes} routes, ${result.metadata.totalLayouts} layouts`
41
+ )
42
+ );
43
+ return { ...result, graph };
44
+ }
45
+ program.command("analyze").description("Analyze project routing").option("-r, --root <path>", "Project root", ".").option(
46
+ "-f, --format <format>",
47
+ "Output format (json|mermaid|plantuml|dot|html|markdown)",
48
+ "json"
49
+ ).option("-o, --out <path>", "Output directory", "ri-output").action(async (opts) => {
50
+ const result = await runAnalysis(opts.root);
51
+ const outDir = resolve(opts.root, opts.out);
52
+ mkdirSync(outDir, { recursive: true });
53
+ const exporters = {
54
+ json: () => exportJson(result.graph, resolve(opts.root)),
55
+ mermaid: () => exportMermaid(result.graph),
56
+ plantuml: () => exportPlantUML(result.graph),
57
+ dot: () => exportDot(result.graph),
58
+ html: () => exportHtml(result.graph, resolve(opts.root)),
59
+ markdown: () => exportMarkdown(result.graph, resolve(opts.root))
60
+ };
61
+ const exporter = exporters[opts.format];
62
+ if (!exporter) {
63
+ console.error(chalk.red(`Unknown format: ${opts.format}`));
64
+ process.exit(1);
65
+ }
66
+ const ext = opts.format === "json" ? "json" : opts.format === "markdown" ? "md" : opts.format;
67
+ const outPath = join(outDir, `graph.${ext}`);
68
+ writeFileSync(outPath, exporter());
69
+ console.log(chalk.blue(`Written to ${outPath}`));
70
+ if (result.diagnostics.length > 0) {
71
+ console.log(chalk.yellow(`
72
+ ${result.diagnostics.length} diagnostics:`));
73
+ for (const d of result.diagnostics.slice(0, 10)) {
74
+ console.log(` [${d.severity}] ${d.ruleId}: ${d.message}`);
75
+ }
76
+ }
77
+ });
78
+ program.command("graph").description("Launch interactive graph browser").option("-r, --root <path>", "Project root", ".").option("-p, --port <port>", "Port", "3001").option("--host <host>", "Host", "localhost").action(async (opts) => {
79
+ const result = await runAnalysis(opts.root);
80
+ const outDir = resolve(opts.root, "ri-output");
81
+ mkdirSync(outDir, { recursive: true });
82
+ const graphJson = exportJson(result.graph, resolve(opts.root));
83
+ writeFileSync(join(outDir, "graph.json"), graphJson);
84
+ const port = Number.parseInt(opts.port, 10);
85
+ const server = createServer((req, res) => {
86
+ if (req.url === "/" || req.url === "/index.html") {
87
+ res.writeHead(200, { "Content-Type": "text/html" });
88
+ res.end(getVisualizerHtml());
89
+ } else if (req.url === "/graph.json") {
90
+ res.writeHead(200, { "Content-Type": "application/json" });
91
+ res.end(graphJson);
92
+ } else {
93
+ res.writeHead(404);
94
+ res.end("Not found");
95
+ }
96
+ });
97
+ server.listen(port, opts.host, () => {
98
+ console.log(chalk.green(`Graph server running at http://${opts.host}:${port}`));
99
+ });
100
+ });
101
+ program.command("doctor").description("Run static analysis health check").option("-r, --root <path>", "Project root", ".").option("--strict", "Exit with error on warnings").option("-f, --format <format>", "Output format (text|json)", "text").action(async (opts) => {
102
+ const result = await runAnalysis(opts.root);
103
+ const errors = result.diagnostics.filter((d) => d.severity === "error");
104
+ const warnings = result.diagnostics.filter((d) => d.severity === "warning");
105
+ if (opts.format === "json") {
106
+ console.log(JSON.stringify(result.diagnostics, null, 2));
107
+ } else {
108
+ console.log(chalk.bold("\nRoute Intelligence Doctor\n"));
109
+ console.log(`Errors: ${errors.length}, Warnings: ${warnings.length}
110
+ `);
111
+ for (const d of result.diagnostics) {
112
+ const color = d.severity === "error" ? chalk.red : chalk.yellow;
113
+ console.log(color(`[${d.severity}] ${d.ruleId}: ${d.message}`));
114
+ }
115
+ }
116
+ if (errors.length > 0 || opts.strict && warnings.length > 0) {
117
+ process.exit(1);
118
+ }
119
+ });
120
+ program.command("docs").description("Generate route documentation").option("-r, --root <path>", "Project root", ".").option("-f, --format <format>", "Format (markdown|html)", "markdown").option("-o, --out <path>", "Output directory", "docs/routes").action(async (opts) => {
121
+ const result = await runAnalysis(opts.root);
122
+ const outDir = resolve(opts.root, opts.out);
123
+ mkdirSync(outDir, { recursive: true });
124
+ const content = opts.format === "html" ? exportHtml(result.graph, resolve(opts.root)) : exportMarkdown(result.graph, resolve(opts.root));
125
+ const ext = opts.format === "html" ? "html" : "md";
126
+ writeFileSync(join(outDir, `routes.${ext}`), content);
127
+ console.log(chalk.green(`Documentation written to ${outDir}/routes.${ext}`));
128
+ });
129
+ program.command("export").description("Export route graph").option("-r, --root <path>", "Project root", ".").option("-f, --format <format>", "Format", "json").option("-o, --out <path>", "Output directory", "ri-export").action(async (opts) => {
130
+ const result = await runAnalysis(opts.root);
131
+ const outDir = resolve(opts.root, opts.out);
132
+ mkdirSync(outDir, { recursive: true });
133
+ const exporters = {
134
+ json: () => exportJson(result.graph, resolve(opts.root)),
135
+ mermaid: () => exportMermaid(result.graph),
136
+ plantuml: () => exportPlantUML(result.graph),
137
+ dot: () => exportDot(result.graph)
138
+ };
139
+ const content = exporters[opts.format]?.() ?? exportJson(result.graph, resolve(opts.root));
140
+ writeFileSync(join(outDir, `graph.${opts.format}`), content);
141
+ console.log(chalk.green(`Exported to ${outDir}/graph.${opts.format}`));
142
+ });
143
+ program.command("watch").description("Watch for changes and incrementally update graph").option("-r, --root <path>", "Project root", ".").option("-p, --port <port>", "Graph server port", "3001").action(async (opts) => {
144
+ const config = getDefaultConfig(resolve(opts.root));
145
+ const analyzer = createAnalyzer(config);
146
+ const watcher = analyzer.watch();
147
+ watcher.on("update", (patch) => {
148
+ console.log(
149
+ chalk.blue(
150
+ `Graph updated: +${patch.addedNodes.length} nodes, -${patch.removedNodeIds.length} nodes`
151
+ )
152
+ );
153
+ });
154
+ watcher.on("error", (err) => {
155
+ console.error(chalk.red(err.message));
156
+ });
157
+ console.log(chalk.green("Watching for route changes..."));
158
+ process.on("SIGINT", async () => {
159
+ await watcher.stop();
160
+ process.exit(0);
161
+ });
162
+ });
163
+ function getVisualizerHtml() {
164
+ return `<!DOCTYPE html>
165
+ <html lang="en">
166
+ <head>
167
+ <meta charset="UTF-8">
168
+ <title>Route Intelligence Graph</title>
169
+ <style>
170
+ * { box-sizing: border-box; margin: 0; padding: 0; }
171
+ body { font-family: system-ui, sans-serif; background: #0a0a0a; color: #fafafa; height: 100vh; display: flex; flex-direction: column; }
172
+ header { padding: 1rem; border-bottom: 1px solid #333; display: flex; gap: 1rem; align-items: center; }
173
+ input { flex: 1; padding: 0.5rem; background: #1a1a1a; border: 1px solid #333; color: #fff; border-radius: 4px; }
174
+ #graph { flex: 1; overflow: auto; padding: 1rem; }
175
+ .node { background: #1e3a5f; border: 1px solid #3b82f6; border-radius: 8px; padding: 0.75rem; margin: 0.5rem; display: inline-block; cursor: pointer; }
176
+ .node.dead { border-color: #ef4444; opacity: 0.7; }
177
+ .node-type { font-size: 0.75rem; color: #888; }
178
+ .node-path { font-weight: 600; }
179
+ #detail { position: fixed; right: 0; top: 0; width: 300px; height: 100%; background: #111; border-left: 1px solid #333; padding: 1rem; overflow: auto; transform: translateX(100%); transition: transform 0.2s; }
180
+ #detail.open { transform: translateX(0); }
181
+ .overlay-toggle { display: flex; gap: 0.5rem; flex-wrap: wrap; }
182
+ .overlay-toggle label { font-size: 0.875rem; }
183
+ </style>
184
+ </head>
185
+ <body>
186
+ <header>
187
+ <h1>Route Intelligence</h1>
188
+ <input type="search" id="search" placeholder="Search routes...">
189
+ <div class="overlay-toggle">
190
+ <label><input type="checkbox" id="show-dead"> Dead</label>
191
+ <label><input type="checkbox" id="show-api" checked> API</label>
192
+ <label><input type="checkbox" id="show-dynamic" checked> Dynamic</label>
193
+ </div>
194
+ </header>
195
+ <div id="graph"></div>
196
+ <div id="detail"></div>
197
+ <script>
198
+ let graphData = null;
199
+ fetch('/graph.json').then(r => r.json()).then(data => { graphData = data; render(); });
200
+ function render() {
201
+ const container = document.getElementById('graph');
202
+ const search = document.getElementById('search').value.toLowerCase();
203
+ const showDead = document.getElementById('show-dead').checked;
204
+ container.innerHTML = '';
205
+ for (const node of graphData.nodes) {
206
+ if (node.attributes.type !== 'route' && node.attributes.type !== 'layout' && node.attributes.type !== 'api-route') continue;
207
+ if (!showDead && node.attributes.isDead) continue;
208
+ if (search && !node.attributes.path.toLowerCase().includes(search)) continue;
209
+ const el = document.createElement('div');
210
+ el.className = 'node' + (node.attributes.isDead ? ' dead' : '');
211
+ el.innerHTML = '<div class="node-type">' + node.attributes.type + '</div><div class="node-path">' + node.attributes.path + '</div>';
212
+ el.onclick = () => showDetail(node);
213
+ container.appendChild(el);
214
+ }
215
+ }
216
+ function showDetail(node) {
217
+ const detail = document.getElementById('detail');
218
+ detail.className = 'open';
219
+ detail.innerHTML = '<h2>' + node.attributes.path + '</h2><p>Type: ' + node.attributes.type + '</p><p>File: ' + node.attributes.filePath + '</p><p>Depth: ' + node.attributes.depth + '</p>';
220
+ }
221
+ document.getElementById('search').oninput = render;
222
+ document.getElementById('show-dead').onchange = render;
223
+ </script>
224
+ </body>
225
+ </html>`;
226
+ }
227
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@route-intelligence/cli",
3
+ "version": "2.1.0",
4
+ "description": "CLI for Route Intelligence",
5
+ "type": "module",
6
+ "bin": {
7
+ "route-intelligence": "./bin/route-intelligence.mjs",
8
+ "ri": "./bin/route-intelligence.mjs"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/cli.d.ts",
13
+ "development": "./src/cli.ts",
14
+ "import": "./dist/cli.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "bin"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsup src/cli.ts --format esm --dts --clean",
23
+ "dev": "tsup src/cli.ts --format esm --dts --watch",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "dependencies": {
27
+ "@route-intelligence/core": "*",
28
+ "@route-intelligence/next": "*",
29
+ "@route-intelligence/shared": "*",
30
+ "chalk": "^5.4.1",
31
+ "commander": "^14.0.0",
32
+ "execa": "^9.6.0",
33
+ "ora": "^8.2.0"
34
+ },
35
+ "devDependencies": {
36
+ "@route-intelligence/tsconfig": "*",
37
+ "@types/node": "^22.15.32",
38
+ "tsup": "^8.4.0",
39
+ "typescript": "^5.8.3"
40
+ },
41
+ "engines": {
42
+ "node": ">=22"
43
+ },
44
+ "license": "MIT",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }