@mishasinitcyn/betterrank 0.1.2 → 0.1.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/package.json +1 -1
- package/src/cache.js +5 -0
- package/src/cli.js +14 -0
- package/src/server.js +299 -0
- package/src/ui.html +1249 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mishasinitcyn/betterrank",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Structural code index with PageRank-ranked repo maps, symbol search, call-graph queries, and dependency analysis. Built on tree-sitter and graphology.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/cache.js
CHANGED
|
@@ -50,6 +50,11 @@ const IGNORE_PATTERNS = [
|
|
|
50
50
|
'**/Pods/**',
|
|
51
51
|
'**/*.xcframework/**',
|
|
52
52
|
|
|
53
|
+
// UI component libraries (shadcn, etc.) — high fan-in but rarely investigation targets.
|
|
54
|
+
// To re-include, add "!**/components/ui/**" in .code-index/config.json ignore list,
|
|
55
|
+
// or pass --ignore '!**/components/ui/**' on the CLI.
|
|
56
|
+
'**/components/ui/**',
|
|
57
|
+
|
|
53
58
|
// Scratch / temp
|
|
54
59
|
'tmp/**',
|
|
55
60
|
];
|
package/src/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ const USAGE = `
|
|
|
10
10
|
betterrank <command> [options]
|
|
11
11
|
|
|
12
12
|
Commands:
|
|
13
|
+
ui [--port N] Launch web UI (default port: 3333)
|
|
13
14
|
map [--focus file1,file2] Repo map (ranked by PageRank)
|
|
14
15
|
search <query> [--kind type] Substring search on symbol names + signatures (ranked by PageRank)
|
|
15
16
|
structure [--depth N] File tree with symbol counts (default depth: ${DEFAULT_DEPTH})
|
|
@@ -37,6 +38,19 @@ async function main() {
|
|
|
37
38
|
|
|
38
39
|
const command = args[0];
|
|
39
40
|
const flags = parseFlags(args.slice(1));
|
|
41
|
+
|
|
42
|
+
// UI command doesn't need --root or a CodeIndex instance
|
|
43
|
+
if (command === 'ui') {
|
|
44
|
+
const { startServer } = await import('./server.js');
|
|
45
|
+
const port = parseInt(flags.port || '3333', 10);
|
|
46
|
+
startServer(port);
|
|
47
|
+
// Open browser
|
|
48
|
+
const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
49
|
+
const { exec } = await import('child_process');
|
|
50
|
+
exec(`${opener} http://localhost:${port}`);
|
|
51
|
+
return; // Keep process alive (server is listening)
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
const projectRoot = resolve(flags.root || process.cwd());
|
|
41
55
|
if (!flags.root) {
|
|
42
56
|
process.stderr.write(`⚠ No --root specified, using cwd: ${projectRoot}\n`);
|
package/src/server.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
import { CodeIndex } from './index.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
let currentIndex = null;
|
|
10
|
+
let currentRoot = null;
|
|
11
|
+
let currentStats = null;
|
|
12
|
+
|
|
13
|
+
function json(res, data, status = 200) {
|
|
14
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
15
|
+
res.end(JSON.stringify(data));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function error(res, msg, status = 400) {
|
|
19
|
+
json(res, { error: msg }, status);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function params(url) {
|
|
23
|
+
const u = new URL(url, 'http://localhost');
|
|
24
|
+
const get = (k, fallback) => {
|
|
25
|
+
const v = u.searchParams.get(k);
|
|
26
|
+
return v === null ? fallback : v;
|
|
27
|
+
};
|
|
28
|
+
const getInt = (k, fallback) => {
|
|
29
|
+
const v = u.searchParams.get(k);
|
|
30
|
+
return v === null ? fallback : parseInt(v, 10);
|
|
31
|
+
};
|
|
32
|
+
return { get, getInt, path: u.pathname };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readBody(req) {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
38
|
+
return JSON.parse(Buffer.concat(chunks).toString());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requireIndex(res) {
|
|
42
|
+
if (!currentIndex) {
|
|
43
|
+
error(res, 'No repo indexed. POST /api/index first.', 400);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const routes = {
|
|
50
|
+
|
|
51
|
+
'GET /api/open': async (req, res) => {
|
|
52
|
+
if (!currentRoot) return error(res, 'No repo indexed');
|
|
53
|
+
const p = params(req.url);
|
|
54
|
+
const file = p.get('file', '');
|
|
55
|
+
const line = p.get('line', '');
|
|
56
|
+
const ide = p.get('ide', 'cursor');
|
|
57
|
+
if (!file) return error(res, 'file is required');
|
|
58
|
+
|
|
59
|
+
const fullPath = join(currentRoot, file);
|
|
60
|
+
const target = line ? `${fullPath}:${line}` : fullPath;
|
|
61
|
+
|
|
62
|
+
const { exec: execCb } = await import('child_process');
|
|
63
|
+
const cliMap = {
|
|
64
|
+
cursor: 'cursor',
|
|
65
|
+
vscode: 'code',
|
|
66
|
+
'vscode-insiders': 'code-insiders',
|
|
67
|
+
};
|
|
68
|
+
const cli = cliMap[ide];
|
|
69
|
+
|
|
70
|
+
if (cli) {
|
|
71
|
+
execCb(`${cli} --goto "${target}"`, (err) => {
|
|
72
|
+
if (err) {
|
|
73
|
+
// CLI not found, fall back to OS open
|
|
74
|
+
execCb(`open "${fullPath}"`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
execCb(`open "${fullPath}"`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
json(res, { ok: true });
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
'POST /api/browse': async (_req, res) => {
|
|
85
|
+
const { exec } = await import('child_process');
|
|
86
|
+
const { promisify } = await import('util');
|
|
87
|
+
const execAsync = promisify(exec);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
let cmd;
|
|
91
|
+
switch (process.platform) {
|
|
92
|
+
case 'darwin':
|
|
93
|
+
cmd = `osascript -e 'POSIX path of (choose folder with prompt "Select a repository folder")'`;
|
|
94
|
+
break;
|
|
95
|
+
case 'win32':
|
|
96
|
+
cmd = `powershell -NoProfile -Command "Add-Type -AssemblyName System.Windows.Forms; $d = New-Object System.Windows.Forms.FolderBrowserDialog; $d.Description = 'Select a repository folder'; if ($d.ShowDialog() -eq 'OK') { $d.SelectedPath }"`;
|
|
97
|
+
break;
|
|
98
|
+
default:
|
|
99
|
+
cmd = `zenity --file-selection --directory --title="Select a repository folder" 2>/dev/null || kdialog --getexistingdirectory ~ 2>/dev/null`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { stdout } = await execAsync(cmd, { timeout: 120000 });
|
|
103
|
+
const folderPath = stdout.trim().replace(/\/$/, '');
|
|
104
|
+
if (!folderPath) return error(res, 'No folder selected');
|
|
105
|
+
json(res, { path: folderPath });
|
|
106
|
+
} catch {
|
|
107
|
+
error(res, 'Cancelled', 400);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
'POST /api/index': async (req, res) => {
|
|
112
|
+
const body = await readBody(req);
|
|
113
|
+
const root = body.root;
|
|
114
|
+
if (!root) return error(res, 'root is required');
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
currentIndex = new CodeIndex(root);
|
|
118
|
+
const t0 = Date.now();
|
|
119
|
+
await currentIndex.reindex();
|
|
120
|
+
const elapsed = Date.now() - t0;
|
|
121
|
+
currentRoot = root;
|
|
122
|
+
currentStats = await currentIndex.stats();
|
|
123
|
+
json(res, { root, stats: currentStats, elapsed });
|
|
124
|
+
} catch (e) {
|
|
125
|
+
currentIndex = null;
|
|
126
|
+
currentRoot = null;
|
|
127
|
+
error(res, e.message, 500);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
'GET /api/stats': async (_req, res) => {
|
|
132
|
+
if (!requireIndex(res)) return;
|
|
133
|
+
const stats = await currentIndex.stats();
|
|
134
|
+
json(res, { root: currentRoot, stats });
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
'GET /api/map': async (req, res) => {
|
|
138
|
+
if (!requireIndex(res)) return;
|
|
139
|
+
const p = params(req.url);
|
|
140
|
+
const focus = p.get('focus', '');
|
|
141
|
+
const focusFiles = focus ? focus.split(',') : [];
|
|
142
|
+
const result = await currentIndex.map({
|
|
143
|
+
focusFiles,
|
|
144
|
+
offset: p.getInt('offset', undefined),
|
|
145
|
+
limit: p.getInt('limit', 50),
|
|
146
|
+
});
|
|
147
|
+
json(res, result);
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
'GET /api/search': async (req, res) => {
|
|
151
|
+
if (!requireIndex(res)) return;
|
|
152
|
+
const p = params(req.url);
|
|
153
|
+
const query = p.get('q', '');
|
|
154
|
+
if (!query) return error(res, 'q is required');
|
|
155
|
+
const results = await currentIndex.search({
|
|
156
|
+
query,
|
|
157
|
+
kind: p.get('kind', undefined),
|
|
158
|
+
offset: p.getInt('offset', undefined),
|
|
159
|
+
limit: p.getInt('limit', 20),
|
|
160
|
+
});
|
|
161
|
+
// Get total count for pagination
|
|
162
|
+
const total = await currentIndex.search({
|
|
163
|
+
query,
|
|
164
|
+
kind: p.get('kind', undefined),
|
|
165
|
+
count: true,
|
|
166
|
+
});
|
|
167
|
+
json(res, { results, total: total.total });
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
'GET /api/symbols': async (req, res) => {
|
|
171
|
+
if (!requireIndex(res)) return;
|
|
172
|
+
const p = params(req.url);
|
|
173
|
+
const results = await currentIndex.symbols({
|
|
174
|
+
file: p.get('file', undefined),
|
|
175
|
+
kind: p.get('kind', undefined),
|
|
176
|
+
offset: p.getInt('offset', undefined),
|
|
177
|
+
limit: p.getInt('limit', 20),
|
|
178
|
+
});
|
|
179
|
+
const total = await currentIndex.symbols({
|
|
180
|
+
file: p.get('file', undefined),
|
|
181
|
+
kind: p.get('kind', undefined),
|
|
182
|
+
count: true,
|
|
183
|
+
});
|
|
184
|
+
json(res, { results, total: total.total });
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
'GET /api/callers': async (req, res) => {
|
|
188
|
+
if (!requireIndex(res)) return;
|
|
189
|
+
const p = params(req.url);
|
|
190
|
+
const symbol = p.get('symbol', '');
|
|
191
|
+
if (!symbol) return error(res, 'symbol is required');
|
|
192
|
+
const results = await currentIndex.callers({
|
|
193
|
+
symbol,
|
|
194
|
+
file: p.get('file', undefined),
|
|
195
|
+
offset: p.getInt('offset', undefined),
|
|
196
|
+
limit: p.getInt('limit', 20),
|
|
197
|
+
});
|
|
198
|
+
const total = await currentIndex.callers({
|
|
199
|
+
symbol,
|
|
200
|
+
file: p.get('file', undefined),
|
|
201
|
+
count: true,
|
|
202
|
+
});
|
|
203
|
+
json(res, { results, total: total.total });
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
'GET /api/dependents': async (req, res) => {
|
|
207
|
+
if (!requireIndex(res)) return;
|
|
208
|
+
const p = params(req.url);
|
|
209
|
+
const file = p.get('file', '');
|
|
210
|
+
if (!file) return error(res, 'file is required');
|
|
211
|
+
const results = await currentIndex.dependents({
|
|
212
|
+
file,
|
|
213
|
+
offset: p.getInt('offset', undefined),
|
|
214
|
+
limit: p.getInt('limit', 20),
|
|
215
|
+
});
|
|
216
|
+
const total = await currentIndex.dependents({ file, count: true });
|
|
217
|
+
json(res, { results, total: total.total });
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
'GET /api/deps': async (req, res) => {
|
|
221
|
+
if (!requireIndex(res)) return;
|
|
222
|
+
const p = params(req.url);
|
|
223
|
+
const file = p.get('file', '');
|
|
224
|
+
if (!file) return error(res, 'file is required');
|
|
225
|
+
const results = await currentIndex.dependencies({
|
|
226
|
+
file,
|
|
227
|
+
offset: p.getInt('offset', undefined),
|
|
228
|
+
limit: p.getInt('limit', 20),
|
|
229
|
+
});
|
|
230
|
+
const total = await currentIndex.dependencies({ file, count: true });
|
|
231
|
+
json(res, { results, total: total.total });
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
'GET /api/neighborhood': async (req, res) => {
|
|
235
|
+
if (!requireIndex(res)) return;
|
|
236
|
+
const p = params(req.url);
|
|
237
|
+
const file = p.get('file', '');
|
|
238
|
+
if (!file) return error(res, 'file is required');
|
|
239
|
+
const result = await currentIndex.neighborhood({
|
|
240
|
+
file,
|
|
241
|
+
hops: p.getInt('hops', 2),
|
|
242
|
+
maxFiles: p.getInt('maxFiles', 15),
|
|
243
|
+
offset: p.getInt('offset', undefined),
|
|
244
|
+
limit: p.getInt('limit', undefined),
|
|
245
|
+
});
|
|
246
|
+
json(res, result);
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
'GET /api/structure': async (req, res) => {
|
|
250
|
+
if (!requireIndex(res)) return;
|
|
251
|
+
const p = params(req.url);
|
|
252
|
+
const result = await currentIndex.structure({
|
|
253
|
+
depth: p.getInt('depth', 3),
|
|
254
|
+
});
|
|
255
|
+
json(res, { content: result });
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export function startServer(port = 3333) {
|
|
260
|
+
const server = createServer(async (req, res) => {
|
|
261
|
+
// CORS
|
|
262
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
263
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
264
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
265
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
266
|
+
|
|
267
|
+
// Serve UI
|
|
268
|
+
if (req.url === '/' || req.url === '/index.html') {
|
|
269
|
+
try {
|
|
270
|
+
const html = await readFile(join(__dirname, 'ui.html'), 'utf-8');
|
|
271
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
272
|
+
res.end(html);
|
|
273
|
+
} catch {
|
|
274
|
+
res.writeHead(500); res.end('Failed to load UI');
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// API routes
|
|
280
|
+
const routeKey = `${req.method} ${params(req.url).path}`;
|
|
281
|
+
const handler = routes[routeKey];
|
|
282
|
+
if (handler) {
|
|
283
|
+
try {
|
|
284
|
+
await handler(req, res);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
error(res, e.message, 500);
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
res.writeHead(404);
|
|
290
|
+
res.end('Not found');
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
server.listen(port, () => {
|
|
295
|
+
console.log(`betterrank ui running at http://localhost:${port}`);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
return server;
|
|
299
|
+
}
|