@zhn2605/topo 0.1.0 → 0.3.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.
- package/README.md +101 -76
- package/dist/grammars/tree-sitter-cpp.wasm +0 -0
- package/dist/topo.js +1739 -415
- package/dist/web/assets/answer-3GChwEmV.js +2 -0
- package/dist/web/assets/{elk.bundled-DHL5Qd5-.js → elk.bundled-Bx4dyQBB.js} +1 -1
- package/dist/web/assets/index-CX9S_hg9.js +23 -0
- package/dist/web/assets/index-D-MaOEzL.css +1 -0
- package/dist/web/index.html +2 -2
- package/dist/web/snapshot.html +44 -37
- package/package.json +2 -2
- package/dist/web/assets/index-Bx25CC1b.js +0 -17
- package/dist/web/assets/index-CJ5z47qU.css +0 -1
package/dist/topo.js
CHANGED
|
@@ -315,6 +315,9 @@ function findEntryPoints(graph, rootDir) {
|
|
|
315
315
|
if (n.language === "rust" && n.kind === "function" && n.name === "main" && RUST_BIN.test(n.file)) {
|
|
316
316
|
add(n.id, "nativeMain", "the binary's fn main");
|
|
317
317
|
}
|
|
318
|
+
if ((n.language === "c" || n.language === "cpp") && n.id === `${n.file}#main`) {
|
|
319
|
+
add(n.id, "nativeMain", "the program's main()");
|
|
320
|
+
}
|
|
318
321
|
}
|
|
319
322
|
for (const n of graph.nodes) {
|
|
320
323
|
if (n.kind !== "function" || !isStarter(n) || !n.exported) continue;
|
|
@@ -446,7 +449,7 @@ var init_entry = __esm({
|
|
|
446
449
|
robloxScript: 80,
|
|
447
450
|
// Foo.server.luau / Foo.client.luau: a Script/LocalScript Roblox runs itself
|
|
448
451
|
nativeMain: 80,
|
|
449
|
-
// Rust fn main in src/main.rs or src/bin/*.rs; a Python
|
|
452
|
+
// Rust fn main in src/main.rs or src/bin/*.rs; C/C++ main(); a Python __main__ guard
|
|
450
453
|
scriptRoot: 20,
|
|
451
454
|
// …that is a folder's init.server/init.client (the root of a script tree)
|
|
452
455
|
moduleRoot: 10,
|
|
@@ -504,12 +507,13 @@ var init_hubs = __esm({
|
|
|
504
507
|
import fs2 from "node:fs";
|
|
505
508
|
import path2 from "node:path";
|
|
506
509
|
function readLayers(repo) {
|
|
507
|
-
let raw;
|
|
508
510
|
try {
|
|
509
|
-
|
|
511
|
+
return parseLayers(JSON.parse(fs2.readFileSync(layersFile(repo), "utf8")));
|
|
510
512
|
} catch {
|
|
511
513
|
return void 0;
|
|
512
514
|
}
|
|
515
|
+
}
|
|
516
|
+
function parseLayers(raw) {
|
|
513
517
|
const d = raw;
|
|
514
518
|
if (!d || !Array.isArray(d.layers)) return void 0;
|
|
515
519
|
const layers = d.layers.filter(
|
|
@@ -521,6 +525,20 @@ function readLayers(repo) {
|
|
|
521
525
|
);
|
|
522
526
|
return { version: 1, layers, rules };
|
|
523
527
|
}
|
|
528
|
+
function writeLayers(repo, doc) {
|
|
529
|
+
fs2.mkdirSync(path2.dirname(layersFile(repo)), { recursive: true });
|
|
530
|
+
fs2.writeFileSync(layersFile(repo), `${JSON.stringify(doc, null, 2)}
|
|
531
|
+
`);
|
|
532
|
+
}
|
|
533
|
+
function layerMap(doc, graph) {
|
|
534
|
+
const layerOf = layerIndex(doc);
|
|
535
|
+
const byFile = {};
|
|
536
|
+
for (const n of graph.nodes) {
|
|
537
|
+
const l = n.kind === "file" ? layerOf(n.file) : void 0;
|
|
538
|
+
if (l) byFile[n.file] = l;
|
|
539
|
+
}
|
|
540
|
+
return { names: doc.layers.map((l) => l.name), byFile };
|
|
541
|
+
}
|
|
524
542
|
function layerIndex(doc) {
|
|
525
543
|
const compiled = doc.layers.map((l) => ({ name: l.name, res: l.paths.map(globToRegExp) }));
|
|
526
544
|
const memo = /* @__PURE__ */ new Map();
|
|
@@ -905,8 +923,8 @@ function parseUnifiedDiff(text) {
|
|
|
905
923
|
if (line.startsWith("diff --git ")) {
|
|
906
924
|
flush();
|
|
907
925
|
const both = line.slice("diff --git ".length);
|
|
908
|
-
const
|
|
909
|
-
cur = { oldPath:
|
|
926
|
+
const path29 = unquote(both.slice(0, (both.length - 1) / 2));
|
|
927
|
+
cur = { oldPath: path29, newPath: path29, status: "modified", oldRanges: [], newRanges: [] };
|
|
910
928
|
} else if (!cur) {
|
|
911
929
|
} else if (line.startsWith("new file mode")) {
|
|
912
930
|
cur.status = "added";
|
|
@@ -956,10 +974,10 @@ function getFileDiffs(repo, baseSha, headRef) {
|
|
|
956
974
|
const diffs = parseUnifiedDiff(git(repo, args).toString());
|
|
957
975
|
if (headRef) return diffs;
|
|
958
976
|
const untracked = git(repo, ["ls-files", "--others", "--exclude-standard", "-z"]).toString().split("\0").filter((p) => p && !p.endsWith("/"));
|
|
959
|
-
for (const
|
|
960
|
-
const n = lineCount(join(repo,
|
|
977
|
+
for (const path29 of untracked) {
|
|
978
|
+
const n = lineCount(join(repo, path29));
|
|
961
979
|
diffs.push({
|
|
962
|
-
newPath:
|
|
980
|
+
newPath: path29,
|
|
963
981
|
status: "added",
|
|
964
982
|
oldRanges: [],
|
|
965
983
|
newRanges: n ? [{ startLine: 1, endLine: n }] : []
|
|
@@ -998,10 +1016,10 @@ import fs3 from "node:fs";
|
|
|
998
1016
|
import os from "node:os";
|
|
999
1017
|
import path3 from "node:path";
|
|
1000
1018
|
function requireSpec(arg) {
|
|
1001
|
-
const
|
|
1002
|
-
if (
|
|
1003
|
-
const relative2 = /^(\.\.?|@self)\//.test(
|
|
1004
|
-
return { ...relative2 && { relative: relative2 }, tokens:
|
|
1019
|
+
const str3 = /^(["'`])(.*)\1$/.exec(arg)?.[2];
|
|
1020
|
+
if (str3 !== void 0) {
|
|
1021
|
+
const relative2 = /^(\.\.?|@self)\//.test(str3) ? str3 : void 0;
|
|
1022
|
+
return { ...relative2 && { relative: relative2 }, tokens: str3.split("/") };
|
|
1005
1023
|
}
|
|
1006
1024
|
const tokens = (arg.match(/[A-Za-z_][\w-]*/g) ?? []).filter((t) => !LOOKUPS.has(t));
|
|
1007
1025
|
const chain = /^script((?:\.Parent)*)((?:\.[A-Za-z_]\w*)+)$/.exec(arg.replace(/\s+/g, ""));
|
|
@@ -1042,13 +1060,17 @@ function matchTarget(spec, targets, gamePath) {
|
|
|
1042
1060
|
}
|
|
1043
1061
|
function findTool(name, root) {
|
|
1044
1062
|
const rokit = path3.join(os.homedir(), ".rokit");
|
|
1045
|
-
const
|
|
1063
|
+
const store = path3.join(rokit, "tool-storage");
|
|
1064
|
+
const ls = (dir) => {
|
|
1046
1065
|
try {
|
|
1047
|
-
return fs3.
|
|
1066
|
+
return fs3.readdirSync(dir);
|
|
1048
1067
|
} catch {
|
|
1049
1068
|
return [];
|
|
1050
1069
|
}
|
|
1051
|
-
}
|
|
1070
|
+
};
|
|
1071
|
+
const stored = ls(store).flatMap(
|
|
1072
|
+
(author) => ls(path3.join(store, author, name)).map((v) => path3.join(author, name, v, name))
|
|
1073
|
+
).filter((p) => fs3.existsSync(path3.join(store, p))).sort((a, b) => a.localeCompare(b, "en", { numeric: true })).map((p) => path3.join(store, p)).reverse();
|
|
1052
1074
|
for (const bin of [name, path3.join(rokit, "bin", name), ...stored]) {
|
|
1053
1075
|
if (spawnSync2(bin, ["--version"], { cwd: root, stdio: "ignore" }).status === 0) return bin;
|
|
1054
1076
|
}
|
|
@@ -1200,6 +1222,7 @@ function indexerVersion() {
|
|
|
1200
1222
|
h.update(depVersion(path4.join(ENGINE_ROOT, "package.json"), "web-tree-sitter"));
|
|
1201
1223
|
h.update(depVersion(path4.join(ENGINE_ROOT, "package.json"), "tree-sitter-python"));
|
|
1202
1224
|
h.update(depVersion(path4.join(ENGINE_ROOT, "package.json"), "tree-sitter-rust"));
|
|
1225
|
+
h.update(depVersion(path4.join(ENGINE_ROOT, "package.json"), "tree-sitter-cpp"));
|
|
1203
1226
|
h.update(luauLspIdentity());
|
|
1204
1227
|
memoVersion = h.digest("hex").slice(0, 16);
|
|
1205
1228
|
return memoVersion;
|
|
@@ -1766,8 +1789,8 @@ function calleeName(call) {
|
|
|
1766
1789
|
if (ts2.isPropertyAccessExpression(c) && ts2.isIdentifier(c.name)) return c.name.text;
|
|
1767
1790
|
return void 0;
|
|
1768
1791
|
}
|
|
1769
|
-
function segsOf(
|
|
1770
|
-
return
|
|
1792
|
+
function segsOf(path29) {
|
|
1793
|
+
return path29.split("/").filter(Boolean).map((s) => s.includes("*") || s.startsWith("[...") || s.startsWith("[[...") ? "*" : s).map((s) => /^[:{[]/.test(s) ? ":" : s);
|
|
1771
1794
|
}
|
|
1772
1795
|
function urlPath(e) {
|
|
1773
1796
|
let text;
|
|
@@ -1832,8 +1855,8 @@ function routeCall(r, call, site, target, handlerOf) {
|
|
|
1832
1855
|
if (to) r.servers.push({ method: shape.method, segs: segsOf(shape.path), to });
|
|
1833
1856
|
if (to !== void 0) return;
|
|
1834
1857
|
}
|
|
1835
|
-
const
|
|
1836
|
-
if (!
|
|
1858
|
+
const path29 = arg && urlPath(arg);
|
|
1859
|
+
if (!path29) return;
|
|
1837
1860
|
const m = name.toLowerCase();
|
|
1838
1861
|
let method;
|
|
1839
1862
|
if (ts2.isIdentifier(call.expression) && CLIENT_FNS.has(name)) method = fetchMethod(call);
|
|
@@ -1841,9 +1864,9 @@ function routeCall(r, call, site, target, handlerOf) {
|
|
|
1841
1864
|
method = m.toUpperCase();
|
|
1842
1865
|
else if (target && (target.kind === "function" || target.kind === "method")) method = "ANY";
|
|
1843
1866
|
else return;
|
|
1844
|
-
const segs = segsOf(
|
|
1867
|
+
const segs = segsOf(path29);
|
|
1845
1868
|
if (segs.some((s) => s !== ":" && s !== "*"))
|
|
1846
|
-
r.clients.push({ ...site, method, segs, raw:
|
|
1869
|
+
r.clients.push({ ...site, method, segs, raw: path29 });
|
|
1847
1870
|
}
|
|
1848
1871
|
function serverLiteral(r, lit, to, prefix = false) {
|
|
1849
1872
|
const m = isStr(lit) ? ROUTE_LITERAL.exec(lit.text) : null;
|
|
@@ -2342,6 +2365,16 @@ function grammarWasm(pkg) {
|
|
|
2342
2365
|
const bundled = fileURLToPath2(new URL(`./grammars/${pkg}.wasm`, import.meta.url));
|
|
2343
2366
|
return fs5.existsSync(bundled) ? bundled : createRequire(import.meta.url).resolve(`${pkg}/${pkg}.wasm`);
|
|
2344
2367
|
}
|
|
2368
|
+
function leadingComments(stmt) {
|
|
2369
|
+
const out = [];
|
|
2370
|
+
let at = stmt;
|
|
2371
|
+
for (let p = stmt.previousNamedSibling; p?.type === "comment"; p = p.previousNamedSibling) {
|
|
2372
|
+
if (p.endPosition.row < at.startPosition.row - 1) break;
|
|
2373
|
+
out.unshift(p);
|
|
2374
|
+
at = p;
|
|
2375
|
+
}
|
|
2376
|
+
return out;
|
|
2377
|
+
}
|
|
2345
2378
|
var treeSitterReady;
|
|
2346
2379
|
var init_tree_sitter = __esm({
|
|
2347
2380
|
"packages/engine/src/indexer/tree-sitter.ts"() {
|
|
@@ -2350,11 +2383,547 @@ var init_tree_sitter = __esm({
|
|
|
2350
2383
|
}
|
|
2351
2384
|
});
|
|
2352
2385
|
|
|
2353
|
-
// packages/engine/src/indexer/
|
|
2354
|
-
import { createHash as createHash4 } from "node:crypto";
|
|
2386
|
+
// packages/engine/src/indexer/cpp.ts
|
|
2355
2387
|
import fs6 from "node:fs";
|
|
2356
2388
|
import path6 from "node:path";
|
|
2357
2389
|
import { Language, Parser as Parser2 } from "web-tree-sitter";
|
|
2390
|
+
function qualSegs(n) {
|
|
2391
|
+
if (!n) return void 0;
|
|
2392
|
+
switch (n.type) {
|
|
2393
|
+
case "identifier":
|
|
2394
|
+
case "field_identifier":
|
|
2395
|
+
case "type_identifier":
|
|
2396
|
+
case "namespace_identifier":
|
|
2397
|
+
case "destructor_name":
|
|
2398
|
+
case "operator_name":
|
|
2399
|
+
return [n.text];
|
|
2400
|
+
case "template_type":
|
|
2401
|
+
case "template_function":
|
|
2402
|
+
case "template_method":
|
|
2403
|
+
return qualSegs(n.childForFieldName("name"));
|
|
2404
|
+
case "qualified_identifier": {
|
|
2405
|
+
const scope = n.childForFieldName("scope");
|
|
2406
|
+
const head = scope ? qualSegs(scope) : [];
|
|
2407
|
+
const tail = qualSegs(n.childForFieldName("name"));
|
|
2408
|
+
return head && tail ? [...head, ...tail] : void 0;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
return void 0;
|
|
2412
|
+
}
|
|
2413
|
+
function templateArg(n) {
|
|
2414
|
+
const t = n.type === "qualified_identifier" ? innermost(n) : n;
|
|
2415
|
+
const arg = t.childForFieldName("arguments")?.namedChildren[0];
|
|
2416
|
+
return arg?.type === "type_descriptor" ? arg.childForFieldName("type") : null;
|
|
2417
|
+
}
|
|
2418
|
+
function typeSegs(t) {
|
|
2419
|
+
if (!t) return void 0;
|
|
2420
|
+
if (t.type === "struct_specifier" || t.type === "class_specifier")
|
|
2421
|
+
return qualSegs(t.childForFieldName("name"));
|
|
2422
|
+
const segs = qualSegs(t);
|
|
2423
|
+
if (segs && SMART_PTRS.has(segs.at(-1) ?? "")) return typeSegs(templateArg(t));
|
|
2424
|
+
return segs;
|
|
2425
|
+
}
|
|
2426
|
+
function declName(d) {
|
|
2427
|
+
for (let n = d; n; ) {
|
|
2428
|
+
if (n.type === "identifier" || n.type === "field_identifier") return n.text;
|
|
2429
|
+
if (n.type === "function_declarator") return void 0;
|
|
2430
|
+
n = n.childForFieldName("declarator") ?? n.namedChildren.find((c) => c?.type === "identifier" || c?.type.endsWith("declarator")) ?? null;
|
|
2431
|
+
}
|
|
2432
|
+
return void 0;
|
|
2433
|
+
}
|
|
2434
|
+
function fnDeclarator(d) {
|
|
2435
|
+
for (let n = d; n; ) {
|
|
2436
|
+
if (n.type === "function_declarator") return n;
|
|
2437
|
+
n = n.childForFieldName("declarator") ?? n.namedChildren.find((c) => c?.type.endsWith("declarator")) ?? null;
|
|
2438
|
+
}
|
|
2439
|
+
return void 0;
|
|
2440
|
+
}
|
|
2441
|
+
function builtType(v) {
|
|
2442
|
+
if (v?.type === "new_expression") return typeSegs(v.childForFieldName("type"));
|
|
2443
|
+
if (v?.type === "call_expression") {
|
|
2444
|
+
const fn = v.childForFieldName("function");
|
|
2445
|
+
if (fn && MAKERS.has(qualSegs(fn)?.at(-1) ?? "")) return typeSegs(templateArg(fn));
|
|
2446
|
+
}
|
|
2447
|
+
return void 0;
|
|
2448
|
+
}
|
|
2449
|
+
function collect(file, text, tags, root) {
|
|
2450
|
+
const language = /\.c$/.test(file) ? "c" : "cpp";
|
|
2451
|
+
const lineCount2 = text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
2452
|
+
const fileNode = {
|
|
2453
|
+
id: fileId(file),
|
|
2454
|
+
kind: "file",
|
|
2455
|
+
name: path6.posix.basename(file),
|
|
2456
|
+
file,
|
|
2457
|
+
range: { startLine: 1, endLine: Math.max(1, lineCount2) },
|
|
2458
|
+
bodyHash: bodyHash(text),
|
|
2459
|
+
exported: false,
|
|
2460
|
+
language,
|
|
2461
|
+
tags: [...tags],
|
|
2462
|
+
mappingStatus: "full"
|
|
2463
|
+
};
|
|
2464
|
+
const out = {
|
|
2465
|
+
file,
|
|
2466
|
+
nodes: [fileNode],
|
|
2467
|
+
includes: [],
|
|
2468
|
+
defs: [],
|
|
2469
|
+
classes: [],
|
|
2470
|
+
calls: [],
|
|
2471
|
+
declared: /* @__PURE__ */ new Set()
|
|
2472
|
+
};
|
|
2473
|
+
const block2 = (item, outer, name, kind, bodyStart, exported) => {
|
|
2474
|
+
const comments2 = leadingComments(outer);
|
|
2475
|
+
const doc = comments2.map((c) => c.text.replace(/^\/\/\/?\s?|^\/\*+\s?|\s*\*+\/$/g, "").replace(/^\s*\* ?/gm, "")).join("\n").trim();
|
|
2476
|
+
return {
|
|
2477
|
+
id: "",
|
|
2478
|
+
// set once the qualified name is known
|
|
2479
|
+
kind,
|
|
2480
|
+
name,
|
|
2481
|
+
file,
|
|
2482
|
+
range: {
|
|
2483
|
+
startLine: (comments2[0] ?? outer).startPosition.row + 1,
|
|
2484
|
+
endLine: outer.endPosition.row + 1
|
|
2485
|
+
},
|
|
2486
|
+
signature: collapse(text.slice(item.startIndex, bodyStart)),
|
|
2487
|
+
...doc && { doc: doc.slice(0, DOC_CAP) },
|
|
2488
|
+
bodyHash: bodyHash(item.text, name),
|
|
2489
|
+
exported,
|
|
2490
|
+
language,
|
|
2491
|
+
tags: [...tags],
|
|
2492
|
+
mappingStatus: "full"
|
|
2493
|
+
};
|
|
2494
|
+
};
|
|
2495
|
+
const ctxOf = (s) => s.def ? defCtx(s.def) : s.ns;
|
|
2496
|
+
const walk2 = (n, s, inLoop, access) => {
|
|
2497
|
+
for (const c of n.namedChildren) {
|
|
2498
|
+
if (!c || c.type === "comment") continue;
|
|
2499
|
+
if (c.type === "access_specifier" && access) {
|
|
2500
|
+
access.v = c.text;
|
|
2501
|
+
continue;
|
|
2502
|
+
}
|
|
2503
|
+
if (c.type === "preproc_include") {
|
|
2504
|
+
const p = c.childForFieldName("path");
|
|
2505
|
+
if (p?.type === "string_literal" || p?.type === "system_lib_string") {
|
|
2506
|
+
const angle = p.type === "system_lib_string";
|
|
2507
|
+
out.includes.push({
|
|
2508
|
+
path: angle ? p.text.slice(1, -1) : p.text.replace(/^"|"$/g, ""),
|
|
2509
|
+
angle,
|
|
2510
|
+
line: c.startPosition.row + 1
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
continue;
|
|
2514
|
+
}
|
|
2515
|
+
if (c.type === "namespace_definition") {
|
|
2516
|
+
const name = c.childForFieldName("name")?.text;
|
|
2517
|
+
const body = c.childForFieldName("body");
|
|
2518
|
+
const segs = name ? name.split("::") : [];
|
|
2519
|
+
if (body) walk2(body, { ...s, ns: [...s.ns, ...segs] }, inLoop);
|
|
2520
|
+
continue;
|
|
2521
|
+
}
|
|
2522
|
+
const outer = c;
|
|
2523
|
+
const item = c.type === "template_declaration" ? c.namedChildren.at(-1) ?? c : c;
|
|
2524
|
+
const spec = item.type === "type_definition" ? item.childForFieldName("type") : item;
|
|
2525
|
+
if ((spec?.type === "class_specifier" || spec?.type === "struct_specifier") && spec.childForFieldName("body") && !s.def) {
|
|
2526
|
+
const body = spec.childForFieldName("body");
|
|
2527
|
+
const name = spec.childForFieldName("name")?.text ?? (item !== spec ? declName(item.childForFieldName("declarator")) : void 0);
|
|
2528
|
+
if (!name) {
|
|
2529
|
+
walk2(body, s, inLoop);
|
|
2530
|
+
continue;
|
|
2531
|
+
}
|
|
2532
|
+
const node = block2(spec, outer, name, "class", body.startIndex, true);
|
|
2533
|
+
const info = {
|
|
2534
|
+
q: join2([...s.ns, name]),
|
|
2535
|
+
node,
|
|
2536
|
+
ns: s.ns,
|
|
2537
|
+
fields: /* @__PURE__ */ new Map(),
|
|
2538
|
+
bases: []
|
|
2539
|
+
};
|
|
2540
|
+
node.id = nodeId(file, info.q);
|
|
2541
|
+
node.parentId = s.cls?.node.id ?? fileNode.id;
|
|
2542
|
+
for (const b of spec.namedChildren)
|
|
2543
|
+
if (b?.type === "base_class_clause")
|
|
2544
|
+
for (const t of b.namedChildren) {
|
|
2545
|
+
const segs = typeSegs(t);
|
|
2546
|
+
if (segs) info.bases.push(segs);
|
|
2547
|
+
}
|
|
2548
|
+
out.nodes.push(node);
|
|
2549
|
+
out.classes.push(info);
|
|
2550
|
+
for (const f of body.namedChildren) {
|
|
2551
|
+
if (f?.type !== "field_declaration") continue;
|
|
2552
|
+
const t = typeSegs(f.childForFieldName("type"));
|
|
2553
|
+
if (!t) continue;
|
|
2554
|
+
for (const d of f.childrenForFieldName("declarator")) {
|
|
2555
|
+
const fname = declName(d);
|
|
2556
|
+
if (fname) info.fields.set(fname, t);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
const isStruct = spec.type === "struct_specifier";
|
|
2560
|
+
walk2(body, { from: node, ns: [...s.ns, name], cls: info }, false, {
|
|
2561
|
+
v: isStruct ? "public" : "private"
|
|
2562
|
+
});
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
if (item.type === "function_definition" && !s.def) {
|
|
2566
|
+
const fd = fnDeclarator(item.childForFieldName("declarator"));
|
|
2567
|
+
const segs = qualSegs(fd?.childForFieldName("declarator") ?? null);
|
|
2568
|
+
const body = item.childForFieldName("body");
|
|
2569
|
+
const name = segs?.at(-1);
|
|
2570
|
+
if (!fd || !segs || !name || !body) continue;
|
|
2571
|
+
const isStatic = item.namedChildren.some(
|
|
2572
|
+
(m) => m?.type === "storage_class_specifier" && m.text === "static"
|
|
2573
|
+
);
|
|
2574
|
+
const inClass = !!s.cls;
|
|
2575
|
+
const exported = inClass ? access?.v !== "private" : !isStatic;
|
|
2576
|
+
const node = block2(
|
|
2577
|
+
item,
|
|
2578
|
+
outer,
|
|
2579
|
+
name,
|
|
2580
|
+
inClass ? "method" : "function",
|
|
2581
|
+
body.startIndex,
|
|
2582
|
+
exported
|
|
2583
|
+
);
|
|
2584
|
+
const def = {
|
|
2585
|
+
node,
|
|
2586
|
+
name,
|
|
2587
|
+
ns: s.ns,
|
|
2588
|
+
scope: segs.slice(0, -1),
|
|
2589
|
+
...s.cls && { cls: s.cls.q },
|
|
2590
|
+
locals: /* @__PURE__ */ new Map()
|
|
2591
|
+
};
|
|
2592
|
+
if (s.cls) node.parentId = s.cls.node.id;
|
|
2593
|
+
for (const p of fd.childForFieldName("parameters")?.namedChildren ?? []) {
|
|
2594
|
+
if (p?.type !== "parameter_declaration" && p?.type !== "optional_parameter_declaration")
|
|
2595
|
+
continue;
|
|
2596
|
+
const name2 = declName(p.childForFieldName("declarator"));
|
|
2597
|
+
const t = typeSegs(p.childForFieldName("type"));
|
|
2598
|
+
if (name2 && t) def.locals.set(name2, t);
|
|
2599
|
+
}
|
|
2600
|
+
out.nodes.push(node);
|
|
2601
|
+
out.defs.push(def);
|
|
2602
|
+
if (!s.cls) out.declared.add(join2([...s.ns, ...segs]));
|
|
2603
|
+
const inner = { from: def, ns: s.ns, def };
|
|
2604
|
+
for (const init of item.namedChildren)
|
|
2605
|
+
if (init?.type === "field_initializer_list") walk2(init, inner, false);
|
|
2606
|
+
walk2(body, inner, false);
|
|
2607
|
+
continue;
|
|
2608
|
+
}
|
|
2609
|
+
if ((item.type === "declaration" || item.type === "field_declaration") && !s.def) {
|
|
2610
|
+
const fd = fnDeclarator(item.childForFieldName("declarator"));
|
|
2611
|
+
const segs = fd && qualSegs(fd.childForFieldName("declarator"));
|
|
2612
|
+
if (segs) {
|
|
2613
|
+
if (!s.cls) out.declared.add(join2([...s.ns, ...segs]));
|
|
2614
|
+
continue;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (c.type === "declaration" && s.def) {
|
|
2618
|
+
const t = typeSegs(c.childForFieldName("type"));
|
|
2619
|
+
for (const d of c.childrenForFieldName("declarator")) {
|
|
2620
|
+
const wrapped = d?.type === "init_declarator" || d?.type === "function_declarator";
|
|
2621
|
+
const target = wrapped ? d.childForFieldName("declarator") : d;
|
|
2622
|
+
const v = d?.type === "init_declarator" ? d.childForFieldName("value") : null;
|
|
2623
|
+
const name = declName(target);
|
|
2624
|
+
const type = t ?? builtType(v);
|
|
2625
|
+
if (name && type) s.def.locals.set(name, type);
|
|
2626
|
+
const constructs = !v || v.type === "argument_list" || v.type === "initializer_list";
|
|
2627
|
+
if (t && target?.type === "identifier" && constructs) {
|
|
2628
|
+
out.calls.push({
|
|
2629
|
+
from: s.from,
|
|
2630
|
+
ctx: ctxOf(s),
|
|
2631
|
+
form: "ctor",
|
|
2632
|
+
segs: t,
|
|
2633
|
+
line: c.startPosition.row + 1,
|
|
2634
|
+
inLoop
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
if (c.type === "new_expression") {
|
|
2640
|
+
const t = typeSegs(c.childForFieldName("type"));
|
|
2641
|
+
if (t)
|
|
2642
|
+
out.calls.push({
|
|
2643
|
+
from: s.from,
|
|
2644
|
+
ctx: ctxOf(s),
|
|
2645
|
+
form: "ctor",
|
|
2646
|
+
segs: t,
|
|
2647
|
+
line: c.startPosition.row + 1,
|
|
2648
|
+
inLoop
|
|
2649
|
+
});
|
|
2650
|
+
}
|
|
2651
|
+
if (c.type === "call_expression") {
|
|
2652
|
+
const fn = c.childForFieldName("function");
|
|
2653
|
+
const site = { from: s.from, ctx: ctxOf(s), line: c.startPosition.row + 1, inLoop };
|
|
2654
|
+
if (fn?.type === "field_expression") {
|
|
2655
|
+
const recv = fn.childForFieldName("argument");
|
|
2656
|
+
const method = qualSegs(fn.childForFieldName("field"))?.at(-1);
|
|
2657
|
+
let segs;
|
|
2658
|
+
if (recv?.type === "this") {
|
|
2659
|
+
if (method) out.calls.push({ ...site, form: "this", segs: [], method });
|
|
2660
|
+
} else if (recv?.type === "identifier") segs = [recv.text];
|
|
2661
|
+
else if (recv?.type === "field_expression" && recv.childForFieldName("argument")?.type === "this")
|
|
2662
|
+
segs = ["this", recv.childForFieldName("field")?.text ?? ""];
|
|
2663
|
+
if (segs && method) out.calls.push({ ...site, form: "member", segs, method });
|
|
2664
|
+
} else if (fn) {
|
|
2665
|
+
const segs = qualSegs(fn);
|
|
2666
|
+
if (segs && MAKERS.has(segs.at(-1) ?? "")) {
|
|
2667
|
+
const t = typeSegs(templateArg(fn));
|
|
2668
|
+
if (t) out.calls.push({ ...site, form: "ctor", segs: t });
|
|
2669
|
+
} else if (segs) out.calls.push({ ...site, form: "name", segs });
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
walk2(c, s, inLoop || LOOPS.has(c.type));
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
walk2(root, { from: fileNode, ns: [] }, false);
|
|
2676
|
+
return out;
|
|
2677
|
+
}
|
|
2678
|
+
function inScope(ctx, segs, map) {
|
|
2679
|
+
for (let i = ctx.length; i >= 0; i--) {
|
|
2680
|
+
const k = join2([...ctx.slice(0, i), ...segs]);
|
|
2681
|
+
if (map.has(k)) return k;
|
|
2682
|
+
}
|
|
2683
|
+
return void 0;
|
|
2684
|
+
}
|
|
2685
|
+
function includeDirs(root, files) {
|
|
2686
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
2687
|
+
for (const f of files) {
|
|
2688
|
+
const segs = f.split("/");
|
|
2689
|
+
const i = segs.lastIndexOf("include");
|
|
2690
|
+
if (i >= 0 && i < segs.length - 1) dirs.add(segs.slice(0, i + 1).join("/"));
|
|
2691
|
+
}
|
|
2692
|
+
const read = (name) => {
|
|
2693
|
+
try {
|
|
2694
|
+
return fs6.readFileSync(path6.join(root, name), "utf8");
|
|
2695
|
+
} catch {
|
|
2696
|
+
return "";
|
|
2697
|
+
}
|
|
2698
|
+
};
|
|
2699
|
+
const flags = [...read("Makefile").matchAll(/-I\s*([^\s"']+)/g)].map((m) => m[1]);
|
|
2700
|
+
for (const m of read("CMakeLists.txt").matchAll(/include_directories\s*\(([^)]*)\)/gi))
|
|
2701
|
+
flags.push(...m[1].split(/\s+/));
|
|
2702
|
+
for (const raw of flags) {
|
|
2703
|
+
const d = raw.replace(/^"|"$/g, "").replace(/^\$\{(CMAKE_CURRENT_SOURCE_DIR|PROJECT_SOURCE_DIR|CMAKE_SOURCE_DIR)\}\/?/, "");
|
|
2704
|
+
if (!d || d.startsWith("/") || d.includes("$") || /^[A-Z]+$/.test(d)) continue;
|
|
2705
|
+
dirs.add(path6.posix.normalize(d).replace(/\/$/, "").replace(/^\.$/, ""));
|
|
2706
|
+
}
|
|
2707
|
+
dirs.add("");
|
|
2708
|
+
return [...dirs];
|
|
2709
|
+
}
|
|
2710
|
+
function buildGraph(root, parsed) {
|
|
2711
|
+
const byFile = new Map(parsed.map((f) => [f.file, f]));
|
|
2712
|
+
const dirs = includeDirs(root, [...byFile.keys()]);
|
|
2713
|
+
const { edges, add } = edgeSink();
|
|
2714
|
+
const classes = /* @__PURE__ */ new Map();
|
|
2715
|
+
for (const f of parsed) for (const c of f.classes) if (!classes.has(c.q)) classes.set(c.q, c);
|
|
2716
|
+
const methods = /* @__PURE__ */ new Map();
|
|
2717
|
+
const funcs = /* @__PURE__ */ new Map();
|
|
2718
|
+
const push2 = (m, k, d) => m.set(k, [...m.get(k) ?? [], d]);
|
|
2719
|
+
for (const f of parsed) {
|
|
2720
|
+
const fileNode = f.nodes[0];
|
|
2721
|
+
const taken = new Set(f.classes.map((c) => c.q));
|
|
2722
|
+
for (const d of f.defs) {
|
|
2723
|
+
if (!d.cls && d.scope.length) {
|
|
2724
|
+
const cls = inScope(d.ns, d.scope, classes);
|
|
2725
|
+
if (cls) {
|
|
2726
|
+
d.cls = cls;
|
|
2727
|
+
d.node.kind = "method";
|
|
2728
|
+
const owner = classes.get(cls);
|
|
2729
|
+
d.node.parentId = owner.node.file === f.file ? owner.node.id : fileNode.id;
|
|
2730
|
+
d.node.exported = true;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
d.node.parentId ??= fileNode.id;
|
|
2734
|
+
const base = d.cls ? `${d.cls}.${d.name}` : join2([...d.ns, ...d.scope, d.name]);
|
|
2735
|
+
let q = base;
|
|
2736
|
+
for (let i = 2; taken.has(q); i++) q = `${base}@${d.node.kind}${i === 2 ? "" : i}`;
|
|
2737
|
+
taken.add(q);
|
|
2738
|
+
d.node.id = nodeId(f.file, q);
|
|
2739
|
+
push2(d.cls ? methods : funcs, base, d);
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
const resolveInclude = (from, inc) => {
|
|
2743
|
+
const tries = inc.angle ? [] : [path6.posix.join(path6.posix.dirname(from), inc.path)];
|
|
2744
|
+
for (const d of dirs) tries.push(path6.posix.join(d, inc.path));
|
|
2745
|
+
return tries.map((t) => path6.posix.normalize(t)).find((t) => byFile.has(t));
|
|
2746
|
+
};
|
|
2747
|
+
const direct = /* @__PURE__ */ new Map();
|
|
2748
|
+
for (const f of parsed) {
|
|
2749
|
+
const to = [];
|
|
2750
|
+
for (const inc of f.includes) {
|
|
2751
|
+
const hit = resolveInclude(f.file, inc);
|
|
2752
|
+
if (!hit) continue;
|
|
2753
|
+
to.push(hit);
|
|
2754
|
+
add(fileId(f.file), "imports", fileId(hit), ["static"], 1, { line: inc.line, inLoop: false });
|
|
2755
|
+
}
|
|
2756
|
+
direct.set(f.file, to);
|
|
2757
|
+
}
|
|
2758
|
+
const closures = /* @__PURE__ */ new Map();
|
|
2759
|
+
const closure = (file) => {
|
|
2760
|
+
let seen = closures.get(file);
|
|
2761
|
+
if (seen) return seen;
|
|
2762
|
+
seen = /* @__PURE__ */ new Set([file]);
|
|
2763
|
+
const stack = [file];
|
|
2764
|
+
while (stack.length) {
|
|
2765
|
+
for (const g of direct.get(stack.pop()) ?? []) {
|
|
2766
|
+
if (!seen.has(g)) {
|
|
2767
|
+
seen.add(g);
|
|
2768
|
+
stack.push(g);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
closures.set(file, seen);
|
|
2773
|
+
return seen;
|
|
2774
|
+
};
|
|
2775
|
+
const ctorOf = (q) => methods.get(`${q}.${classes.get(q).node.name}`)?.[0]?.node.id;
|
|
2776
|
+
const methodOf = (q, name, depth = 0) => {
|
|
2777
|
+
const hit = methods.get(`${q}.${name}`)?.[0];
|
|
2778
|
+
if (hit || depth > 4) return hit;
|
|
2779
|
+
const cls = classes.get(q);
|
|
2780
|
+
for (const b of cls?.bases ?? []) {
|
|
2781
|
+
const bq = inScope(cls?.ns ?? [], b, classes);
|
|
2782
|
+
const m = bq && methodOf(bq, name, depth + 1);
|
|
2783
|
+
if (m) return m;
|
|
2784
|
+
}
|
|
2785
|
+
return void 0;
|
|
2786
|
+
};
|
|
2787
|
+
const fieldOf = (q, name, depth = 0) => {
|
|
2788
|
+
const cls = classes.get(q);
|
|
2789
|
+
if (!cls || depth > 4) return void 0;
|
|
2790
|
+
const t = cls.fields.get(name);
|
|
2791
|
+
if (t) return { type: t, ns: cls.ns };
|
|
2792
|
+
for (const b of cls.bases) {
|
|
2793
|
+
const bq = inScope(cls.ns, b, classes);
|
|
2794
|
+
const hit = bq && fieldOf(bq, name, depth + 1);
|
|
2795
|
+
if (hit) return hit;
|
|
2796
|
+
}
|
|
2797
|
+
return void 0;
|
|
2798
|
+
};
|
|
2799
|
+
for (const f of parsed) {
|
|
2800
|
+
const seen = closure(f.file);
|
|
2801
|
+
const visible = (q) => [...seen].some((g) => byFile.get(g)?.declared.has(q));
|
|
2802
|
+
const pickFn = (q) => {
|
|
2803
|
+
const cands = funcs.get(q) ?? [];
|
|
2804
|
+
const here = cands.find((d) => d.node.file === f.file);
|
|
2805
|
+
if (here) return { id: here.node.id, how: STATIC };
|
|
2806
|
+
const inc = cands.filter((d) => seen.has(d.node.file));
|
|
2807
|
+
if (inc.length === 1) return { id: inc[0].node.id, how: CROSS };
|
|
2808
|
+
if (cands.length !== 1) return void 0;
|
|
2809
|
+
return { id: cands[0].node.id, how: visible(q) ? CROSS : GUESS };
|
|
2810
|
+
};
|
|
2811
|
+
const how = (to) => to.split("#")[0] === f.file ? STATIC : CROSS;
|
|
2812
|
+
for (const c of f.calls) {
|
|
2813
|
+
const def = "locals" in c.from ? c.from : void 0;
|
|
2814
|
+
const fromId = def ? def.node.id : c.from.id;
|
|
2815
|
+
const site = { line: c.line, inLoop: c.inLoop };
|
|
2816
|
+
const link = (to, h = to ? how(to) : STATIC) => {
|
|
2817
|
+
if (to) add(fromId, "calls", to, ...h, site);
|
|
2818
|
+
};
|
|
2819
|
+
if (c.form === "ctor") {
|
|
2820
|
+
const q = inScope(c.ctx, c.segs, classes);
|
|
2821
|
+
if (q) link(ctorOf(q));
|
|
2822
|
+
continue;
|
|
2823
|
+
}
|
|
2824
|
+
if (c.form === "this") {
|
|
2825
|
+
if (def?.cls && c.method) link(methodOf(def.cls, c.method)?.node.id);
|
|
2826
|
+
continue;
|
|
2827
|
+
}
|
|
2828
|
+
if (c.form === "member") {
|
|
2829
|
+
const [head, field] = c.segs;
|
|
2830
|
+
let t;
|
|
2831
|
+
if (head === "this") t = def?.cls && field ? fieldOf(def.cls, field) : void 0;
|
|
2832
|
+
else {
|
|
2833
|
+
const local = def?.locals.get(head);
|
|
2834
|
+
t = local ? { type: local, ns: c.ctx } : def?.cls ? fieldOf(def.cls, head) : void 0;
|
|
2835
|
+
}
|
|
2836
|
+
const q = t && inScope(t.ns, t.type, classes);
|
|
2837
|
+
if (q && c.method) link(methodOf(q, c.method)?.node.id);
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
const name = c.segs.at(-1);
|
|
2841
|
+
if (c.segs.length === 1 && def?.cls) {
|
|
2842
|
+
const m = methodOf(def.cls, name);
|
|
2843
|
+
if (m) {
|
|
2844
|
+
link(m.node.id);
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
if (c.segs.length > 1) {
|
|
2849
|
+
const cls = inScope(c.ctx, c.segs.slice(0, -1), classes);
|
|
2850
|
+
if (cls) {
|
|
2851
|
+
link(methodOf(cls, name)?.node.id);
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
const fq = inScope(c.ctx, c.segs, funcs);
|
|
2856
|
+
const fn = fq && pickFn(fq);
|
|
2857
|
+
if (fn) {
|
|
2858
|
+
link(fn.id, fn.how);
|
|
2859
|
+
continue;
|
|
2860
|
+
}
|
|
2861
|
+
const cq = inScope(c.ctx, c.segs, classes);
|
|
2862
|
+
if (cq) link(ctorOf(cq));
|
|
2863
|
+
}
|
|
2864
|
+
for (const cls of f.classes) {
|
|
2865
|
+
for (const b of cls.bases) {
|
|
2866
|
+
const bq = inScope(cls.ns, b, classes);
|
|
2867
|
+
if (bq) {
|
|
2868
|
+
const to = classes.get(bq).node.id;
|
|
2869
|
+
add(cls.node.id, "inherits", to, ...how(to), {
|
|
2870
|
+
line: cls.node.range.startLine,
|
|
2871
|
+
inLoop: false
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
return { nodes: parsed.flatMap((f) => f.nodes), edges: [...edges.values()] };
|
|
2878
|
+
}
|
|
2879
|
+
function indexCpp(root, files, tagsFor2) {
|
|
2880
|
+
const parsed = [];
|
|
2881
|
+
for (const file of [...files].sort()) {
|
|
2882
|
+
const text = fs6.readFileSync(path6.join(root, file), "utf8");
|
|
2883
|
+
if (GENERATED.test(text.split("\n", 30).join("\n"))) continue;
|
|
2884
|
+
const tree = parser.parse(text);
|
|
2885
|
+
if (!tree) continue;
|
|
2886
|
+
try {
|
|
2887
|
+
parsed.push(collect(file, text, tagsFor2(file), tree.rootNode));
|
|
2888
|
+
} finally {
|
|
2889
|
+
tree.delete();
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
return buildGraph(root, parsed);
|
|
2893
|
+
}
|
|
2894
|
+
var parser, STATIC, CROSS, GUESS, LOOPS, SMART_PTRS, MAKERS, GENERATED, join2, innermost, defCtx;
|
|
2895
|
+
var init_cpp = __esm({
|
|
2896
|
+
async "packages/engine/src/indexer/cpp.ts"() {
|
|
2897
|
+
"use strict";
|
|
2898
|
+
init_ids();
|
|
2899
|
+
init_blocks();
|
|
2900
|
+
init_edges();
|
|
2901
|
+
init_tree_sitter();
|
|
2902
|
+
await treeSitterReady;
|
|
2903
|
+
parser = new Parser2();
|
|
2904
|
+
parser.setLanguage(await Language.load(grammarWasm("tree-sitter-cpp")));
|
|
2905
|
+
STATIC = [["static"], 0.95];
|
|
2906
|
+
CROSS = [["inferred"], 0.9];
|
|
2907
|
+
GUESS = [["inferred"], 0.5];
|
|
2908
|
+
LOOPS = /* @__PURE__ */ new Set(["for_statement", "for_range_loop", "while_statement", "do_statement"]);
|
|
2909
|
+
SMART_PTRS = /* @__PURE__ */ new Set(["unique_ptr", "shared_ptr", "weak_ptr"]);
|
|
2910
|
+
MAKERS = /* @__PURE__ */ new Set(["make_unique", "make_shared"]);
|
|
2911
|
+
GENERATED = /generated by|do not edit/i;
|
|
2912
|
+
join2 = (segs) => segs.join(".");
|
|
2913
|
+
innermost = (n) => {
|
|
2914
|
+
let at = n;
|
|
2915
|
+
while (at.type === "qualified_identifier") at = at.childForFieldName("name") ?? at;
|
|
2916
|
+
return at;
|
|
2917
|
+
};
|
|
2918
|
+
defCtx = (d) => d.cls ? d.cls.split(".") : [...d.ns, ...d.scope];
|
|
2919
|
+
}
|
|
2920
|
+
});
|
|
2921
|
+
|
|
2922
|
+
// packages/engine/src/indexer/luau.ts
|
|
2923
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2924
|
+
import fs7 from "node:fs";
|
|
2925
|
+
import path7 from "node:path";
|
|
2926
|
+
import { Language as Language2, Parser as Parser3 } from "web-tree-sitter";
|
|
2358
2927
|
function chainOf(n) {
|
|
2359
2928
|
if (n?.type === "identifier") return n.text;
|
|
2360
2929
|
if (n?.type !== "dot_index_expression") return void 0;
|
|
@@ -2371,7 +2940,7 @@ function ownLine(c) {
|
|
|
2371
2940
|
const prev = c.previousSibling;
|
|
2372
2941
|
return !prev || prev.endPosition.row < c.startPosition.row;
|
|
2373
2942
|
}
|
|
2374
|
-
function
|
|
2943
|
+
function leadingComments2(stmt) {
|
|
2375
2944
|
const out = [];
|
|
2376
2945
|
let below = stmt;
|
|
2377
2946
|
for (let c = above(stmt); c?.type === "comment"; c = above(c)) {
|
|
@@ -2398,20 +2967,20 @@ function signatureOf(fn, text) {
|
|
|
2398
2967
|
return collapse(text.slice(start, stop?.startIndex ?? params.endIndex));
|
|
2399
2968
|
}
|
|
2400
2969
|
function walkFile(file, text, tags) {
|
|
2401
|
-
const tree =
|
|
2970
|
+
const tree = parser2.parse(text);
|
|
2402
2971
|
if (!tree) throw new Error(`tree-sitter could not parse ${file}`);
|
|
2403
2972
|
try {
|
|
2404
|
-
return
|
|
2973
|
+
return collect2(file, text, tags, tree.rootNode);
|
|
2405
2974
|
} finally {
|
|
2406
2975
|
tree.delete();
|
|
2407
2976
|
}
|
|
2408
2977
|
}
|
|
2409
|
-
function
|
|
2978
|
+
function collect2(file, text, tags, root) {
|
|
2410
2979
|
const lineCount2 = text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
2411
2980
|
const fileNode = {
|
|
2412
2981
|
id: fileId(file),
|
|
2413
2982
|
kind: "file",
|
|
2414
|
-
name:
|
|
2983
|
+
name: path7.posix.basename(file),
|
|
2415
2984
|
file,
|
|
2416
2985
|
range: { startLine: 1, endLine: Math.max(1, lineCount2) },
|
|
2417
2986
|
bodyHash: bodyHash(text),
|
|
@@ -2450,7 +3019,7 @@ function collect(file, text, tags, root) {
|
|
|
2450
3019
|
q = `${qname}@${kind}`;
|
|
2451
3020
|
for (let n = 2; out.byQname.has(q); n++) q = `${qname}@${kind}${n}`;
|
|
2452
3021
|
}
|
|
2453
|
-
const docs =
|
|
3022
|
+
const docs = leadingComments2(stmt);
|
|
2454
3023
|
const node = {
|
|
2455
3024
|
id: nodeId(file, q),
|
|
2456
3025
|
kind,
|
|
@@ -2790,7 +3359,7 @@ function parseAllFiles(root, files, tagsFor2, cacheRepo) {
|
|
|
2790
3359
|
const nextCache = {};
|
|
2791
3360
|
const parsed = /* @__PURE__ */ new Map();
|
|
2792
3361
|
for (const file of [...files].sort()) {
|
|
2793
|
-
const text =
|
|
3362
|
+
const text = fs7.readFileSync(path7.join(root, file), "utf8");
|
|
2794
3363
|
const hash = createHash4("sha256").update(text).digest("hex");
|
|
2795
3364
|
const cached = onDisk[file];
|
|
2796
3365
|
const lf = cached?.hash === hash ? deserializeLuauFile(cached.data) : walkFile(file, text, tagsFor2(file));
|
|
@@ -2800,7 +3369,7 @@ function parseAllFiles(root, files, tagsFor2, cacheRepo) {
|
|
|
2800
3369
|
if (useCache) writeJsonCacheAtomic(cacheFile, nextCache);
|
|
2801
3370
|
return parsed;
|
|
2802
3371
|
}
|
|
2803
|
-
function
|
|
3372
|
+
function buildGraph2(parsed, graph) {
|
|
2804
3373
|
if (!graph && !warned) {
|
|
2805
3374
|
warned = true;
|
|
2806
3375
|
console.warn("topo: luau-lsp not found or failed; Luau imports resolve relative requires only");
|
|
@@ -2826,12 +3395,12 @@ function buildGraph(parsed, graph) {
|
|
|
2826
3395
|
function indexLuau(root, files, tagsFor2, cacheRepo = root) {
|
|
2827
3396
|
const parsed = parseAllFiles(root, files, tagsFor2, cacheRepo);
|
|
2828
3397
|
const graph = requireGraph(root, [...parsed.keys()]);
|
|
2829
|
-
return
|
|
3398
|
+
return buildGraph2(parsed, graph);
|
|
2830
3399
|
}
|
|
2831
3400
|
function indexLuauState(root, files, tagsFor2, cacheRepo = root) {
|
|
2832
3401
|
const parsed = parseAllFiles(root, files, tagsFor2, cacheRepo);
|
|
2833
3402
|
const graph = requireGraph(root, [...parsed.keys()]);
|
|
2834
|
-
return { graph:
|
|
3403
|
+
return { graph: buildGraph2(parsed, graph), state: { parsed, requireGraph: graph } };
|
|
2835
3404
|
}
|
|
2836
3405
|
function updateLuau(root, files, changedFiles, tagsFor2, prev, forceRequireGraph = false) {
|
|
2837
3406
|
const fileSet = new Set(files);
|
|
@@ -2845,13 +3414,13 @@ function updateLuau(root, files, changedFiles, tagsFor2, prev, forceRequireGraph
|
|
|
2845
3414
|
parsed.set(f, old);
|
|
2846
3415
|
continue;
|
|
2847
3416
|
}
|
|
2848
|
-
const text =
|
|
3417
|
+
const text = fs7.readFileSync(path7.join(root, f), "utf8");
|
|
2849
3418
|
const fresh = walkFile(f, text, tagsFor2(f));
|
|
2850
3419
|
if (!old || !sameSpecs(old.requires, fresh.requires)) requiresChanged = true;
|
|
2851
3420
|
parsed.set(f, fresh);
|
|
2852
3421
|
}
|
|
2853
3422
|
const graph = requiresChanged ? requireGraph(root, files) : prev.requireGraph;
|
|
2854
|
-
return { graph:
|
|
3423
|
+
return { graph: buildGraph2(parsed, graph), state: { parsed, requireGraph: graph } };
|
|
2855
3424
|
}
|
|
2856
3425
|
function resolveRef2(f, r, files) {
|
|
2857
3426
|
if (r.base === void 0) {
|
|
@@ -2861,14 +3430,14 @@ function resolveRef2(f, r, files) {
|
|
|
2861
3430
|
const req = r.binding?.req;
|
|
2862
3431
|
if (req) {
|
|
2863
3432
|
const to2 = !r.base.includes(".") && req.target ? files.get(req.target)?.exports.get(r.name) : void 0;
|
|
2864
|
-
return to2 && { to: to2, how:
|
|
3433
|
+
return to2 && { to: to2, how: CROSS2 };
|
|
2865
3434
|
}
|
|
2866
3435
|
const self = r.binding?.selfOf;
|
|
2867
3436
|
const table = self ? r.base.replace(/^self\b/, self) : r.base;
|
|
2868
3437
|
const to = f.byQname.get(`${table}.${r.name}`);
|
|
2869
3438
|
return to && to.kind !== "file" ? { to, how: CALL } : void 0;
|
|
2870
3439
|
}
|
|
2871
|
-
var
|
|
3440
|
+
var parser2, SKIP, CALL, CROSS2, NOT_DOC, optDoc2, named2, qnameOf, isRequire, unwrapCast, warned, sameSpecs, same2;
|
|
2872
3441
|
var init_luau = __esm({
|
|
2873
3442
|
async "packages/engine/src/indexer/luau.ts"() {
|
|
2874
3443
|
"use strict";
|
|
@@ -2879,11 +3448,11 @@ var init_luau = __esm({
|
|
|
2879
3448
|
init_luau_requires();
|
|
2880
3449
|
init_tree_sitter();
|
|
2881
3450
|
await treeSitterReady;
|
|
2882
|
-
|
|
2883
|
-
|
|
3451
|
+
parser2 = new Parser3();
|
|
3452
|
+
parser2.setLanguage(await Language2.load(grammarWasm("tree-sitter-luau")));
|
|
2884
3453
|
SKIP = /* @__PURE__ */ new Set(["comment", "type_definition", "number", "true", "false", "nil"]);
|
|
2885
3454
|
CALL = [["static"], 0.95];
|
|
2886
|
-
|
|
3455
|
+
CROSS2 = [["inferred"], 0.9];
|
|
2887
3456
|
NOT_DOC = /^--!|^--\s*(selene|stylua):|copyright|SPDX-License-Identifier/i;
|
|
2888
3457
|
optDoc2 = (doc) => doc ? { doc } : {};
|
|
2889
3458
|
named2 = (n, type) => n.namedChildren.find((c) => c.type === type);
|
|
@@ -2897,23 +3466,13 @@ var init_luau = __esm({
|
|
|
2897
3466
|
});
|
|
2898
3467
|
|
|
2899
3468
|
// packages/engine/src/indexer/python.ts
|
|
2900
|
-
import
|
|
2901
|
-
import
|
|
2902
|
-
import { Language as
|
|
3469
|
+
import fs8 from "node:fs";
|
|
3470
|
+
import path8 from "node:path";
|
|
3471
|
+
import { Language as Language3, Parser as Parser4 } from "web-tree-sitter";
|
|
2903
3472
|
function docstring(body) {
|
|
2904
3473
|
const first = body?.namedChildren[0];
|
|
2905
|
-
const
|
|
2906
|
-
return
|
|
2907
|
-
}
|
|
2908
|
-
function leadingComments2(stmt) {
|
|
2909
|
-
const out = [];
|
|
2910
|
-
let at = stmt;
|
|
2911
|
-
for (let p = stmt.previousNamedSibling; p?.type === "comment"; p = p.previousNamedSibling) {
|
|
2912
|
-
if (p.endPosition.row < at.startPosition.row - 1) break;
|
|
2913
|
-
out.unshift(p);
|
|
2914
|
-
at = p;
|
|
2915
|
-
}
|
|
2916
|
-
return out;
|
|
3474
|
+
const str3 = first?.type === "expression_statement" ? first.namedChildren[0] : void 0;
|
|
3475
|
+
return str3?.type === "string" ? stripQuotes(str3.text) : void 0;
|
|
2917
3476
|
}
|
|
2918
3477
|
function dotted(n) {
|
|
2919
3478
|
if (!n) return void 0;
|
|
@@ -2923,13 +3482,13 @@ function dotted(n) {
|
|
|
2923
3482
|
const attr = n.childForFieldName("attribute");
|
|
2924
3483
|
return head && attr ? [...head, attr.text] : void 0;
|
|
2925
3484
|
}
|
|
2926
|
-
function
|
|
3485
|
+
function collect3(file, text, tags, root) {
|
|
2927
3486
|
const lineCount2 = text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
2928
3487
|
const fileDoc = docstring(root);
|
|
2929
3488
|
const fileNode = {
|
|
2930
3489
|
id: fileId(file),
|
|
2931
3490
|
kind: "file",
|
|
2932
|
-
name:
|
|
3491
|
+
name: path8.posix.basename(file),
|
|
2933
3492
|
file,
|
|
2934
3493
|
range: { startLine: 1, endLine: Math.max(1, lineCount2) },
|
|
2935
3494
|
...fileDoc && { doc: fileDoc.slice(0, DOC_CAP) },
|
|
@@ -2956,7 +3515,7 @@ function collect2(file, text, tags, root) {
|
|
|
2956
3515
|
const base = parent.kind === "file" ? name : `${qnameOf2(parent)}.${name}`;
|
|
2957
3516
|
let q = base;
|
|
2958
3517
|
for (let i = 2; out.byQname.has(q); i++) q = `${base}@${kind}${i === 2 ? "" : i}`;
|
|
2959
|
-
const comments2 =
|
|
3518
|
+
const comments2 = leadingComments(outer);
|
|
2960
3519
|
const doc = docstring(def.childForFieldName("body")) ?? comments2.map((c) => c.text.replace(/^#\s?/, "")).join("\n");
|
|
2961
3520
|
const params = def.childForFieldName(isClass ? "superclasses" : "parameters")?.text ?? "";
|
|
2962
3521
|
const ret = def.childForFieldName("return_type")?.text;
|
|
@@ -3029,7 +3588,7 @@ function collect2(file, text, tags, root) {
|
|
|
3029
3588
|
});
|
|
3030
3589
|
}
|
|
3031
3590
|
}
|
|
3032
|
-
walk2(c, block2, fnScopes, classQ, inLoop ||
|
|
3591
|
+
walk2(c, block2, fnScopes, classQ, inLoop || LOOPS2.has(c.type));
|
|
3033
3592
|
}
|
|
3034
3593
|
};
|
|
3035
3594
|
walk2(root, fileNode, [], void 0, false);
|
|
@@ -3067,9 +3626,9 @@ function importsOf(stmt) {
|
|
|
3067
3626
|
function importRoots(files) {
|
|
3068
3627
|
const roots = /* @__PURE__ */ new Set(["", "src"]);
|
|
3069
3628
|
for (const f of files) {
|
|
3070
|
-
if (
|
|
3071
|
-
const parent =
|
|
3072
|
-
if (!files.has(
|
|
3629
|
+
if (path8.posix.basename(f) !== "__init__.py") continue;
|
|
3630
|
+
const parent = path8.posix.dirname(path8.posix.dirname(f));
|
|
3631
|
+
if (!files.has(path8.posix.join(parent, "__init__.py"))) roots.add(parent === "." ? "" : parent);
|
|
3073
3632
|
}
|
|
3074
3633
|
return [...roots];
|
|
3075
3634
|
}
|
|
@@ -3077,15 +3636,15 @@ function moduleFile(files, roots, from, module, level) {
|
|
|
3077
3636
|
const parts = module ? module.split(".") : [];
|
|
3078
3637
|
let bases;
|
|
3079
3638
|
if (level > 0) {
|
|
3080
|
-
let dir =
|
|
3081
|
-
for (let i = 1; i < level; i++) dir =
|
|
3639
|
+
let dir = path8.posix.dirname(from);
|
|
3640
|
+
for (let i = 1; i < level; i++) dir = path8.posix.dirname(dir);
|
|
3082
3641
|
bases = [dir];
|
|
3083
3642
|
} else {
|
|
3084
|
-
bases = [...roots,
|
|
3643
|
+
bases = [...roots, path8.posix.dirname(from)];
|
|
3085
3644
|
}
|
|
3086
3645
|
for (const b of bases) {
|
|
3087
|
-
const p =
|
|
3088
|
-
for (const f of [`${p}.py`,
|
|
3646
|
+
const p = path8.posix.join(b === "." ? "" : b, ...parts);
|
|
3647
|
+
for (const f of [`${p}.py`, path8.posix.join(p, "__init__.py")]) if (files.has(f)) return f;
|
|
3089
3648
|
}
|
|
3090
3649
|
return void 0;
|
|
3091
3650
|
}
|
|
@@ -3119,7 +3678,7 @@ function bindingsOf(f, files, roots, add) {
|
|
|
3119
3678
|
}
|
|
3120
3679
|
return binds;
|
|
3121
3680
|
}
|
|
3122
|
-
function
|
|
3681
|
+
function buildGraph3(parsed) {
|
|
3123
3682
|
const byFile = new Map(parsed.map((f) => [f.file, f]));
|
|
3124
3683
|
const files = new Set(byFile.keys());
|
|
3125
3684
|
const roots = importRoots(files);
|
|
@@ -3131,32 +3690,32 @@ function buildGraph2(parsed) {
|
|
|
3131
3690
|
roots,
|
|
3132
3691
|
(to, line) => add(fileId(f.file), "imports", fileId(to), ["static"], 1, { line, inLoop: false })
|
|
3133
3692
|
);
|
|
3134
|
-
const inModule = (file,
|
|
3693
|
+
const inModule = (file, path29) => byFile.get(file)?.byQname.get(path29.join("."));
|
|
3135
3694
|
const resolve2 = (callee, fnScopes, classQ) => {
|
|
3136
3695
|
const [head, ...rest] = callee;
|
|
3137
3696
|
if (!head) return void 0;
|
|
3138
3697
|
if ((head === "self" || head === "cls") && classQ && rest.length === 1) {
|
|
3139
3698
|
const hit = f.byQname.get(`${classQ}.${rest[0]}`);
|
|
3140
|
-
return hit && { node: hit, how:
|
|
3699
|
+
return hit && { node: hit, how: STATIC2 };
|
|
3141
3700
|
}
|
|
3142
3701
|
if (rest.length === 0) {
|
|
3143
3702
|
for (const scope of [...fnScopes].reverse()) {
|
|
3144
3703
|
const hit = f.byQname.get(`${scope}.${head}`);
|
|
3145
|
-
if (hit) return { node: hit, how:
|
|
3704
|
+
if (hit) return { node: hit, how: STATIC2 };
|
|
3146
3705
|
}
|
|
3147
3706
|
}
|
|
3148
3707
|
const local = f.byQname.get(callee.join("."));
|
|
3149
|
-
if (local) return { node: local, how:
|
|
3708
|
+
if (local) return { node: local, how: STATIC2 };
|
|
3150
3709
|
for (let i = callee.length - 1; i >= 1; i--) {
|
|
3151
3710
|
const b2 = binds.get(callee.slice(0, i).join("."));
|
|
3152
3711
|
if (!b2) continue;
|
|
3153
3712
|
const hit = inModule(b2.module, [...b2.member ? [b2.member] : [], ...callee.slice(i)]);
|
|
3154
|
-
return hit && { node: hit, how:
|
|
3713
|
+
return hit && { node: hit, how: CROSS3 };
|
|
3155
3714
|
}
|
|
3156
3715
|
const b = binds.get(head);
|
|
3157
3716
|
if (b?.member && rest.length === 0) {
|
|
3158
3717
|
const hit = inModule(b.module, [b.member]);
|
|
3159
|
-
return hit && { node: hit, how:
|
|
3718
|
+
return hit && { node: hit, how: CROSS3 };
|
|
3160
3719
|
}
|
|
3161
3720
|
return void 0;
|
|
3162
3721
|
};
|
|
@@ -3175,18 +3734,18 @@ function buildGraph2(parsed) {
|
|
|
3175
3734
|
function indexPython(root, files, tagsFor2) {
|
|
3176
3735
|
const parsed = [];
|
|
3177
3736
|
for (const file of [...files].sort()) {
|
|
3178
|
-
const text =
|
|
3179
|
-
const tree =
|
|
3737
|
+
const text = fs8.readFileSync(path8.join(root, file), "utf8");
|
|
3738
|
+
const tree = parser3.parse(text);
|
|
3180
3739
|
if (!tree) continue;
|
|
3181
3740
|
try {
|
|
3182
|
-
parsed.push(
|
|
3741
|
+
parsed.push(collect3(file, text, tagsFor2(file), tree.rootNode));
|
|
3183
3742
|
} finally {
|
|
3184
3743
|
tree.delete();
|
|
3185
3744
|
}
|
|
3186
3745
|
}
|
|
3187
|
-
return
|
|
3746
|
+
return buildGraph3(parsed);
|
|
3188
3747
|
}
|
|
3189
|
-
var
|
|
3748
|
+
var parser3, STATIC2, CROSS3, LOOPS2, stripQuotes;
|
|
3190
3749
|
var init_python = __esm({
|
|
3191
3750
|
async "packages/engine/src/indexer/python.ts"() {
|
|
3192
3751
|
"use strict";
|
|
@@ -3195,23 +3754,23 @@ var init_python = __esm({
|
|
|
3195
3754
|
init_edges();
|
|
3196
3755
|
init_tree_sitter();
|
|
3197
3756
|
await treeSitterReady;
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3757
|
+
parser3 = new Parser4();
|
|
3758
|
+
parser3.setLanguage(await Language3.load(grammarWasm("tree-sitter-python")));
|
|
3759
|
+
STATIC2 = [["static"], 0.95];
|
|
3760
|
+
CROSS3 = [["inferred"], 0.9];
|
|
3761
|
+
LOOPS2 = /* @__PURE__ */ new Set(["for_statement", "while_statement", "list_comprehension", "for_in_clause"]);
|
|
3203
3762
|
stripQuotes = (s) => s.replace(/^[rbuf]*("""|'''|"|')/i, "").replace(/("""|'''|"|')$/, "").trim();
|
|
3204
3763
|
}
|
|
3205
3764
|
});
|
|
3206
3765
|
|
|
3207
3766
|
// packages/engine/src/indexer/rust.ts
|
|
3208
|
-
import
|
|
3209
|
-
import
|
|
3210
|
-
import { Language as
|
|
3767
|
+
import fs9 from "node:fs";
|
|
3768
|
+
import path9 from "node:path";
|
|
3769
|
+
import { Language as Language4, Parser as Parser5 } from "web-tree-sitter";
|
|
3211
3770
|
function moduleOf(root, file) {
|
|
3212
|
-
for (let dir =
|
|
3213
|
-
if (
|
|
3214
|
-
const rel =
|
|
3771
|
+
for (let dir = path9.posix.dirname(file); ; dir = path9.posix.dirname(dir)) {
|
|
3772
|
+
if (fs9.existsSync(path9.join(root, dir, "Cargo.toml"))) {
|
|
3773
|
+
const rel = path9.posix.relative(path9.posix.join(dir, "src"), file);
|
|
3215
3774
|
const bin = rel === "main.rs" || rel.startsWith("bin/") || rel.startsWith("..");
|
|
3216
3775
|
if (bin) return { crate: file, segs: [], lib: dir };
|
|
3217
3776
|
const segs = rel.replace(/\.rs$/, "").split("/");
|
|
@@ -3226,7 +3785,7 @@ function moduleOf(root, file) {
|
|
|
3226
3785
|
function libName(root, crate) {
|
|
3227
3786
|
let toml;
|
|
3228
3787
|
try {
|
|
3229
|
-
toml =
|
|
3788
|
+
toml = fs9.readFileSync(path9.join(root, crate, "Cargo.toml"), "utf8");
|
|
3230
3789
|
} catch {
|
|
3231
3790
|
return void 0;
|
|
3232
3791
|
}
|
|
@@ -3272,12 +3831,12 @@ function pathSegs(n) {
|
|
|
3272
3831
|
const h = pathSegs(head);
|
|
3273
3832
|
return h ? [...h, name] : void 0;
|
|
3274
3833
|
}
|
|
3275
|
-
function
|
|
3834
|
+
function collect4(root, file, text, tags, tree, ix) {
|
|
3276
3835
|
const lineCount2 = text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
3277
3836
|
const fileNode = {
|
|
3278
3837
|
id: fileId(file),
|
|
3279
3838
|
kind: "file",
|
|
3280
|
-
name:
|
|
3839
|
+
name: path9.posix.basename(file),
|
|
3281
3840
|
file,
|
|
3282
3841
|
range: { startLine: 1, endLine: Math.max(1, lineCount2) },
|
|
3283
3842
|
bodyHash: bodyHash(text),
|
|
@@ -3419,7 +3978,7 @@ function collect3(root, file, text, tags, tree, ix) {
|
|
|
3419
3978
|
});
|
|
3420
3979
|
}
|
|
3421
3980
|
}
|
|
3422
|
-
walk2(c, block2, mod, prefix, implType, inLoop ||
|
|
3981
|
+
walk2(c, block2, mod, prefix, implType, inLoop || LOOPS3.has(c.type), testy);
|
|
3423
3982
|
}
|
|
3424
3983
|
};
|
|
3425
3984
|
walk2(tree, fileNode, fileMod, "", void 0, false, false);
|
|
@@ -3484,17 +4043,17 @@ function indexRust(root, files, tagsFor2) {
|
|
|
3484
4043
|
};
|
|
3485
4044
|
const parsed = [];
|
|
3486
4045
|
for (const file of [...files].sort()) {
|
|
3487
|
-
const text =
|
|
3488
|
-
const tree =
|
|
4046
|
+
const text = fs9.readFileSync(path9.join(root, file), "utf8");
|
|
4047
|
+
const tree = parser4.parse(text);
|
|
3489
4048
|
if (!tree) continue;
|
|
3490
4049
|
try {
|
|
3491
|
-
parsed.push(
|
|
4050
|
+
parsed.push(collect4(root, file, text, tagsFor2(file), tree.rootNode, ix));
|
|
3492
4051
|
} finally {
|
|
3493
4052
|
tree.delete();
|
|
3494
4053
|
}
|
|
3495
4054
|
}
|
|
3496
4055
|
const { edges, add } = edgeSink();
|
|
3497
|
-
const how = (from, to) => ix.fileOf.get(to) === from.split("#")[0] ?
|
|
4056
|
+
const how = (from, to) => ix.fileOf.get(to) === from.split("#")[0] ? STATIC3 : CROSS4;
|
|
3498
4057
|
for (const f of parsed) {
|
|
3499
4058
|
for (const c of f.calls) {
|
|
3500
4059
|
const [head, ...rest] = c.callee;
|
|
@@ -3528,7 +4087,7 @@ function indexRust(root, files, tagsFor2) {
|
|
|
3528
4087
|
const commands = new Map(parsed.flatMap((f) => f.commands.map((c) => [c.name, c.id])));
|
|
3529
4088
|
return { nodes: parsed.flatMap((f) => f.nodes), edges: [...edges.values()], commands };
|
|
3530
4089
|
}
|
|
3531
|
-
var
|
|
4090
|
+
var parser4, STATIC3, CROSS4, LOOPS3, TYPE_ITEMS, modKey;
|
|
3532
4091
|
var init_rust = __esm({
|
|
3533
4092
|
async "packages/engine/src/indexer/rust.ts"() {
|
|
3534
4093
|
"use strict";
|
|
@@ -3537,11 +4096,11 @@ var init_rust = __esm({
|
|
|
3537
4096
|
init_edges();
|
|
3538
4097
|
init_tree_sitter();
|
|
3539
4098
|
await treeSitterReady;
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
4099
|
+
parser4 = new Parser5();
|
|
4100
|
+
parser4.setLanguage(await Language4.load(grammarWasm("tree-sitter-rust")));
|
|
4101
|
+
STATIC3 = [["static"], 0.95];
|
|
4102
|
+
CROSS4 = [["inferred"], 0.9];
|
|
4103
|
+
LOOPS3 = /* @__PURE__ */ new Set(["for_expression", "while_expression", "loop_expression"]);
|
|
3545
4104
|
TYPE_ITEMS = {
|
|
3546
4105
|
struct_item: "class",
|
|
3547
4106
|
enum_item: "class",
|
|
@@ -3554,13 +4113,13 @@ var init_rust = __esm({
|
|
|
3554
4113
|
});
|
|
3555
4114
|
|
|
3556
4115
|
// packages/engine/src/indexer/tauri.ts
|
|
3557
|
-
import
|
|
3558
|
-
import
|
|
4116
|
+
import fs10 from "node:fs";
|
|
4117
|
+
import path10 from "node:path";
|
|
3559
4118
|
function linkTauri(root, files, graph, commands) {
|
|
3560
4119
|
if (commands.size === 0) return [];
|
|
3561
4120
|
const { edges, add } = edgeSink();
|
|
3562
4121
|
for (const file of files) {
|
|
3563
|
-
const text =
|
|
4122
|
+
const text = fs10.readFileSync(path10.join(root, file), "utf8");
|
|
3564
4123
|
if (!text.includes("invoke")) continue;
|
|
3565
4124
|
const blocks = graph.nodes.filter((n) => n.file === file && n.kind !== "file");
|
|
3566
4125
|
for (const m of text.matchAll(INVOKE)) {
|
|
@@ -3589,18 +4148,18 @@ var init_tauri = __esm({
|
|
|
3589
4148
|
});
|
|
3590
4149
|
|
|
3591
4150
|
// packages/engine/src/indexer/index.ts
|
|
3592
|
-
import
|
|
3593
|
-
import
|
|
4151
|
+
import fs11 from "node:fs";
|
|
4152
|
+
import path11 from "node:path";
|
|
3594
4153
|
import { API } from "typescript/unstable/sync";
|
|
3595
4154
|
function isRepoSource(root, abs) {
|
|
3596
|
-
const rel =
|
|
3597
|
-
return !rel.startsWith("..") && !
|
|
4155
|
+
const rel = path11.relative(root, abs);
|
|
4156
|
+
return !rel.startsWith("..") && !path11.isAbsolute(rel) && SOURCE_EXT.test(rel) && !DECL_FILE.test(rel) && !toPosix(rel).split("/").includes("node_modules");
|
|
3598
4157
|
}
|
|
3599
4158
|
function walk(dir, skip, want, out = []) {
|
|
3600
|
-
for (const e of
|
|
3601
|
-
const full =
|
|
4159
|
+
for (const e of fs11.readdirSync(dir, { withFileTypes: true })) {
|
|
4160
|
+
const full = path11.join(dir, e.name);
|
|
3602
4161
|
if (e.isDirectory()) {
|
|
3603
|
-
const nested =
|
|
4162
|
+
const nested = fs11.existsSync(path11.join(full, ".git"));
|
|
3604
4163
|
if (!skip.has(e.name) && !e.name.startsWith(".") && !nested) walk(full, skip, want, out);
|
|
3605
4164
|
} else if (e.isFile() && want(e.name)) {
|
|
3606
4165
|
out.push(full);
|
|
@@ -3609,22 +4168,22 @@ function walk(dir, skip, want, out = []) {
|
|
|
3609
4168
|
return out;
|
|
3610
4169
|
}
|
|
3611
4170
|
function references(config) {
|
|
3612
|
-
if (!
|
|
4171
|
+
if (!fs11.existsSync(config)) return [];
|
|
3613
4172
|
try {
|
|
3614
|
-
const json2 =
|
|
4173
|
+
const json2 = fs11.readFileSync(config, "utf8").replace(/("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (_, str3) => str3 ?? "").replace(/,(\s*[}\]])/g, "$1");
|
|
3615
4174
|
const refs = JSON.parse(json2).references ?? [];
|
|
3616
4175
|
return refs.map((r) => {
|
|
3617
|
-
const p =
|
|
3618
|
-
return
|
|
3619
|
-
}).filter((p) =>
|
|
4176
|
+
const p = path11.resolve(path11.dirname(config), r.path);
|
|
4177
|
+
return fs11.existsSync(p) && fs11.statSync(p).isDirectory() ? path11.join(p, "tsconfig.json") : p;
|
|
4178
|
+
}).filter((p) => fs11.existsSync(p));
|
|
3620
4179
|
} catch {
|
|
3621
4180
|
return [];
|
|
3622
4181
|
}
|
|
3623
4182
|
}
|
|
3624
4183
|
function discover(api, root, sources) {
|
|
3625
|
-
const listed = (c) => api.parseConfigFile(c).fileNames.filter((f) => isRepoSource(root, f)).map((f) =>
|
|
3626
|
-
const rootConfig =
|
|
3627
|
-
if (
|
|
4184
|
+
const listed = (c) => api.parseConfigFile(c).fileNames.filter((f) => isRepoSource(root, f)).map((f) => path11.resolve(f));
|
|
4185
|
+
const rootConfig = path11.join(root, "tsconfig.json");
|
|
4186
|
+
if (fs11.existsSync(rootConfig)) {
|
|
3628
4187
|
const files = listed(rootConfig);
|
|
3629
4188
|
if (files.length) return { units: [{ config: rootConfig, files }], loose: [] };
|
|
3630
4189
|
}
|
|
@@ -3632,7 +4191,7 @@ function discover(api, root, sources) {
|
|
|
3632
4191
|
const configs = [.../* @__PURE__ */ new Set([...found, ...references(rootConfig)])].sort();
|
|
3633
4192
|
const projects = configs.map((config) => ({
|
|
3634
4193
|
config,
|
|
3635
|
-
dir:
|
|
4194
|
+
dir: path11.dirname(config) + path11.sep,
|
|
3636
4195
|
listed: new Set(listed(config)),
|
|
3637
4196
|
files: []
|
|
3638
4197
|
})).filter((p) => p.listed.size).sort((a, b) => b.dir.length - a.dir.length);
|
|
@@ -3654,56 +4213,52 @@ function tagsFor(file) {
|
|
|
3654
4213
|
return tags;
|
|
3655
4214
|
}
|
|
3656
4215
|
function discoverSourceFiles(rootDir) {
|
|
3657
|
-
const root =
|
|
4216
|
+
const root = path11.resolve(rootDir);
|
|
3658
4217
|
const found = walk(
|
|
3659
4218
|
root,
|
|
3660
4219
|
SKIP_DIRS,
|
|
3661
|
-
(n) => isSourceName(n) || LUAU_EXT.test(n) || PY_EXT.test(n) || RS_EXT.test(n)
|
|
4220
|
+
(n) => isSourceName(n) || LUAU_EXT.test(n) || PY_EXT.test(n) || RS_EXT.test(n) || CPP_EXT.test(n)
|
|
3662
4221
|
);
|
|
3663
|
-
const ts4 = found.filter((f) => isSourceName(
|
|
3664
|
-
const rel = found.map((f) => toPosix(
|
|
4222
|
+
const ts4 = found.filter((f) => isSourceName(path11.basename(f)));
|
|
4223
|
+
const rel = found.map((f) => toPosix(path11.relative(root, f)));
|
|
3665
4224
|
return {
|
|
3666
4225
|
ts: ts4,
|
|
3667
4226
|
luau: rel.filter(isLuauSourcePath),
|
|
3668
4227
|
py: rel.filter(isPySourcePath),
|
|
3669
|
-
rs: rel.filter(isRsSourcePath)
|
|
4228
|
+
rs: rel.filter(isRsSourcePath),
|
|
4229
|
+
cpp: rel.filter(isCppSourcePath)
|
|
3670
4230
|
};
|
|
3671
4231
|
}
|
|
3672
4232
|
function mergeGraphs(root, ts4, parts) {
|
|
3673
|
-
const graphs = [parts.ts, parts.luau, parts.py, parts.rs].filter(
|
|
4233
|
+
const graphs = [parts.ts, parts.luau, parts.py, parts.rs, parts.cpp].filter(
|
|
4234
|
+
(g) => !!g
|
|
4235
|
+
);
|
|
3674
4236
|
const merged = { nodes: graphs.flatMap((g) => g.nodes), edges: graphs.flatMap((g) => g.edges) };
|
|
3675
4237
|
if (parts.ts && parts.rs?.commands.size) {
|
|
3676
|
-
const rel = ts4.map((f) => toPosix(
|
|
4238
|
+
const rel = ts4.map((f) => toPosix(path11.relative(root, f)));
|
|
3677
4239
|
merged.edges.push(...linkTauri(root, rel, parts.ts, parts.rs.commands));
|
|
3678
4240
|
}
|
|
3679
4241
|
return merged;
|
|
3680
4242
|
}
|
|
3681
4243
|
function indexRepo(rootDir, cacheRepo = rootDir) {
|
|
3682
|
-
const root =
|
|
3683
|
-
const { ts: ts4, luau, py, rs } = discoverSourceFiles(rootDir);
|
|
4244
|
+
const root = path11.resolve(rootDir);
|
|
4245
|
+
const { ts: ts4, luau, py, rs, cpp } = discoverSourceFiles(rootDir);
|
|
3684
4246
|
return mergeGraphs(root, ts4, {
|
|
3685
4247
|
...ts4.length > 0 && { ts: indexTypeScript(root, ts4) },
|
|
3686
4248
|
...luau.length > 0 && { luau: indexLuau(root, luau, tagsFor, cacheRepo) },
|
|
3687
4249
|
...py.length > 0 && { py: indexPython(root, py, tagsFor) },
|
|
3688
|
-
...rs.length > 0 && { rs: indexRust(root, rs, tagsFor) }
|
|
4250
|
+
...rs.length > 0 && { rs: indexRust(root, rs, tagsFor) },
|
|
4251
|
+
...cpp.length > 0 && { cpp: indexCpp(root, cpp, tagsFor) }
|
|
3689
4252
|
});
|
|
3690
4253
|
}
|
|
3691
4254
|
function filesRead(rootDir) {
|
|
3692
|
-
const root =
|
|
3693
|
-
const
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
(n) => isSourceName(n) || LUAU_EXT.test(n) || FINGERPRINT_CONFIG_NAMES.has(n)
|
|
3697
|
-
);
|
|
3698
|
-
return found.filter((f) => {
|
|
3699
|
-
const base = path10.basename(f);
|
|
3700
|
-
if (isSourceName(base) || FINGERPRINT_CONFIG_NAMES.has(base)) return true;
|
|
3701
|
-
const rel = toPosix(path10.relative(root, f));
|
|
3702
|
-
return !LUAU_DECL.test(rel) && !rel.split("/").some((s) => LUAU_SKIP_DIRS.has(s));
|
|
3703
|
-
});
|
|
4255
|
+
const root = path11.resolve(rootDir);
|
|
4256
|
+
const { ts: ts4, luau, py, rs, cpp } = discoverSourceFiles(root);
|
|
4257
|
+
const rel = [...luau, ...py, ...rs, ...cpp].map((f) => path11.join(root, f));
|
|
4258
|
+
return [...ts4, ...rel, ...walk(root, SKIP_DIRS, (n) => FINGERPRINT_CONFIG_NAMES.has(n))];
|
|
3704
4259
|
}
|
|
3705
4260
|
function openTs(root, sources) {
|
|
3706
|
-
const virtualConfig =
|
|
4261
|
+
const virtualConfig = path11.join(root, VIRTUAL_CONFIG);
|
|
3707
4262
|
const virtualBox = { text: "" };
|
|
3708
4263
|
const api = new API({
|
|
3709
4264
|
cwd: root,
|
|
@@ -3745,7 +4300,7 @@ function collectGraph(root, open) {
|
|
|
3745
4300
|
});
|
|
3746
4301
|
for (const { ctx, sfs } of projects) {
|
|
3747
4302
|
for (const sf of sfs) {
|
|
3748
|
-
const file = toPosix(
|
|
4303
|
+
const file = toPosix(path11.relative(root, sf.fileName));
|
|
3749
4304
|
collectFile(ctx, sf, file, tagsFor(file));
|
|
3750
4305
|
}
|
|
3751
4306
|
}
|
|
@@ -3781,11 +4336,12 @@ function reopenTsLive(root, sources, prevOpen) {
|
|
|
3781
4336
|
prevOpen.api.close();
|
|
3782
4337
|
return indexTsLive(root, sources);
|
|
3783
4338
|
}
|
|
3784
|
-
var SOURCE_EXT, DECL_FILE, LUAU_EXT, LUAU_DECL, LUAU_SKIP_DIRS, PY_EXT, RS_EXT, PY_SKIP_DIRS, SKIP_DIRS, SKIP_CONFIG_DIRS, UTIL_SEGMENTS, DEFAULT_OPTIONS, VIRTUAL_CONFIG, toPosix, isTsSourcePath, isLuauSourcePath, isPySourcePath, isRsSourcePath, isSourceName, FINGERPRINT_CONFIG_NAMES;
|
|
4339
|
+
var SOURCE_EXT, DECL_FILE, LUAU_EXT, LUAU_DECL, LUAU_SKIP_DIRS, PY_EXT, RS_EXT, CPP_EXT, CPP_SKIP_DIRS, PY_SKIP_DIRS, SKIP_DIRS, SKIP_CONFIG_DIRS, UTIL_SEGMENTS, DEFAULT_OPTIONS, VIRTUAL_CONFIG, toPosix, isTsSourcePath, isLuauSourcePath, isPySourcePath, isRsSourcePath, isCppSourcePath, isSourceName, FINGERPRINT_CONFIG_NAMES;
|
|
3785
4340
|
var init_indexer = __esm({
|
|
3786
4341
|
async "packages/engine/src/indexer/index.ts"() {
|
|
3787
4342
|
"use strict";
|
|
3788
4343
|
init_blocks();
|
|
4344
|
+
await init_cpp();
|
|
3789
4345
|
init_edges();
|
|
3790
4346
|
await init_luau();
|
|
3791
4347
|
await init_python();
|
|
@@ -3798,6 +4354,16 @@ var init_indexer = __esm({
|
|
|
3798
4354
|
LUAU_SKIP_DIRS = /* @__PURE__ */ new Set(["vendor", "Packages", "ServerPackages", "DevPackages"]);
|
|
3799
4355
|
PY_EXT = /\.py$/;
|
|
3800
4356
|
RS_EXT = /\.rs$/;
|
|
4357
|
+
CPP_EXT = /\.(c|h|cc|cpp|cxx|hpp|hh|hxx)$/;
|
|
4358
|
+
CPP_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
4359
|
+
"build",
|
|
4360
|
+
"out",
|
|
4361
|
+
"third_party",
|
|
4362
|
+
"external",
|
|
4363
|
+
"extern",
|
|
4364
|
+
"vendor",
|
|
4365
|
+
"deps"
|
|
4366
|
+
]);
|
|
3801
4367
|
PY_SKIP_DIRS = /* @__PURE__ */ new Set(["venv", "env", "site-packages", "__pycache__", "build"]);
|
|
3802
4368
|
SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist"]);
|
|
3803
4369
|
SKIP_CONFIG_DIRS = /* @__PURE__ */ new Set([...SKIP_DIRS, "build", "coverage", "fixtures"]);
|
|
@@ -3815,25 +4381,29 @@ var init_indexer = __esm({
|
|
|
3815
4381
|
types: ["node"]
|
|
3816
4382
|
};
|
|
3817
4383
|
VIRTUAL_CONFIG = "__topo__.tsconfig.json";
|
|
3818
|
-
toPosix = (p) => p.split(
|
|
3819
|
-
isTsSourcePath = (rel) => isSourceName(
|
|
4384
|
+
toPosix = (p) => p.split(path11.sep).join("/");
|
|
4385
|
+
isTsSourcePath = (rel) => isSourceName(path11.posix.basename(rel));
|
|
3820
4386
|
isLuauSourcePath = (rel) => LUAU_EXT.test(rel) && !LUAU_DECL.test(rel) && !rel.split("/").some((s) => LUAU_SKIP_DIRS.has(s));
|
|
3821
4387
|
isPySourcePath = (rel) => PY_EXT.test(rel) && !rel.split("/").some((s) => PY_SKIP_DIRS.has(s));
|
|
3822
4388
|
isRsSourcePath = (rel) => RS_EXT.test(rel) && !rel.split("/").includes("target");
|
|
4389
|
+
isCppSourcePath = (rel) => CPP_EXT.test(rel) && !rel.split("/").some((s) => CPP_SKIP_DIRS.has(s) || s.startsWith("cmake-build-"));
|
|
3823
4390
|
isSourceName = (n) => SOURCE_EXT.test(n) && !DECL_FILE.test(n);
|
|
3824
4391
|
FINGERPRINT_CONFIG_NAMES = /* @__PURE__ */ new Set([
|
|
3825
4392
|
"tsconfig.json",
|
|
3826
4393
|
"package.json",
|
|
3827
4394
|
".luaurc",
|
|
3828
4395
|
"sourcemap.json",
|
|
3829
|
-
"default.project.json"
|
|
4396
|
+
"default.project.json",
|
|
4397
|
+
"Cargo.toml",
|
|
4398
|
+
"Makefile",
|
|
4399
|
+
"CMakeLists.txt"
|
|
3830
4400
|
]);
|
|
3831
4401
|
}
|
|
3832
4402
|
});
|
|
3833
4403
|
|
|
3834
4404
|
// packages/engine/src/lenses.ts
|
|
3835
|
-
import
|
|
3836
|
-
import
|
|
4405
|
+
import fs12 from "node:fs";
|
|
4406
|
+
import path12 from "node:path";
|
|
3837
4407
|
function lensProblem(l) {
|
|
3838
4408
|
if (typeof l.name !== "string" || !l.name) return "missing name";
|
|
3839
4409
|
if (l.mode !== void 0 && l.mode !== "review" && l.mode !== "flow") return "bad mode";
|
|
@@ -3850,10 +4420,10 @@ function lensProblem(l) {
|
|
|
3850
4420
|
return void 0;
|
|
3851
4421
|
}
|
|
3852
4422
|
function loadLenses(dir, warn = console.warn) {
|
|
3853
|
-
const file =
|
|
4423
|
+
const file = path12.join(dir, ".topo", "lenses.json");
|
|
3854
4424
|
let raw;
|
|
3855
4425
|
try {
|
|
3856
|
-
raw = JSON.parse(
|
|
4426
|
+
raw = JSON.parse(fs12.readFileSync(file, "utf8"));
|
|
3857
4427
|
} catch (e) {
|
|
3858
4428
|
if (e.code !== "ENOENT") warn(`topo: ignoring ${file}: ${e}`);
|
|
3859
4429
|
return BUILTIN_LENSES;
|
|
@@ -3950,15 +4520,21 @@ var init_lenses = __esm({
|
|
|
3950
4520
|
});
|
|
3951
4521
|
|
|
3952
4522
|
// packages/engine/src/live.ts
|
|
3953
|
-
import
|
|
4523
|
+
import path13 from "node:path";
|
|
3954
4524
|
function toRel(root, f) {
|
|
3955
|
-
const abs =
|
|
3956
|
-
return toPosix(
|
|
4525
|
+
const abs = path13.isAbsolute(f) ? f : path13.resolve(root, f);
|
|
4526
|
+
return toPosix(path13.relative(root, abs));
|
|
3957
4527
|
}
|
|
3958
4528
|
function createLiveIndexer(root, opts) {
|
|
3959
|
-
const rootAbs =
|
|
4529
|
+
const rootAbs = path13.resolve(root);
|
|
3960
4530
|
const cacheRepo = opts?.cacheRepo ?? rootAbs;
|
|
3961
|
-
let {
|
|
4531
|
+
let {
|
|
4532
|
+
ts: tsFiles,
|
|
4533
|
+
luau: luauFiles,
|
|
4534
|
+
py: pyFiles,
|
|
4535
|
+
rs: rsFiles,
|
|
4536
|
+
cpp: cppFiles
|
|
4537
|
+
} = discoverSourceFiles(rootAbs);
|
|
3962
4538
|
let tsState;
|
|
3963
4539
|
let tsGraph = { nodes: [], edges: [] };
|
|
3964
4540
|
if (tsFiles.length) {
|
|
@@ -3975,17 +4551,19 @@ function createLiveIndexer(root, opts) {
|
|
|
3975
4551
|
}
|
|
3976
4552
|
let pyGraph = pyFiles.length ? indexPython(rootAbs, pyFiles, tagsFor) : { nodes: [], edges: [] };
|
|
3977
4553
|
let rsGraph = rsFiles.length ? indexRust(rootAbs, rsFiles, tagsFor) : void 0;
|
|
4554
|
+
let cppGraph = cppFiles.length ? indexCpp(rootAbs, cppFiles, tagsFor) : { nodes: [], edges: [] };
|
|
3978
4555
|
const all = () => mergeGraphs(rootAbs, tsFiles, {
|
|
3979
4556
|
ts: tsGraph,
|
|
3980
4557
|
luau: luauGraph,
|
|
3981
4558
|
py: pyGraph,
|
|
3982
|
-
...rsGraph && { rs: rsGraph }
|
|
4559
|
+
...rsGraph && { rs: rsGraph },
|
|
4560
|
+
cpp: cppGraph
|
|
3983
4561
|
});
|
|
3984
4562
|
let current = all();
|
|
3985
4563
|
function update(changedFiles) {
|
|
3986
4564
|
const prev = current;
|
|
3987
4565
|
const rel = [...new Set(changedFiles.map((f) => toRel(rootAbs, f)))];
|
|
3988
|
-
const basenames = rel.map((f) =>
|
|
4566
|
+
const basenames = rel.map((f) => path13.posix.basename(f));
|
|
3989
4567
|
const tsConfigChanged = basenames.some((b) => TS_CONFIG_NAMES.has(b));
|
|
3990
4568
|
const luauConfigChanged = basenames.some((b) => LUAU_CONFIG_NAMES.has(b));
|
|
3991
4569
|
const tsTouched = rel.filter(isTsSourcePath);
|
|
@@ -3997,7 +4575,7 @@ function createLiveIndexer(root, opts) {
|
|
|
3997
4575
|
if (!tsConfigChanged && sameFileSet && tsState) {
|
|
3998
4576
|
const r = updateTsLive(
|
|
3999
4577
|
tsState,
|
|
4000
|
-
tsTouched.map((f) =>
|
|
4578
|
+
tsTouched.map((f) => path13.resolve(rootAbs, f))
|
|
4001
4579
|
);
|
|
4002
4580
|
tsState = r.state;
|
|
4003
4581
|
tsGraph = r.graph;
|
|
@@ -4026,6 +4604,10 @@ function createLiveIndexer(root, opts) {
|
|
|
4026
4604
|
rsFiles = discoverSourceFiles(rootAbs).rs;
|
|
4027
4605
|
rsGraph = rsFiles.length ? indexRust(rootAbs, rsFiles, tagsFor) : void 0;
|
|
4028
4606
|
}
|
|
4607
|
+
if (rel.some(isCppSourcePath)) {
|
|
4608
|
+
cppFiles = discoverSourceFiles(rootAbs).cpp;
|
|
4609
|
+
cppGraph = indexCpp(rootAbs, cppFiles, tagsFor);
|
|
4610
|
+
}
|
|
4029
4611
|
current = all();
|
|
4030
4612
|
return { graph: cloneGraph(current), delta: graphDelta(prev, current) };
|
|
4031
4613
|
}
|
|
@@ -4045,6 +4627,7 @@ var init_live = __esm({
|
|
|
4045
4627
|
async "packages/engine/src/live.ts"() {
|
|
4046
4628
|
"use strict";
|
|
4047
4629
|
init_delta();
|
|
4630
|
+
await init_cpp();
|
|
4048
4631
|
await init_indexer();
|
|
4049
4632
|
await init_luau();
|
|
4050
4633
|
await init_python();
|
|
@@ -4308,7 +4891,7 @@ function build(text, root) {
|
|
|
4308
4891
|
}
|
|
4309
4892
|
function highlightLuau(text) {
|
|
4310
4893
|
try {
|
|
4311
|
-
const tree =
|
|
4894
|
+
const tree = parser2.parse(text);
|
|
4312
4895
|
if (!tree) return [];
|
|
4313
4896
|
try {
|
|
4314
4897
|
return build(text, tree.rootNode);
|
|
@@ -4529,15 +5112,15 @@ var init_highlight = __esm({
|
|
|
4529
5112
|
});
|
|
4530
5113
|
|
|
4531
5114
|
// packages/engine/src/source.ts
|
|
4532
|
-
import
|
|
4533
|
-
import
|
|
5115
|
+
import fs13 from "node:fs";
|
|
5116
|
+
import path14 from "node:path";
|
|
4534
5117
|
function readInside(dir, file) {
|
|
4535
|
-
const root =
|
|
4536
|
-
const abs =
|
|
4537
|
-
if (!abs.startsWith(root +
|
|
5118
|
+
const root = path14.resolve(dir);
|
|
5119
|
+
const abs = path14.resolve(root, file);
|
|
5120
|
+
if (!abs.startsWith(root + path14.sep)) return void 0;
|
|
4538
5121
|
try {
|
|
4539
|
-
const text =
|
|
4540
|
-
const { mtimeMs } =
|
|
5122
|
+
const text = fs13.readFileSync(abs, "utf8");
|
|
5123
|
+
const { mtimeMs } = fs13.statSync(abs);
|
|
4541
5124
|
let hit = tokenCache.get(abs);
|
|
4542
5125
|
if (hit?.mtimeMs !== mtimeMs) {
|
|
4543
5126
|
hit = { mtimeMs, tokens: highlight(text, file) };
|
|
@@ -4911,6 +5494,7 @@ __export(src_exports, {
|
|
|
4911
5494
|
indexRepo: () => indexRepo,
|
|
4912
5495
|
indexerVersion: () => indexerVersion,
|
|
4913
5496
|
layerIndex: () => layerIndex,
|
|
5497
|
+
layerMap: () => layerMap,
|
|
4914
5498
|
layersFile: () => layersFile,
|
|
4915
5499
|
lensProblem: () => lensProblem,
|
|
4916
5500
|
loadLenses: () => loadLenses,
|
|
@@ -4919,6 +5503,7 @@ __export(src_exports, {
|
|
|
4919
5503
|
noCache: () => noCache,
|
|
4920
5504
|
nodeId: () => nodeId,
|
|
4921
5505
|
outsideTerritory: () => outsideTerritory,
|
|
5506
|
+
parseLayers: () => parseLayers,
|
|
4922
5507
|
readLayers: () => readLayers,
|
|
4923
5508
|
refreshReview: () => refreshReview,
|
|
4924
5509
|
repoCacheRoot: () => repoCacheRoot,
|
|
@@ -4930,7 +5515,8 @@ __export(src_exports, {
|
|
|
4930
5515
|
tagHubs: () => tagHubs,
|
|
4931
5516
|
territoryContext: () => territoryContext,
|
|
4932
5517
|
toPayload: () => toPayload,
|
|
4933
|
-
visibleSubgraph: () => visibleSubgraph
|
|
5518
|
+
visibleSubgraph: () => visibleSubgraph,
|
|
5519
|
+
writeLayers: () => writeLayers
|
|
4934
5520
|
});
|
|
4935
5521
|
var init_src = __esm({
|
|
4936
5522
|
async "packages/engine/src/index.ts"() {
|
|
@@ -5141,7 +5727,7 @@ var init_graph = __esm({
|
|
|
5141
5727
|
await init_format();
|
|
5142
5728
|
HELP = `Usage: topo graph [path] [--json] [--no-cache]
|
|
5143
5729
|
|
|
5144
|
-
Index a codebase (TypeScript, Luau, Python, Rust) and print a summary with likely entry points (or the full graph with --json).
|
|
5730
|
+
Index a codebase (TypeScript, Luau, Python, Rust, C/C++) and print a summary with likely entry points (or the full graph with --json).
|
|
5145
5731
|
|
|
5146
5732
|
Options:
|
|
5147
5733
|
--json print the full Graph as JSON
|
|
@@ -5216,12 +5802,12 @@ Options:
|
|
|
5216
5802
|
});
|
|
5217
5803
|
|
|
5218
5804
|
// packages/cli/src/activity.ts
|
|
5219
|
-
import
|
|
5805
|
+
import path15 from "node:path";
|
|
5220
5806
|
function toRepoRelative(repo, file) {
|
|
5221
|
-
const abs =
|
|
5222
|
-
const rel =
|
|
5223
|
-
if (rel === "" || rel.startsWith("..") ||
|
|
5224
|
-
return rel.split(
|
|
5807
|
+
const abs = path15.isAbsolute(file) ? file : path15.resolve(repo, file);
|
|
5808
|
+
const rel = path15.relative(repo, abs);
|
|
5809
|
+
if (rel === "" || rel.startsWith("..") || path15.isAbsolute(rel)) return void 0;
|
|
5810
|
+
return rel.split(path15.sep).join("/");
|
|
5225
5811
|
}
|
|
5226
5812
|
function parseActivity(body, repo, inWorktree) {
|
|
5227
5813
|
if (!isRecord(body)) return void 0;
|
|
@@ -5229,7 +5815,7 @@ function parseActivity(body, repo, inWorktree) {
|
|
|
5229
5815
|
if (input) {
|
|
5230
5816
|
const file = input.file_path ?? input.notebook_path;
|
|
5231
5817
|
if (typeof file !== "string") return void 0;
|
|
5232
|
-
const abs =
|
|
5818
|
+
const abs = path15.isAbsolute(file) ? file : path15.resolve(repo, file);
|
|
5233
5819
|
const wt = inWorktree?.(abs);
|
|
5234
5820
|
const rel = wt?.rel ?? toRepoRelative(repo, file);
|
|
5235
5821
|
if (!rel) return void 0;
|
|
@@ -5328,42 +5914,314 @@ Options:
|
|
|
5328
5914
|
}
|
|
5329
5915
|
});
|
|
5330
5916
|
|
|
5917
|
+
// packages/cli/src/permissions.ts
|
|
5918
|
+
import fs14 from "node:fs";
|
|
5919
|
+
import path16 from "node:path";
|
|
5920
|
+
function parseSettings(raw) {
|
|
5921
|
+
const p = isRecord2(raw) ? raw.permissions : void 0;
|
|
5922
|
+
return {
|
|
5923
|
+
version: 1,
|
|
5924
|
+
permissions: PERMISSION_MODES.includes(p) ? p : DEFAULT_SETTINGS.permissions
|
|
5925
|
+
};
|
|
5926
|
+
}
|
|
5927
|
+
function readSettings(repo) {
|
|
5928
|
+
try {
|
|
5929
|
+
return parseSettings(JSON.parse(fs14.readFileSync(settingsFile(repo), "utf8")));
|
|
5930
|
+
} catch {
|
|
5931
|
+
return DEFAULT_SETTINGS;
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
function writeSettings(repo, s) {
|
|
5935
|
+
writeJsonAtomic(settingsFile(repo), s);
|
|
5936
|
+
}
|
|
5937
|
+
function writeJsonAtomic(file, value) {
|
|
5938
|
+
fs14.mkdirSync(path16.dirname(file), { recursive: true });
|
|
5939
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
5940
|
+
fs14.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
|
|
5941
|
+
`);
|
|
5942
|
+
fs14.renameSync(tmp, file);
|
|
5943
|
+
}
|
|
5944
|
+
function workerPermissionArgs(mode) {
|
|
5945
|
+
if (mode === "never") return ["--dangerously-skip-permissions"];
|
|
5946
|
+
const allowed = ["mcp__topo__*", ...mode === "safe" ? SAFE_RULES : []];
|
|
5947
|
+
return [
|
|
5948
|
+
"--permission-mode",
|
|
5949
|
+
"acceptEdits",
|
|
5950
|
+
"--allowedTools",
|
|
5951
|
+
allowed.join(","),
|
|
5952
|
+
"--permission-prompt-tool",
|
|
5953
|
+
"mcp__topo__permission_prompt"
|
|
5954
|
+
];
|
|
5955
|
+
}
|
|
5956
|
+
function ruleFor(toolName, input) {
|
|
5957
|
+
if (toolName !== "Bash") return toolName;
|
|
5958
|
+
const cmd = isRecord2(input) && typeof input.command === "string" ? input.command.trim() : "";
|
|
5959
|
+
if (!cmd || COMPOUND.test(cmd)) return void 0;
|
|
5960
|
+
const words = cmd.split(/\s+/);
|
|
5961
|
+
const [first = "", second] = words;
|
|
5962
|
+
if (second === void 0) return `Bash(${first})`;
|
|
5963
|
+
if (second.startsWith("-")) return `Bash(${cmd})`;
|
|
5964
|
+
const prefix = SUBCOMMANDS.has(first) ? `${first} ${second}` : first;
|
|
5965
|
+
return `Bash(${prefix} *)`;
|
|
5966
|
+
}
|
|
5967
|
+
function ruleMatches(rule, toolName, input) {
|
|
5968
|
+
const m = /^([^(]+)(?:\((.*)\))?$/.exec(rule.trim());
|
|
5969
|
+
if (!m || m[1] !== toolName) return false;
|
|
5970
|
+
const pattern = m[2];
|
|
5971
|
+
if (pattern === void 0) return true;
|
|
5972
|
+
if (toolName !== "Bash") return false;
|
|
5973
|
+
const cmd = isRecord2(input) && typeof input.command === "string" ? input.command.trim() : "";
|
|
5974
|
+
if (!cmd || COMPOUND.test(cmd)) return false;
|
|
5975
|
+
const prefix = /^(.*?)(?::\*| \*)$/.exec(pattern)?.[1];
|
|
5976
|
+
if (prefix === void 0) return cmd === pattern;
|
|
5977
|
+
return cmd === prefix || cmd.startsWith(`${prefix} `);
|
|
5978
|
+
}
|
|
5979
|
+
function readLocalSettings(repo) {
|
|
5980
|
+
try {
|
|
5981
|
+
const raw = JSON.parse(fs14.readFileSync(localSettingsFile(repo), "utf8"));
|
|
5982
|
+
return isRecord2(raw) ? raw : {};
|
|
5983
|
+
} catch {
|
|
5984
|
+
return {};
|
|
5985
|
+
}
|
|
5986
|
+
}
|
|
5987
|
+
function readLocalAllow(repo) {
|
|
5988
|
+
const perms = readLocalSettings(repo).permissions;
|
|
5989
|
+
const allow = isRecord2(perms) ? perms.allow : void 0;
|
|
5990
|
+
return Array.isArray(allow) ? allow.filter((r) => typeof r === "string") : [];
|
|
5991
|
+
}
|
|
5992
|
+
function withAllowRule(doc, rule) {
|
|
5993
|
+
const perms = isRecord2(doc.permissions) ? doc.permissions : {};
|
|
5994
|
+
const allow = Array.isArray(perms.allow) ? perms.allow : [];
|
|
5995
|
+
if (allow.includes(rule)) return doc;
|
|
5996
|
+
return { ...doc, permissions: { ...perms, allow: [...allow, rule] } };
|
|
5997
|
+
}
|
|
5998
|
+
function addLocalAllowRule(repo, rule) {
|
|
5999
|
+
const file = localSettingsFile(repo);
|
|
6000
|
+
let doc = {};
|
|
6001
|
+
if (fs14.existsSync(file)) {
|
|
6002
|
+
const raw = JSON.parse(fs14.readFileSync(file, "utf8"));
|
|
6003
|
+
if (!isRecord2(raw)) throw new Error(`${file} isn't a JSON object; not changing it`);
|
|
6004
|
+
doc = raw;
|
|
6005
|
+
}
|
|
6006
|
+
writeJsonAtomic(file, withAllowRule(doc, rule));
|
|
6007
|
+
}
|
|
6008
|
+
var PERMISSION_MODES, DEFAULT_SETTINGS, SAFE_RULES, isRecord2, settingsFile, SUBCOMMANDS, COMPOUND, localSettingsFile;
|
|
6009
|
+
var init_permissions = __esm({
|
|
6010
|
+
"packages/cli/src/permissions.ts"() {
|
|
6011
|
+
"use strict";
|
|
6012
|
+
PERMISSION_MODES = ["safe", "ask", "never"];
|
|
6013
|
+
DEFAULT_SETTINGS = { version: 1, permissions: "safe" };
|
|
6014
|
+
SAFE_RULES = [
|
|
6015
|
+
"Bash(ls *)",
|
|
6016
|
+
"Bash(pwd)",
|
|
6017
|
+
"Bash(cat *)",
|
|
6018
|
+
"Bash(head *)",
|
|
6019
|
+
"Bash(tail *)",
|
|
6020
|
+
"Bash(wc *)",
|
|
6021
|
+
"Bash(grep *)",
|
|
6022
|
+
"Bash(rg *)",
|
|
6023
|
+
"Bash(find *)",
|
|
6024
|
+
"Bash(git status *)",
|
|
6025
|
+
"Bash(git diff *)",
|
|
6026
|
+
"Bash(git log *)",
|
|
6027
|
+
"Bash(git show *)",
|
|
6028
|
+
"Bash(git blame *)"
|
|
6029
|
+
];
|
|
6030
|
+
isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
6031
|
+
settingsFile = (repo) => path16.join(repo, ".topo", "settings.json");
|
|
6032
|
+
SUBCOMMANDS = /* @__PURE__ */ new Set(["git", "npm", "pnpm", "yarn", "bun", "npx", "cargo", "go", "docker"]);
|
|
6033
|
+
COMPOUND = /&&|\|\||[;|`<>]|\$\(/;
|
|
6034
|
+
localSettingsFile = (repo) => path16.join(repo, ".claude", "settings.local.json");
|
|
6035
|
+
}
|
|
6036
|
+
});
|
|
6037
|
+
|
|
6038
|
+
// packages/cli/src/adapters.ts
|
|
6039
|
+
var AGENT_IDS, mcpUrl, str, rec, claude, GEMINI_EDIT_TOOLS, gemini, codex, ADAPTERS;
|
|
6040
|
+
var init_adapters = __esm({
|
|
6041
|
+
"packages/cli/src/adapters.ts"() {
|
|
6042
|
+
"use strict";
|
|
6043
|
+
init_hook();
|
|
6044
|
+
init_permissions();
|
|
6045
|
+
AGENT_IDS = ["claude", "codex", "gemini"];
|
|
6046
|
+
mcpUrl = (port) => `http://127.0.0.1:${port}/mcp`;
|
|
6047
|
+
str = (v) => typeof v === "string" ? v : void 0;
|
|
6048
|
+
rec = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : {};
|
|
6049
|
+
claude = {
|
|
6050
|
+
id: "claude",
|
|
6051
|
+
name: "Claude",
|
|
6052
|
+
bin: "claude",
|
|
6053
|
+
asksMidRun: true,
|
|
6054
|
+
resumes: true,
|
|
6055
|
+
worker: (t) => ({
|
|
6056
|
+
argv: [
|
|
6057
|
+
"-p",
|
|
6058
|
+
t.prompt,
|
|
6059
|
+
"--output-format",
|
|
6060
|
+
"stream-json",
|
|
6061
|
+
"--verbose",
|
|
6062
|
+
"--mcp-config",
|
|
6063
|
+
"{file:mcp}",
|
|
6064
|
+
"--settings",
|
|
6065
|
+
"{file:settings}",
|
|
6066
|
+
...workerPermissionArgs(t.mode),
|
|
6067
|
+
// Without this, workers run on the user's default model (often the priciest one).
|
|
6068
|
+
"--model",
|
|
6069
|
+
process.env.TOPO_WORKER_MODEL ?? "sonnet",
|
|
6070
|
+
...t.resume ? ["--resume", t.resume] : []
|
|
6071
|
+
],
|
|
6072
|
+
json: {
|
|
6073
|
+
mcp: {
|
|
6074
|
+
mcpServers: {
|
|
6075
|
+
topo: { type: "http", url: mcpUrl(t.port), headers: { "x-topo-run": t.runId } }
|
|
6076
|
+
}
|
|
6077
|
+
},
|
|
6078
|
+
// Its edits come in through the same PostToolUse hook `topo hook` prints.
|
|
6079
|
+
settings: hookSettings(t.port)
|
|
6080
|
+
}
|
|
6081
|
+
}),
|
|
6082
|
+
parse: (o) => {
|
|
6083
|
+
if (o.type === "system" && o.subtype === "init" && str(o.session_id))
|
|
6084
|
+
return [{ kind: "started", session: o.session_id }];
|
|
6085
|
+
if (o.type === "result")
|
|
6086
|
+
return [
|
|
6087
|
+
{
|
|
6088
|
+
kind: "finished",
|
|
6089
|
+
failed: o.is_error === true,
|
|
6090
|
+
summary: str(o.result),
|
|
6091
|
+
costUsd: typeof o.total_cost_usd === "number" ? o.total_cost_usd : void 0
|
|
6092
|
+
}
|
|
6093
|
+
];
|
|
6094
|
+
return [];
|
|
6095
|
+
}
|
|
6096
|
+
};
|
|
6097
|
+
GEMINI_EDIT_TOOLS = /* @__PURE__ */ new Set(["write_file", "replace", "edit"]);
|
|
6098
|
+
gemini = {
|
|
6099
|
+
id: "gemini",
|
|
6100
|
+
name: "Gemini",
|
|
6101
|
+
bin: "gemini",
|
|
6102
|
+
asksMidRun: false,
|
|
6103
|
+
resumes: false,
|
|
6104
|
+
// --resume takes "latest" or a list index, not a session id
|
|
6105
|
+
worker: (t) => ({
|
|
6106
|
+
argv: [
|
|
6107
|
+
"-p",
|
|
6108
|
+
t.prompt,
|
|
6109
|
+
"--output-format",
|
|
6110
|
+
"stream-json",
|
|
6111
|
+
"--skip-trust",
|
|
6112
|
+
"--approval-mode",
|
|
6113
|
+
t.mode === "never" ? "yolo" : "auto_edit",
|
|
6114
|
+
...process.env.TOPO_GEMINI_MODEL ? ["--model", process.env.TOPO_GEMINI_MODEL] : []
|
|
6115
|
+
],
|
|
6116
|
+
env: { GEMINI_CLI_SYSTEM_SETTINGS_PATH: "{file:settings}" },
|
|
6117
|
+
json: {
|
|
6118
|
+
settings: {
|
|
6119
|
+
mcpServers: {
|
|
6120
|
+
topo: { httpUrl: mcpUrl(t.port), headers: { "x-topo-run": t.runId }, trust: true }
|
|
6121
|
+
}
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
6124
|
+
}),
|
|
6125
|
+
parse: (o) => {
|
|
6126
|
+
if (o.type === "init" && str(o.session_id))
|
|
6127
|
+
return [{ kind: "started", session: o.session_id }];
|
|
6128
|
+
if (o.type === "message" && o.role === "assistant" && str(o.content))
|
|
6129
|
+
return [{ kind: "text", text: o.content, append: o.delta === true }];
|
|
6130
|
+
if (o.type === "tool_use" && GEMINI_EDIT_TOOLS.has(str(o.tool_name) ?? "")) {
|
|
6131
|
+
const file = str(rec(o.parameters).file_path);
|
|
6132
|
+
return file ? [{ kind: "edit", file }] : [];
|
|
6133
|
+
}
|
|
6134
|
+
if (o.type === "result") return [{ kind: "finished", failed: o.status !== "success" }];
|
|
6135
|
+
return [];
|
|
6136
|
+
}
|
|
6137
|
+
};
|
|
6138
|
+
codex = {
|
|
6139
|
+
id: "codex",
|
|
6140
|
+
name: "Codex",
|
|
6141
|
+
bin: "codex",
|
|
6142
|
+
asksMidRun: false,
|
|
6143
|
+
resumes: true,
|
|
6144
|
+
worker: (t) => {
|
|
6145
|
+
const opts = [
|
|
6146
|
+
"--json",
|
|
6147
|
+
"-c",
|
|
6148
|
+
`mcp_servers.topo.url=${JSON.stringify(mcpUrl(t.port))}`,
|
|
6149
|
+
"-c",
|
|
6150
|
+
`mcp_servers.topo.http_headers={ "x-topo-run" = ${JSON.stringify(t.runId)} }`,
|
|
6151
|
+
...t.mode === "never" ? ["--dangerously-bypass-approvals-and-sandbox"] : ["-s", "workspace-write", "-c", 'approval_policy="never"'],
|
|
6152
|
+
...process.env.TOPO_CODEX_MODEL ? ["-m", process.env.TOPO_CODEX_MODEL] : []
|
|
6153
|
+
];
|
|
6154
|
+
return {
|
|
6155
|
+
argv: t.resume ? ["exec", "resume", ...opts, t.resume, t.prompt] : ["exec", ...opts, t.prompt]
|
|
6156
|
+
};
|
|
6157
|
+
},
|
|
6158
|
+
parse: (o) => {
|
|
6159
|
+
if (o.type === "thread.started" && str(o.thread_id))
|
|
6160
|
+
return [{ kind: "started", session: o.thread_id }];
|
|
6161
|
+
if (o.type === "item.completed") {
|
|
6162
|
+
const item = rec(o.item);
|
|
6163
|
+
if (item.type === "agent_message" && str(item.text))
|
|
6164
|
+
return [{ kind: "text", text: item.text }];
|
|
6165
|
+
if (item.type === "file_change" && Array.isArray(item.changes))
|
|
6166
|
+
return item.changes.flatMap((c) => {
|
|
6167
|
+
const file = str(rec(c).path);
|
|
6168
|
+
return file ? [{ kind: "edit", file }] : [];
|
|
6169
|
+
});
|
|
6170
|
+
}
|
|
6171
|
+
if (o.type === "turn.completed") return [{ kind: "finished", failed: false }];
|
|
6172
|
+
if (o.type === "turn.failed")
|
|
6173
|
+
return [{ kind: "finished", failed: true, summary: str(rec(o.error).message) }];
|
|
6174
|
+
return [];
|
|
6175
|
+
}
|
|
6176
|
+
};
|
|
6177
|
+
ADAPTERS = { claude, codex, gemini };
|
|
6178
|
+
}
|
|
6179
|
+
});
|
|
6180
|
+
|
|
5331
6181
|
// packages/cli/src/agents.ts
|
|
5332
6182
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
5333
|
-
import
|
|
6183
|
+
import fs15 from "node:fs";
|
|
5334
6184
|
import os2 from "node:os";
|
|
5335
|
-
import
|
|
6185
|
+
import path17 from "node:path";
|
|
6186
|
+
function describeContext(c) {
|
|
6187
|
+
const roots = c.roots ?? [];
|
|
6188
|
+
const what = c.mode === "review" ? "the review of the changes" : c.mode === "flow" ? "a flow" : "a map";
|
|
6189
|
+
const depth = c.depth === void 0 ? "" : c.depth === 0 ? ", just those blocks" : `, with what they call and what calls them ${c.depth} hop(s) out`;
|
|
6190
|
+
const more = roots.length > MAX_CONTEXT_ROOTS ? ` and ${roots.length - MAX_CONTEXT_ROOTS} more` : "";
|
|
6191
|
+
const sel = c.selected?.length ? ` Selected: ${c.selected.join(", ")}.` : "";
|
|
6192
|
+
return roots.length ? `The human's map right now: ${what}, rooted at ${roots.length} block(s)${depth}: ${roots.slice(0, MAX_CONTEXT_ROOTS).join(", ")}${more}.${sel}` : `The human's map right now: ${what}, empty.${sel}`;
|
|
6193
|
+
}
|
|
5336
6194
|
function resolveBin(bin) {
|
|
5337
6195
|
const tryExec = (p) => {
|
|
5338
6196
|
try {
|
|
5339
|
-
|
|
6197
|
+
fs15.accessSync(p, fs15.constants.X_OK);
|
|
5340
6198
|
return p;
|
|
5341
6199
|
} catch {
|
|
5342
6200
|
return void 0;
|
|
5343
6201
|
}
|
|
5344
6202
|
};
|
|
5345
|
-
if (bin.includes(
|
|
5346
|
-
const dirs = (process.env.PATH ?? "").split(
|
|
6203
|
+
if (bin.includes(path17.sep)) return tryExec(bin);
|
|
6204
|
+
const dirs = (process.env.PATH ?? "").split(path17.delimiter).filter(Boolean);
|
|
5347
6205
|
const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD").split(";") : [""];
|
|
5348
6206
|
for (const dir of dirs) {
|
|
5349
6207
|
for (const ext of exts) {
|
|
5350
|
-
const hit = tryExec(
|
|
6208
|
+
const hit = tryExec(path17.join(dir, bin + ext));
|
|
5351
6209
|
if (hit) return hit;
|
|
5352
6210
|
}
|
|
5353
6211
|
}
|
|
5354
6212
|
return void 0;
|
|
5355
6213
|
}
|
|
5356
6214
|
function writeTempJson(prefix, value) {
|
|
5357
|
-
const file =
|
|
6215
|
+
const file = path17.join(
|
|
5358
6216
|
os2.tmpdir(),
|
|
5359
6217
|
`${prefix}-${process.pid}-${Math.random().toString(36).slice(2)}.json`
|
|
5360
6218
|
);
|
|
5361
|
-
|
|
6219
|
+
fs15.writeFileSync(file, JSON.stringify(value));
|
|
5362
6220
|
return file;
|
|
5363
6221
|
}
|
|
5364
6222
|
function rmQuiet(file) {
|
|
5365
6223
|
try {
|
|
5366
|
-
|
|
6224
|
+
fs15.unlinkSync(file);
|
|
5367
6225
|
} catch {
|
|
5368
6226
|
}
|
|
5369
6227
|
}
|
|
@@ -5393,19 +6251,41 @@ function createAgentManager(opts) {
|
|
|
5393
6251
|
const spawn3 = opts.spawn ?? nodeSpawn;
|
|
5394
6252
|
const claudeBin = opts.claudeBin ?? "claude";
|
|
5395
6253
|
const resolved = resolveBin(claudeBin);
|
|
6254
|
+
const installed = /* @__PURE__ */ new Map();
|
|
6255
|
+
for (const id of AGENT_IDS) {
|
|
6256
|
+
const bin = id === "claude" ? resolved : resolveBin(opts.bins?.[id] ?? ADAPTERS[id].bin);
|
|
6257
|
+
if (bin) installed.set(id, bin);
|
|
6258
|
+
}
|
|
5396
6259
|
const runs = /* @__PURE__ */ new Map();
|
|
5397
6260
|
const children = /* @__PURE__ */ new Map();
|
|
5398
6261
|
let runSeq = 0;
|
|
5399
6262
|
let cmdSeq = 0;
|
|
6263
|
+
let exchanges = [];
|
|
6264
|
+
let session;
|
|
5400
6265
|
function emitRuns() {
|
|
5401
6266
|
opts.bus.emit({ type: "runs", runs: [...runs.values()] });
|
|
5402
6267
|
}
|
|
6268
|
+
function emitExchanges() {
|
|
6269
|
+
opts.bus.emit({ type: "exchanges", exchanges });
|
|
6270
|
+
}
|
|
6271
|
+
function updateExchange(id, patch) {
|
|
6272
|
+
const i = exchanges.findIndex((x2) => x2.id === id);
|
|
6273
|
+
const x = exchanges[i];
|
|
6274
|
+
if (!x) return;
|
|
6275
|
+
exchanges = exchanges.with(i, { ...x, ...patch(x) });
|
|
6276
|
+
emitExchanges();
|
|
6277
|
+
}
|
|
6278
|
+
opts.bus.subscribe((e) => {
|
|
6279
|
+
if ((e.type === "explain" || e.type === "focus" || e.type === "lens") && e.command)
|
|
6280
|
+
updateExchange(e.command, () => ({ view: e }));
|
|
6281
|
+
else if (e.type === "ask" && e.command) updateExchange(e.command, () => ({ ask: e.questions }));
|
|
6282
|
+
});
|
|
5403
6283
|
function setTerritoryStatus(territoryId, patch) {
|
|
5404
6284
|
const territories = opts.getTerritories();
|
|
5405
6285
|
const next = territories.map((t) => t.id === territoryId ? { ...t, ...patch } : t);
|
|
5406
6286
|
opts.saveTerritories(next);
|
|
5407
6287
|
}
|
|
5408
|
-
function launch(territoryId, reply) {
|
|
6288
|
+
function launch(territoryId, reply, agent) {
|
|
5409
6289
|
const territory = opts.getTerritories().find((t) => t.id === territoryId);
|
|
5410
6290
|
if (!territory) throw new Error(`no territory "${territoryId}"`);
|
|
5411
6291
|
const territoryRuns = [...runs.values()].filter((r) => r.territoryId === territoryId);
|
|
@@ -5415,99 +6295,97 @@ function createAgentManager(opts) {
|
|
|
5415
6295
|
if (!reply && !territory.note?.trim()) {
|
|
5416
6296
|
throw new Error(`"${territoryId}" has no note; write the task before launching`);
|
|
5417
6297
|
}
|
|
5418
|
-
const
|
|
6298
|
+
const wanted = agent ?? territory.runner;
|
|
6299
|
+
if (wanted !== void 0 && !AGENT_IDS.includes(wanted)) {
|
|
6300
|
+
throw new Error(`unknown agent "${wanted}" (one of ${AGENT_IDS.join(", ")})`);
|
|
6301
|
+
}
|
|
6302
|
+
const adapter = wanted ? ADAPTERS[wanted] : ADAPTERS[[...installed.keys()][0] ?? "claude"];
|
|
6303
|
+
const bin = adapter && installed.get(adapter.id);
|
|
6304
|
+
if (!adapter || !bin) throw new Error(`no \`${adapter?.bin ?? "claude"}\` CLI found on PATH`);
|
|
6305
|
+
const prefix = `${adapter.id}:`;
|
|
6306
|
+
const resumeFrom = reply ? territoryRuns.findLast((r) => r.agent?.startsWith(prefix))?.agent?.slice(prefix.length) : void 0;
|
|
6307
|
+
if (reply && !adapter.resumes) {
|
|
6308
|
+
throw new Error(`${adapter.name} can't continue an earlier run; launch it again instead`);
|
|
6309
|
+
}
|
|
5419
6310
|
if (reply && !resumeFrom) throw new Error(`"${territoryId}" has no earlier run to continue`);
|
|
5420
|
-
if (
|
|
6311
|
+
if (agent && territory.runner !== adapter.id) {
|
|
6312
|
+
setTerritoryStatus(territoryId, { runner: adapter.id });
|
|
6313
|
+
}
|
|
5421
6314
|
const id = `run-${++runSeq}`;
|
|
5422
|
-
const mcpConfig = writeTempJson("topo-mcp", {
|
|
5423
|
-
mcpServers: {
|
|
5424
|
-
topo: {
|
|
5425
|
-
type: "http",
|
|
5426
|
-
url: `http://127.0.0.1:${opts.port()}/mcp`,
|
|
5427
|
-
headers: { "x-topo-run": id }
|
|
5428
|
-
}
|
|
5429
|
-
}
|
|
5430
|
-
});
|
|
5431
|
-
const settings = writeTempJson("topo-settings", hookSettings(opts.port()));
|
|
5432
6315
|
const prompt = reply ?? [
|
|
5433
6316
|
territory.label,
|
|
5434
6317
|
territory.note,
|
|
5435
6318
|
`Call get_territory ${territory.id} first. Stay inside it; if you need blocks outside, call request_territory. After changing a block, call report_change with one sentence on why.`
|
|
5436
6319
|
].filter((s) => !!s).join("\n\n");
|
|
6320
|
+
const spec = adapter.worker({
|
|
6321
|
+
runId: id,
|
|
6322
|
+
prompt,
|
|
6323
|
+
port: opts.port(),
|
|
6324
|
+
mode: opts.permissionMode?.() ?? "safe",
|
|
6325
|
+
resume: resumeFrom
|
|
6326
|
+
});
|
|
6327
|
+
const files = new Map(
|
|
6328
|
+
Object.entries(spec.json ?? {}).map(([k, v]) => [k, writeTempJson(`topo-${k}`, v)])
|
|
6329
|
+
);
|
|
6330
|
+
const fill = (s) => s.replace(/^\{file:(\w+)\}$/, (_, k) => files.get(k) ?? s);
|
|
5437
6331
|
const run8 = {
|
|
5438
6332
|
id,
|
|
5439
6333
|
role: "worker",
|
|
5440
6334
|
territoryId,
|
|
5441
|
-
label:
|
|
6335
|
+
label: `${adapter.name} \xB7 ${territory.label}`,
|
|
5442
6336
|
status: "starting",
|
|
5443
6337
|
startedAt: Date.now()
|
|
5444
6338
|
};
|
|
5445
6339
|
runs.set(id, run8);
|
|
5446
|
-
const child = spawn3(
|
|
5447
|
-
|
|
5448
|
-
[
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
"--settings",
|
|
5457
|
-
settings,
|
|
5458
|
-
...opts.skipPermissions ? ["--dangerously-skip-permissions"] : [
|
|
5459
|
-
"--permission-mode",
|
|
5460
|
-
"acceptEdits",
|
|
5461
|
-
// Topo's own tools never prompt (otherwise every get_territory would wait in the
|
|
5462
|
-
// inbox); everything acceptEdits doesn't cover goes to the inbox via the prompt tool.
|
|
5463
|
-
"--allowedTools",
|
|
5464
|
-
"mcp__topo__*",
|
|
5465
|
-
"--permission-prompt-tool",
|
|
5466
|
-
"mcp__topo__permission_prompt"
|
|
5467
|
-
],
|
|
5468
|
-
// Without this, workers run on the user's default model (often the priciest one).
|
|
5469
|
-
"--model",
|
|
5470
|
-
process.env.TOPO_WORKER_MODEL ?? "sonnet",
|
|
5471
|
-
...resumeFrom ? ["--resume", resumeFrom] : []
|
|
5472
|
-
],
|
|
5473
|
-
{ cwd: opts.repo, stdio: ["ignore", "pipe", "pipe"] }
|
|
5474
|
-
);
|
|
6340
|
+
const child = spawn3(bin, spec.argv.map(fill), {
|
|
6341
|
+
cwd: opts.repo,
|
|
6342
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6343
|
+
...spec.env && {
|
|
6344
|
+
env: {
|
|
6345
|
+
...process.env,
|
|
6346
|
+
...Object.fromEntries(Object.entries(spec.env).map(([k, v]) => [k, fill(v)]))
|
|
6347
|
+
}
|
|
6348
|
+
}
|
|
6349
|
+
});
|
|
5475
6350
|
children.set(id, child);
|
|
5476
6351
|
let gotResult = false;
|
|
6352
|
+
let summary;
|
|
5477
6353
|
const cleanup = () => {
|
|
5478
6354
|
children.delete(id);
|
|
5479
|
-
rmQuiet(
|
|
5480
|
-
rmQuiet(settings);
|
|
6355
|
+
for (const f of files.values()) rmQuiet(f);
|
|
5481
6356
|
};
|
|
5482
6357
|
child.stdout?.on(
|
|
5483
6358
|
"data",
|
|
5484
6359
|
makeLineFeed((obj) => {
|
|
5485
|
-
const
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
6360
|
+
for (const ev of adapter.parse(obj)) {
|
|
6361
|
+
if (ev.kind === "started") {
|
|
6362
|
+
run8.agent = `${prefix}${ev.session}`;
|
|
6363
|
+
run8.status = "running";
|
|
6364
|
+
setTerritoryStatus(territoryId, { agent: run8.agent, status: "active" });
|
|
6365
|
+
emitRuns();
|
|
6366
|
+
} else if (ev.kind === "text") {
|
|
6367
|
+
summary = ev.append ? (summary ?? "") + ev.text : ev.text;
|
|
6368
|
+
} else if (ev.kind === "edit") {
|
|
6369
|
+
if (run8.agent) opts.onEdit?.(run8.agent, ev.file);
|
|
6370
|
+
} else if (ev.kind === "finished") {
|
|
6371
|
+
gotResult = true;
|
|
6372
|
+
run8.status = ev.failed ? "failed" : "done";
|
|
6373
|
+
run8.summary = ev.summary ?? summary;
|
|
6374
|
+
run8.costUsd = ev.costUsd;
|
|
6375
|
+
run8.endedAt = Date.now();
|
|
6376
|
+
setTerritoryStatus(territoryId, { status: "review" });
|
|
6377
|
+
opts.inbox.add({
|
|
6378
|
+
kind: "finished",
|
|
6379
|
+
title: `${territory.label}: ${run8.status === "failed" ? "failed" : "finished"}`,
|
|
6380
|
+
detail: run8.summary,
|
|
6381
|
+
agent: run8.agent,
|
|
6382
|
+
label: run8.label,
|
|
6383
|
+
runId: id,
|
|
6384
|
+
territoryId,
|
|
6385
|
+
nodeIds: territory.nodeIds
|
|
6386
|
+
});
|
|
6387
|
+
emitRuns();
|
|
6388
|
+
}
|
|
5511
6389
|
}
|
|
5512
6390
|
})
|
|
5513
6391
|
);
|
|
@@ -5544,28 +6422,31 @@ function createAgentManager(opts) {
|
|
|
5544
6422
|
const id = `cmd-${++cmdSeq}`;
|
|
5545
6423
|
const contextLine = context ? `
|
|
5546
6424
|
|
|
5547
|
-
|
|
6425
|
+
${describeContext(context)}` : "";
|
|
6426
|
+
exchanges = [
|
|
6427
|
+
...exchanges.slice(-(MAX_EXCHANGES - 1)),
|
|
6428
|
+
{ id, text, status: "running", tools: [], at: Date.now() }
|
|
6429
|
+
];
|
|
5548
6430
|
if (!resolved) {
|
|
5549
6431
|
const nodes = findBlocks(opts.getIndex().graph, text, 20);
|
|
5550
|
-
opts.bus.emit({ type: "focus", nodeIds: nodes.map((n) => n.id) });
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
status: "done",
|
|
5555
|
-
text: "No agent CLI found; showing search results"
|
|
5556
|
-
});
|
|
6432
|
+
opts.bus.emit({ type: "focus", nodeIds: nodes.map((n) => n.id), command: id });
|
|
6433
|
+
const line = nodes.length ? "No agent CLI found, so this is a plain search: the matches are on the map." : "No agent CLI found, so this is a plain search, and nothing matched.";
|
|
6434
|
+
opts.bus.emit({ type: "command", id, status: "done", text: line });
|
|
6435
|
+
updateExchange(id, () => ({ status: "done", answer: line }));
|
|
5557
6436
|
return id;
|
|
5558
6437
|
}
|
|
6438
|
+
emitExchanges();
|
|
5559
6439
|
const mcpConfig = writeTempJson("topo-director-mcp", {
|
|
5560
6440
|
mcpServers: {
|
|
5561
6441
|
topo: {
|
|
5562
6442
|
type: "http",
|
|
5563
6443
|
url: `http://127.0.0.1:${opts.port()}/mcp`,
|
|
5564
|
-
headers: { "x-topo-director": opts.directorToken }
|
|
6444
|
+
headers: { "x-topo-director": opts.directorToken, "x-topo-command": id }
|
|
5565
6445
|
}
|
|
5566
6446
|
}
|
|
5567
6447
|
});
|
|
5568
6448
|
const model = opts.model ?? process.env.TOPO_COMMAND_MODEL ?? "sonnet";
|
|
6449
|
+
const resumeFrom = session;
|
|
5569
6450
|
opts.bus.emit({ type: "command", id, status: "running", text });
|
|
5570
6451
|
const child = spawn3(
|
|
5571
6452
|
resolved,
|
|
@@ -5586,12 +6467,17 @@ Context: ${JSON.stringify(context)}` : "";
|
|
|
5586
6467
|
"--model",
|
|
5587
6468
|
model,
|
|
5588
6469
|
"--append-system-prompt",
|
|
5589
|
-
DIRECTOR_SYSTEM_PROMPT
|
|
6470
|
+
DIRECTOR_SYSTEM_PROMPT,
|
|
6471
|
+
...resumeFrom ? ["--resume", resumeFrom] : []
|
|
5590
6472
|
],
|
|
5591
6473
|
{ cwd: opts.repo, stdio: ["ignore", "pipe", "pipe"] }
|
|
5592
6474
|
);
|
|
5593
6475
|
let gotResult = false;
|
|
5594
6476
|
const cleanup = () => rmQuiet(mcpConfig);
|
|
6477
|
+
const fail = (text2) => {
|
|
6478
|
+
opts.bus.emit({ type: "command", id, status: "failed", text: text2 });
|
|
6479
|
+
updateExchange(id, () => ({ status: "failed", answer: text2 ?? "The director stopped." }));
|
|
6480
|
+
};
|
|
5595
6481
|
child.stdout?.on(
|
|
5596
6482
|
"data",
|
|
5597
6483
|
makeLineFeed((obj) => {
|
|
@@ -5607,48 +6493,76 @@ Context: ${JSON.stringify(context)}` : "";
|
|
|
5607
6493
|
status: "running",
|
|
5608
6494
|
text: `${block2.name}(${shortArgs(block2.input)})`
|
|
5609
6495
|
});
|
|
6496
|
+
const tool = block2.name.replace(/^mcp__topo__/, "");
|
|
6497
|
+
updateExchange(id, (x) => ({ tools: [...x.tools, tool] }));
|
|
5610
6498
|
}
|
|
5611
6499
|
}
|
|
5612
6500
|
} else if (obj.type === "result") {
|
|
5613
6501
|
gotResult = true;
|
|
5614
6502
|
const result = obj;
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
text: result.result
|
|
5620
|
-
});
|
|
6503
|
+
const status = result.is_error ? "failed" : "done";
|
|
6504
|
+
if (result.session_id && exchanges.some((x) => x.id === id)) session = result.session_id;
|
|
6505
|
+
opts.bus.emit({ type: "command", id, status, text: result.result });
|
|
6506
|
+
updateExchange(id, () => ({ status, answer: result.result }));
|
|
5621
6507
|
}
|
|
5622
6508
|
})
|
|
5623
6509
|
);
|
|
5624
6510
|
child.on("error", (e) => {
|
|
5625
|
-
if (!gotResult)
|
|
6511
|
+
if (!gotResult) fail(e.message);
|
|
5626
6512
|
cleanup();
|
|
5627
6513
|
});
|
|
5628
6514
|
child.on("close", () => {
|
|
5629
|
-
if (!gotResult)
|
|
6515
|
+
if (!gotResult) fail();
|
|
5630
6516
|
cleanup();
|
|
5631
6517
|
});
|
|
5632
6518
|
return id;
|
|
5633
6519
|
}
|
|
6520
|
+
function newConversation() {
|
|
6521
|
+
exchanges = [];
|
|
6522
|
+
session = void 0;
|
|
6523
|
+
emitExchanges();
|
|
6524
|
+
}
|
|
5634
6525
|
return {
|
|
5635
6526
|
get available() {
|
|
6527
|
+
return installed.size > 0;
|
|
6528
|
+
},
|
|
6529
|
+
get director() {
|
|
5636
6530
|
return resolved !== void 0;
|
|
5637
6531
|
},
|
|
6532
|
+
agents: () => [...installed.keys()].map((id) => ({
|
|
6533
|
+
id,
|
|
6534
|
+
name: ADAPTERS[id].name,
|
|
6535
|
+
asksMidRun: ADAPTERS[id].asksMidRun,
|
|
6536
|
+
resumes: ADAPTERS[id].resumes
|
|
6537
|
+
})),
|
|
5638
6538
|
list: () => [...runs.values()],
|
|
5639
6539
|
get: (runId) => runs.get(runId),
|
|
5640
6540
|
launch,
|
|
5641
6541
|
stop,
|
|
5642
|
-
runCommand
|
|
6542
|
+
runCommand,
|
|
6543
|
+
exchanges: () => exchanges,
|
|
6544
|
+
newConversation
|
|
5643
6545
|
};
|
|
5644
6546
|
}
|
|
5645
|
-
var DIRECTOR_SYSTEM_PROMPT, isActive;
|
|
6547
|
+
var DIRECTOR_SYSTEM_PROMPT, MAX_EXCHANGES, MAX_CONTEXT_ROOTS, isActive;
|
|
5646
6548
|
var init_agents = __esm({
|
|
5647
6549
|
async "packages/cli/src/agents.ts"() {
|
|
5648
6550
|
"use strict";
|
|
5649
6551
|
await init_src();
|
|
5650
|
-
|
|
5651
|
-
DIRECTOR_SYSTEM_PROMPT =
|
|
6552
|
+
init_adapters();
|
|
6553
|
+
DIRECTOR_SYSTEM_PROMPT = [
|
|
6554
|
+
"You answer the human's questions about this codebase and drive the Topo map for them, through the topo tools only. You never edit code.",
|
|
6555
|
+
"For a question like how does X work, where is X, or what happens when Y: trace it with find, callees, callers and get_block (read the source of the blocks that matter), then call explain once with the steps in execution order. Size it to the question: about 5 steps for the gist, every step that matters when asked for all of it or the details. Pick only blocks that matter to the question; leave out UI, logging and plumbing that merely touches it. If explain reports unresolved steps, fix them and call it again.",
|
|
6556
|
+
"Each message ends with a line saying what the human's map shows right now. For walk me through this, the current map, these changes, or what changed: the blocks are that map's roots (for every change instead, call view with no roots and depth 1 and use its roots). Read them with get_block, then call explain with them in run order (callers before what they call). Each step's text is 2-4 sentences: what the block does, and for a changed block what changed there and why it matters. A tour or walkthrough request always ends in explain, never in only a view change.",
|
|
6557
|
+
"For a request to change the view (a lens, a flow, focusing something), territories, agents or the inbox: use those tools and do it.",
|
|
6558
|
+
"For a request to change the code (fix, add, rename, refactor, remove): the request is the go-ahead, so hand it to a worker without asking. Find the blocks it touches, create_territory with a short label, those blocks, and a note that states the task in full (the worker sees only the note and its blocks), then launch_agent on it. Reply in a line or two: what the worker will do, and that its edits will show on the map. Ask first (with ask_user) only when the request is too vague to write the note.",
|
|
6559
|
+
"To plan a change (plan a change to X, how would I add Y): trace the blocks involved, then call explain with the plan as steps on the blocks it touches (what changes in each and why), and create_territory for each piece of work with a note that states it in full. Use ask_user where the plan forks. Don't launch_agent until the human says go.",
|
|
6560
|
+
`When a request is ambiguous in a way that changes what you'd do (how many agents, which territories to create, how long a walkthrough, which of several areas), call ask_user with concrete options and end your turn; the answer comes as the next message. Don't ask when a sensible default exists, and never ask for a go-ahead on a change already requested. Never end a reply by offering choices in prose ("I can do A, or B. Which?"): put them in ask_user so the human can click one.`,
|
|
6561
|
+
"To sort the code into layers (or regions, areas, parts): call get_layers, read what the files hold, then set_layers with 3-8 layers named by what the code does, so every source file lands in one. Say what each layer is in a line.",
|
|
6562
|
+
"Then reply in plain prose: a few sentences, or a numbered list that mirrors the steps. No headings, no tables, no preamble about which tools you called."
|
|
6563
|
+
].join("\n\n");
|
|
6564
|
+
MAX_EXCHANGES = 50;
|
|
6565
|
+
MAX_CONTEXT_ROOTS = 60;
|
|
5652
6566
|
isActive = (status) => status === "starting" || status === "running";
|
|
5653
6567
|
}
|
|
5654
6568
|
});
|
|
@@ -5690,7 +6604,7 @@ var init_attribution = __esm({
|
|
|
5690
6604
|
|
|
5691
6605
|
// packages/cli/src/editor.ts
|
|
5692
6606
|
import { spawn } from "node:child_process";
|
|
5693
|
-
import
|
|
6607
|
+
import path18 from "node:path";
|
|
5694
6608
|
function editorTemplate(flag) {
|
|
5695
6609
|
return flag ?? process.env.TOPO_EDITOR ?? DEFAULT_EDITOR;
|
|
5696
6610
|
}
|
|
@@ -5719,10 +6633,10 @@ function splitTemplate(template) {
|
|
|
5719
6633
|
return out;
|
|
5720
6634
|
}
|
|
5721
6635
|
function resolveInRepo(repo, file) {
|
|
5722
|
-
const root =
|
|
5723
|
-
const abs =
|
|
5724
|
-
const rel =
|
|
5725
|
-
if (rel === "" || rel.startsWith("..") ||
|
|
6636
|
+
const root = path18.resolve(repo);
|
|
6637
|
+
const abs = path18.resolve(root, file);
|
|
6638
|
+
const rel = path18.relative(root, abs);
|
|
6639
|
+
if (rel === "" || rel.startsWith("..") || path18.isAbsolute(rel)) {
|
|
5726
6640
|
throw new Error(`path outside repository: ${file}`);
|
|
5727
6641
|
}
|
|
5728
6642
|
return abs;
|
|
@@ -5784,6 +6698,18 @@ function createInbox(bus, onDecision) {
|
|
|
5784
6698
|
return {
|
|
5785
6699
|
list: () => items,
|
|
5786
6700
|
add(input) {
|
|
6701
|
+
if (input.kind === "out_of_territory" && input.agent !== void 0) {
|
|
6702
|
+
const idx = items.findIndex(
|
|
6703
|
+
(it) => it.kind === input.kind && it.agent === input.agent && it.status === "open"
|
|
6704
|
+
);
|
|
6705
|
+
if (idx !== -1) {
|
|
6706
|
+
const [prev] = items.splice(idx, 1);
|
|
6707
|
+
const item2 = { ...prev, ...input, at: Date.now() };
|
|
6708
|
+
items.unshift(item2);
|
|
6709
|
+
bus.emit({ type: "inbox", item: item2 });
|
|
6710
|
+
return item2;
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
5787
6713
|
const item = {
|
|
5788
6714
|
...input,
|
|
5789
6715
|
id: `inbox-${++counter}`,
|
|
@@ -5926,6 +6852,72 @@ var init_conflicts = __esm({
|
|
|
5926
6852
|
}
|
|
5927
6853
|
});
|
|
5928
6854
|
|
|
6855
|
+
// packages/cli/src/reviews.ts
|
|
6856
|
+
import fs16 from "node:fs";
|
|
6857
|
+
import path19 from "node:path";
|
|
6858
|
+
function readDoc(repo) {
|
|
6859
|
+
try {
|
|
6860
|
+
const doc = JSON.parse(fs16.readFileSync(reviewsFile(repo), "utf8"));
|
|
6861
|
+
if (doc?.version === 1 && doc.reviews && typeof doc.reviews === "object") return doc;
|
|
6862
|
+
} catch {
|
|
6863
|
+
}
|
|
6864
|
+
return { version: 1, reviews: {} };
|
|
6865
|
+
}
|
|
6866
|
+
function readReviews(repo, key) {
|
|
6867
|
+
const saved = readDoc(repo).reviews[key];
|
|
6868
|
+
if (!saved || typeof saved !== "object") return /* @__PURE__ */ new Map();
|
|
6869
|
+
return new Map(Object.entries(saved).filter(([, r]) => isReview(r)));
|
|
6870
|
+
}
|
|
6871
|
+
function writeReviews(repo, key, reviews) {
|
|
6872
|
+
const doc = readDoc(repo);
|
|
6873
|
+
delete doc.reviews[key];
|
|
6874
|
+
if (reviews.size) doc.reviews[key] = Object.fromEntries(reviews);
|
|
6875
|
+
for (const old of Object.keys(doc.reviews).slice(0, -KEEP_REVIEWS)) delete doc.reviews[old];
|
|
6876
|
+
const file = reviewsFile(repo);
|
|
6877
|
+
fs16.mkdirSync(path19.dirname(file), { recursive: true });
|
|
6878
|
+
const ignore = path19.join(path19.dirname(file), ".gitignore");
|
|
6879
|
+
const ignored = fs16.existsSync(ignore) ? fs16.readFileSync(ignore, "utf8") : "";
|
|
6880
|
+
if (!/^reviews\.json$/m.test(ignored))
|
|
6881
|
+
fs16.appendFileSync(ignore, `${ignored && !ignored.endsWith("\n") ? "\n" : ""}reviews.json
|
|
6882
|
+
`);
|
|
6883
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
6884
|
+
fs16.writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
|
|
6885
|
+
`);
|
|
6886
|
+
fs16.renameSync(tmp, file);
|
|
6887
|
+
}
|
|
6888
|
+
function fingerprint(change, head) {
|
|
6889
|
+
return `${change.changeType}:${head?.bodyHash ?? ""}`;
|
|
6890
|
+
}
|
|
6891
|
+
function currentMark(mark, change, head) {
|
|
6892
|
+
if (!mark) return { review: "pending" };
|
|
6893
|
+
if (mark.decision === "viewed" && mark.fp !== void 0 && mark.fp !== fingerprint(change, head))
|
|
6894
|
+
return { review: "pending", stale: true };
|
|
6895
|
+
return { review: mark.decision, ...mark.note && { note: mark.note } };
|
|
6896
|
+
}
|
|
6897
|
+
function askTerritory(agent, territories) {
|
|
6898
|
+
return agent && agent !== "you" ? territories.find((t) => t.agent === agent)?.id : void 0;
|
|
6899
|
+
}
|
|
6900
|
+
function askReply(asks) {
|
|
6901
|
+
return [
|
|
6902
|
+
"I reviewed your change in Topo. Please make these changes:",
|
|
6903
|
+
...asks.map((a) => `- ${a.nodeId}: ${a.note ?? "needs another look"}`),
|
|
6904
|
+
"After each one, call report_change with one sentence on what you did."
|
|
6905
|
+
].join("\n");
|
|
6906
|
+
}
|
|
6907
|
+
var KEEP_REVIEWS, reviewKey, reviewsFile, isReview;
|
|
6908
|
+
var init_reviews = __esm({
|
|
6909
|
+
"packages/cli/src/reviews.ts"() {
|
|
6910
|
+
"use strict";
|
|
6911
|
+
KEEP_REVIEWS = 20;
|
|
6912
|
+
reviewKey = (cs) => `${cs.baseSha}..${cs.headRef}`;
|
|
6913
|
+
reviewsFile = (repo) => path19.join(repo, ".topo", "reviews.json");
|
|
6914
|
+
isReview = (r) => {
|
|
6915
|
+
const x = r;
|
|
6916
|
+
return !!x && (x.decision === "viewed" || x.decision === "changes_requested") && (x.note === void 0 || typeof x.note === "string") && (x.fp === void 0 || typeof x.fp === "string");
|
|
6917
|
+
};
|
|
6918
|
+
}
|
|
6919
|
+
});
|
|
6920
|
+
|
|
5929
6921
|
// packages/cli/src/live.ts
|
|
5930
6922
|
function createEventBus() {
|
|
5931
6923
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -5980,8 +6972,19 @@ function labelFor(attribution, agent) {
|
|
|
5980
6972
|
}
|
|
5981
6973
|
function annotateChangeSet(changeSet, opts) {
|
|
5982
6974
|
const blocks = changeSet.blocks.map((raw) => {
|
|
5983
|
-
const
|
|
5984
|
-
|
|
6975
|
+
const mark = currentMark(
|
|
6976
|
+
opts.tracking.reviews.get(raw.nodeId),
|
|
6977
|
+
raw,
|
|
6978
|
+
opts.index.byId.get(raw.nodeId)
|
|
6979
|
+
);
|
|
6980
|
+
const agentTerritory = askTerritory(opts.tracking.blockAgent.get(raw.nodeId), opts.territories);
|
|
6981
|
+
const b = {
|
|
6982
|
+
...raw,
|
|
6983
|
+
review: mark.review,
|
|
6984
|
+
...mark.note && { reviewNote: mark.note },
|
|
6985
|
+
...mark.stale && { viewedStale: true },
|
|
6986
|
+
...agentTerritory && { agentTerritory }
|
|
6987
|
+
};
|
|
5985
6988
|
const rationale = opts.tracking.rationale.get(b.nodeId);
|
|
5986
6989
|
const agent = opts.tracking.blockAgent.get(b.nodeId);
|
|
5987
6990
|
const territory = agent ? opts.territories.find((t) => t.agent === agent) : void 0;
|
|
@@ -6013,13 +7016,17 @@ function createLive(opts) {
|
|
|
6013
7016
|
const territory = territories.find(
|
|
6014
7017
|
(t) => t.agent === activity.agent || activity.session !== void 0 && t.agent === activity.session
|
|
6015
7018
|
);
|
|
7019
|
+
const open = opts.inbox.list().find(
|
|
7020
|
+
(it) => it.kind === "out_of_territory" && it.agent === activity.agent && it.status === "open"
|
|
7021
|
+
);
|
|
7022
|
+
const all = [.../* @__PURE__ */ new Set([...open?.nodeIds ?? [], ...outOfTerritory])];
|
|
6016
7023
|
opts.inbox.add({
|
|
6017
7024
|
kind: "out_of_territory",
|
|
6018
|
-
title: `${ownLabel} edited ${
|
|
7025
|
+
title: `${ownLabel} edited ${all.length} block${all.length === 1 ? "" : "s"} outside ${territory?.label ?? "its territory"}`,
|
|
6019
7026
|
agent: activity.agent,
|
|
6020
7027
|
label: activity.label,
|
|
6021
7028
|
territoryId: territory?.id,
|
|
6022
|
-
nodeIds:
|
|
7029
|
+
nodeIds: all
|
|
6023
7030
|
});
|
|
6024
7031
|
}
|
|
6025
7032
|
const changed = nodeIds.map((nodeId2) => ({
|
|
@@ -6113,12 +7120,13 @@ var init_live2 = __esm({
|
|
|
6113
7120
|
await init_src();
|
|
6114
7121
|
await init_attribution();
|
|
6115
7122
|
await init_conflicts();
|
|
7123
|
+
init_reviews();
|
|
6116
7124
|
}
|
|
6117
7125
|
});
|
|
6118
7126
|
|
|
6119
7127
|
// packages/cli/src/mcp.ts
|
|
6120
|
-
import
|
|
6121
|
-
import
|
|
7128
|
+
import fs17 from "node:fs";
|
|
7129
|
+
import path20 from "node:path";
|
|
6122
7130
|
function qualifiedName(n) {
|
|
6123
7131
|
const i = n.id.indexOf("#");
|
|
6124
7132
|
return i === -1 ? n.name : n.id.slice(i + 1);
|
|
@@ -6181,6 +7189,38 @@ function requireString(args, key) {
|
|
|
6181
7189
|
if (typeof v !== "string" || !v) throw new Error(`missing required "${key}"`);
|
|
6182
7190
|
return v;
|
|
6183
7191
|
}
|
|
7192
|
+
function parseQuestions(raw) {
|
|
7193
|
+
if (!Array.isArray(raw) || raw.length < 1 || raw.length > 4)
|
|
7194
|
+
throw new Error('"questions" must hold 1-4 {question, header?, options, multiSelect?} items');
|
|
7195
|
+
const problems = [];
|
|
7196
|
+
const questions = raw.map((q, i) => {
|
|
7197
|
+
const at = `question ${i + 1}`;
|
|
7198
|
+
const r = isRecord3(q) ? q : {};
|
|
7199
|
+
const question = str2(r.question);
|
|
7200
|
+
const header = str2(r.header);
|
|
7201
|
+
const opts = Array.isArray(r.options) ? r.options : [];
|
|
7202
|
+
if (!question) problems.push(`${at}: missing "question"`);
|
|
7203
|
+
if (header.length > 12) problems.push(`${at}: "header" is over 12 characters`);
|
|
7204
|
+
if (opts.length < 2 || opts.length > 4) problems.push(`${at}: give 2-4 options`);
|
|
7205
|
+
const options = opts.map((o, j) => {
|
|
7206
|
+
const label = str2(isRecord3(o) ? o.label : void 0);
|
|
7207
|
+
const description = str2(isRecord3(o) ? o.description : void 0);
|
|
7208
|
+
if (!label) problems.push(`${at}, option ${j + 1}: missing "label"`);
|
|
7209
|
+
return description ? { label, description } : { label };
|
|
7210
|
+
});
|
|
7211
|
+
if (new Set(options.map((o) => o.label)).size < options.length)
|
|
7212
|
+
problems.push(`${at}: option labels repeat`);
|
|
7213
|
+
return {
|
|
7214
|
+
question,
|
|
7215
|
+
...header && { header },
|
|
7216
|
+
options,
|
|
7217
|
+
...r.multiSelect === true && { multiSelect: true }
|
|
7218
|
+
};
|
|
7219
|
+
});
|
|
7220
|
+
if (problems.length)
|
|
7221
|
+
throw new Error(["nothing asked; fix these and call ask_user again:", ...problems].join("\n"));
|
|
7222
|
+
return questions;
|
|
7223
|
+
}
|
|
6184
7224
|
function isToolAllowed(name, ctx) {
|
|
6185
7225
|
const tool = TOOLS[name];
|
|
6186
7226
|
if (!tool) return false;
|
|
@@ -6262,7 +7302,7 @@ function shapeViewTool(deps, args, ctx) {
|
|
|
6262
7302
|
label,
|
|
6263
7303
|
...typeof args.mode === "string" && { mode: args.mode },
|
|
6264
7304
|
...roots && { roots },
|
|
6265
|
-
view:
|
|
7305
|
+
view: isRecord3(args.view) ? args.view : {}
|
|
6266
7306
|
};
|
|
6267
7307
|
const problem = lensProblem(candidate);
|
|
6268
7308
|
if (problem) throw new Error(`invalid lens: ${problem}`);
|
|
@@ -6287,25 +7327,25 @@ function shapeViewTool(deps, args, ctx) {
|
|
|
6287
7327
|
return `proposed lens "${name}"${savedNote}${skipNote}; the human will approve or deny it`;
|
|
6288
7328
|
}
|
|
6289
7329
|
function saveLensToFile(repo, lens) {
|
|
6290
|
-
const file =
|
|
7330
|
+
const file = path20.join(repo, ".topo", "lenses.json");
|
|
6291
7331
|
let existing = [];
|
|
6292
7332
|
try {
|
|
6293
|
-
const raw = JSON.parse(
|
|
7333
|
+
const raw = JSON.parse(fs17.readFileSync(file, "utf8"));
|
|
6294
7334
|
const list = Array.isArray(raw) ? raw : raw?.lenses;
|
|
6295
7335
|
if (Array.isArray(list)) existing = list;
|
|
6296
7336
|
} catch {
|
|
6297
7337
|
}
|
|
6298
7338
|
const byName = new Map(existing.map((l) => [l.name, l]));
|
|
6299
7339
|
byName.set(lens.name, lens);
|
|
6300
|
-
|
|
7340
|
+
fs17.mkdirSync(path20.dirname(file), { recursive: true });
|
|
6301
7341
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
6302
|
-
|
|
7342
|
+
fs17.writeFileSync(tmp, `${JSON.stringify({ lenses: [...byName.values()] }, null, 2)}
|
|
6303
7343
|
`);
|
|
6304
|
-
|
|
7344
|
+
fs17.renameSync(tmp, file);
|
|
6305
7345
|
}
|
|
6306
7346
|
async function callTool(deps, params, ctx) {
|
|
6307
7347
|
const name = params.name;
|
|
6308
|
-
const args =
|
|
7348
|
+
const args = isRecord3(params.arguments) ? params.arguments : {};
|
|
6309
7349
|
if (typeof name !== "string" || !TOOLS[name]) {
|
|
6310
7350
|
return { content: [{ type: "text", text: `unknown tool "${String(name)}"` }], isError: true };
|
|
6311
7351
|
}
|
|
@@ -6330,13 +7370,13 @@ function createMcpHandler(deps) {
|
|
|
6330
7370
|
} catch {
|
|
6331
7371
|
return { status: 200, body: rpcError(null, -32700, "Parse error") };
|
|
6332
7372
|
}
|
|
6333
|
-
if (!
|
|
6334
|
-
const id2 =
|
|
7373
|
+
if (!isRecord3(msg) || msg.jsonrpc !== "2.0" || typeof msg.method !== "string") {
|
|
7374
|
+
const id2 = isRecord3(msg) && typeof msg.id !== "undefined" ? msg.id : null;
|
|
6335
7375
|
return { status: 200, body: rpcError(id2, -32600, "Invalid Request") };
|
|
6336
7376
|
}
|
|
6337
7377
|
if (!("id" in msg)) return { status: 202 };
|
|
6338
7378
|
const id = msg.id;
|
|
6339
|
-
const params =
|
|
7379
|
+
const params = isRecord3(msg.params) ? msg.params : {};
|
|
6340
7380
|
switch (msg.method) {
|
|
6341
7381
|
case "ping":
|
|
6342
7382
|
return { status: 200, body: rpcResult(id, {}) };
|
|
@@ -6373,18 +7413,23 @@ function createMcpHandler(deps) {
|
|
|
6373
7413
|
}
|
|
6374
7414
|
};
|
|
6375
7415
|
}
|
|
6376
|
-
var PROTOCOL_VERSIONS, DEFAULT_PROTOCOL_VERSION, INSTRUCTIONS,
|
|
7416
|
+
var PROTOCOL_VERSIONS, DEFAULT_PROTOCOL_VERSION, INSTRUCTIONS, isRecord3, isStrArr, isDirection, isStatus, str2, MAX_LAYER_FILES, withCommand, MAX_STEPS, TOOLS, rpcResult, rpcError;
|
|
6377
7417
|
var init_mcp = __esm({
|
|
6378
7418
|
async "packages/cli/src/mcp.ts"() {
|
|
6379
7419
|
"use strict";
|
|
6380
7420
|
await init_src();
|
|
7421
|
+
init_permissions();
|
|
6381
7422
|
PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
6382
7423
|
DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
6383
7424
|
INSTRUCTIONS = "This is the live code map the human reviewing this repo is looking at right now. Prefer these tools over grep when you need to know who calls something, what would break if you change it, or where something lives \u2014 they're backed by a resolved call graph, not text search.";
|
|
6384
|
-
|
|
7425
|
+
isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6385
7426
|
isStrArr = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
6386
7427
|
isDirection = (v) => v === "out" || v === "in" || v === "both";
|
|
6387
7428
|
isStatus = (v) => v === "proposed" || v === "active" || v === "review" || v === "done";
|
|
7429
|
+
str2 = (v) => typeof v === "string" ? v.trim() : "";
|
|
7430
|
+
MAX_LAYER_FILES = 300;
|
|
7431
|
+
withCommand = (ctx) => ctx.command ? { command: ctx.command } : {};
|
|
7432
|
+
MAX_STEPS = 40;
|
|
6388
7433
|
TOOLS = {
|
|
6389
7434
|
find: {
|
|
6390
7435
|
description: "Search blocks (files, classes, functions, methods) by name or text. Use this instead of grep to locate where something lives before reading or changing it.",
|
|
@@ -6563,21 +7608,21 @@ var init_mcp = __esm({
|
|
|
6563
7608
|
}
|
|
6564
7609
|
},
|
|
6565
7610
|
review_feedback: {
|
|
6566
|
-
description: "The human's review of changed blocks: which need changes (with their note) and how many they
|
|
7611
|
+
description: "The human's review of changed blocks: which need changes (with their note) and how many they've looked at. Use when you finish, or when told your change was reviewed. An agent Topo launched sees only its own territory and the blocks it changed.",
|
|
6567
7612
|
inputSchema: { type: "object", properties: {} },
|
|
6568
7613
|
run: (deps, _args, ctx) => {
|
|
6569
7614
|
const index = deps.getIndex();
|
|
6570
7615
|
const run8 = ctx.runId ? deps.agents?.get(ctx.runId) : void 0;
|
|
6571
7616
|
const territory = run8 && deps.getTerritories().find((t) => t.id === run8.territoryId);
|
|
6572
7617
|
const mine = (id) => !territory || deps.tracking.blockAgent.get(id) === run8?.agent || outsideTerritory(index, territory.nodeIds, [id]).length === 0;
|
|
6573
|
-
const reviewed = deps.getPayload().changeSet.blocks.filter((b) => b.review === "
|
|
7618
|
+
const reviewed = deps.getPayload().changeSet.blocks.filter((b) => b.review === "viewed" || b.review === "changes_requested").filter((b) => mine(b.nodeId));
|
|
6574
7619
|
if (!reviewed.length) return "no review feedback yet";
|
|
6575
7620
|
const asks = reviewed.filter((b) => b.review === "changes_requested");
|
|
6576
7621
|
return [
|
|
6577
7622
|
...asks.map(
|
|
6578
7623
|
(b) => `${b.nodeId} CHANGES REQUESTED${b.reviewNote ? `: ${b.reviewNote}` : ""}`
|
|
6579
7624
|
),
|
|
6580
|
-
`${reviewed.length - asks.length} block(s)
|
|
7625
|
+
`${reviewed.length - asks.length} block(s) viewed, no changes asked`
|
|
6581
7626
|
].join("\n");
|
|
6582
7627
|
}
|
|
6583
7628
|
},
|
|
@@ -6596,16 +7641,23 @@ var init_mcp = __esm({
|
|
|
6596
7641
|
const input = args.input;
|
|
6597
7642
|
const run8 = deps.agents?.get(ctx.runId);
|
|
6598
7643
|
const label = run8?.label ?? "An agent";
|
|
6599
|
-
const detail = toolName === "Bash" &&
|
|
7644
|
+
const detail = toolName === "Bash" && isRecord3(input) && typeof input.command === "string" ? input.command : JSON.stringify(input);
|
|
7645
|
+
const allow = JSON.stringify({ behavior: "allow", updatedInput: input });
|
|
7646
|
+
if ((deps.autoAllowRules?.() ?? []).some((r) => ruleMatches(r, toolName, input)))
|
|
7647
|
+
return allow;
|
|
7648
|
+
const rule = ruleFor(toolName, input);
|
|
6600
7649
|
const item = deps.inbox.add({
|
|
6601
7650
|
kind: "permission",
|
|
6602
|
-
|
|
7651
|
+
// The command goes in detail, so the card can show it on one line as code.
|
|
7652
|
+
title: `${label} wants to run ${toolName}`,
|
|
7653
|
+
detail,
|
|
6603
7654
|
agent: run8?.agent,
|
|
6604
7655
|
label: run8?.label,
|
|
6605
|
-
runId: ctx.runId
|
|
7656
|
+
runId: ctx.runId,
|
|
7657
|
+
...rule && { rule, actions: ["approve", "always", "deny"] }
|
|
6606
7658
|
});
|
|
6607
7659
|
const status = await deps.inbox.wait(item.id, 10 * 60 * 1e3);
|
|
6608
|
-
return status === "approved" ?
|
|
7660
|
+
return status === "approved" ? allow : JSON.stringify({ behavior: "deny", message: "The human denied this in Topo." });
|
|
6609
7661
|
}
|
|
6610
7662
|
},
|
|
6611
7663
|
list_territories: {
|
|
@@ -6661,7 +7713,7 @@ var init_mcp = __esm({
|
|
|
6661
7713
|
label: { type: "string" }
|
|
6662
7714
|
}
|
|
6663
7715
|
},
|
|
6664
|
-
run: (deps, args) => {
|
|
7716
|
+
run: (deps, args, ctx) => {
|
|
6665
7717
|
const index = deps.getIndex();
|
|
6666
7718
|
const { ids: roots, skipped } = Array.isArray(args.roots) ? resolveRefs(index, args.roots) : { ids: [], skipped: [] };
|
|
6667
7719
|
if (skipped.length && !roots.length) {
|
|
@@ -6673,11 +7725,16 @@ var init_mcp = __esm({
|
|
|
6673
7725
|
label,
|
|
6674
7726
|
...typeof args.mode === "string" && { mode: args.mode },
|
|
6675
7727
|
...roots.length && { roots },
|
|
6676
|
-
view:
|
|
7728
|
+
view: isRecord3(args.view) ? args.view : {}
|
|
6677
7729
|
};
|
|
6678
7730
|
const problem = lensProblem(candidate);
|
|
6679
7731
|
if (problem) throw new Error(`invalid lens: ${problem}`);
|
|
6680
|
-
deps.bus.emit({
|
|
7732
|
+
deps.bus.emit({
|
|
7733
|
+
type: "lens",
|
|
7734
|
+
lens: candidate,
|
|
7735
|
+
apply: true,
|
|
7736
|
+
...withCommand(ctx)
|
|
7737
|
+
});
|
|
6681
7738
|
const skipNote = skipped.length ? `; unresolved roots skipped: ${skipped.join(", ")}` : "";
|
|
6682
7739
|
return `applied view "${label}"${skipNote}`;
|
|
6683
7740
|
}
|
|
@@ -6690,15 +7747,184 @@ var init_mcp = __esm({
|
|
|
6690
7747
|
properties: { blocks: { type: "array", items: { type: "string" } } },
|
|
6691
7748
|
required: ["blocks"]
|
|
6692
7749
|
},
|
|
6693
|
-
run: (deps, args) => {
|
|
7750
|
+
run: (deps, args, ctx) => {
|
|
6694
7751
|
if (!Array.isArray(args.blocks)) throw new Error('missing required "blocks" (array)');
|
|
6695
7752
|
const { ids, skipped } = resolveRefs(deps.getIndex(), args.blocks);
|
|
6696
7753
|
if (!ids.length) throw new Error("none of those blocks resolved");
|
|
6697
|
-
deps.bus.emit({ type: "focus", nodeIds: ids });
|
|
7754
|
+
deps.bus.emit({ type: "focus", nodeIds: ids, ...withCommand(ctx) });
|
|
6698
7755
|
const skipNote = skipped.length ? `; unresolved blocks skipped: ${skipped.join(", ")}` : "";
|
|
6699
7756
|
return `focused on ${ids.length} block(s)${skipNote}`;
|
|
6700
7757
|
}
|
|
6701
7758
|
},
|
|
7759
|
+
explain: {
|
|
7760
|
+
access: "director",
|
|
7761
|
+
description: `Walk the human through how something works, or through a change: the map tours these blocks in step order, numbered, with a card per step. Use for 'how does X work / where is X / what happens when / walk me through this or the changes' requests, after tracing with find/callees/callers/get_block. Give the steps in execution (or reading) order, sized to the question (about 5 for the gist, every step that matters for all of it, at most ${MAX_STEPS}), only blocks that matter; each step's text says what happens there in 2-4 sentences (for a changed block, also what changed and why it matters).`,
|
|
7762
|
+
inputSchema: {
|
|
7763
|
+
type: "object",
|
|
7764
|
+
properties: {
|
|
7765
|
+
title: { type: "string" },
|
|
7766
|
+
steps: {
|
|
7767
|
+
type: "array",
|
|
7768
|
+
items: {
|
|
7769
|
+
type: "object",
|
|
7770
|
+
properties: { block: { type: "string" }, text: { type: "string" } },
|
|
7771
|
+
required: ["block", "text"]
|
|
7772
|
+
}
|
|
7773
|
+
}
|
|
7774
|
+
},
|
|
7775
|
+
required: ["title", "steps"]
|
|
7776
|
+
},
|
|
7777
|
+
run: (deps, args, ctx) => {
|
|
7778
|
+
const title = requireString(args, "title");
|
|
7779
|
+
const raw = Array.isArray(args.steps) ? args.steps : [];
|
|
7780
|
+
if (!raw.length || raw.length > MAX_STEPS) {
|
|
7781
|
+
throw new Error(`"steps" must hold 1-${MAX_STEPS} {block, text} items`);
|
|
7782
|
+
}
|
|
7783
|
+
const index = deps.getIndex();
|
|
7784
|
+
const steps = [];
|
|
7785
|
+
const problems = [];
|
|
7786
|
+
raw.forEach((s, i) => {
|
|
7787
|
+
const ref = isRecord3(s) && typeof s.block === "string" ? s.block : "";
|
|
7788
|
+
const text = isRecord3(s) && typeof s.text === "string" ? s.text.trim() : "";
|
|
7789
|
+
const r = resolveOrText(index, ref);
|
|
7790
|
+
if ("node" in r) steps.push({ nodeId: r.node.id, text });
|
|
7791
|
+
else problems.push(`step ${i + 1} (${ref || "no block"}): ${r.text}`);
|
|
7792
|
+
});
|
|
7793
|
+
if (problems.length) {
|
|
7794
|
+
throw new Error(
|
|
7795
|
+
["nothing shown; fix these steps and call explain again:", ...problems].join("\n")
|
|
7796
|
+
);
|
|
7797
|
+
}
|
|
7798
|
+
deps.bus.emit({ type: "explain", title, steps, ...withCommand(ctx) });
|
|
7799
|
+
return `showing "${title}": ${steps.length} step(s) on the map`;
|
|
7800
|
+
}
|
|
7801
|
+
},
|
|
7802
|
+
ask_user: {
|
|
7803
|
+
access: "director",
|
|
7804
|
+
description: "Ask the human 1-4 multiple-choice questions, shown as buttons under your reply (they can also type their own answer). Use it when the request is ambiguous in a way that changes what you'd do: how many agents, which territories to make, how long a walkthrough, which of several areas. Each option is a concrete choice you can act on. After calling it, end your turn with at most a line of reply; the answer arrives as the human's next message.",
|
|
7805
|
+
inputSchema: {
|
|
7806
|
+
type: "object",
|
|
7807
|
+
properties: {
|
|
7808
|
+
questions: {
|
|
7809
|
+
type: "array",
|
|
7810
|
+
minItems: 1,
|
|
7811
|
+
maxItems: 4,
|
|
7812
|
+
items: {
|
|
7813
|
+
type: "object",
|
|
7814
|
+
properties: {
|
|
7815
|
+
question: { type: "string" },
|
|
7816
|
+
header: { type: "string", description: "a label of 12 characters at most" },
|
|
7817
|
+
options: {
|
|
7818
|
+
type: "array",
|
|
7819
|
+
minItems: 2,
|
|
7820
|
+
maxItems: 4,
|
|
7821
|
+
items: {
|
|
7822
|
+
type: "object",
|
|
7823
|
+
properties: { label: { type: "string" }, description: { type: "string" } },
|
|
7824
|
+
required: ["label"]
|
|
7825
|
+
}
|
|
7826
|
+
},
|
|
7827
|
+
multiSelect: { type: "boolean" }
|
|
7828
|
+
},
|
|
7829
|
+
required: ["question", "options"]
|
|
7830
|
+
}
|
|
7831
|
+
}
|
|
7832
|
+
},
|
|
7833
|
+
required: ["questions"]
|
|
7834
|
+
},
|
|
7835
|
+
run: (deps, args, ctx) => {
|
|
7836
|
+
const questions = parseQuestions(args.questions);
|
|
7837
|
+
deps.bus.emit({ type: "ask", questions, ...withCommand(ctx) });
|
|
7838
|
+
return `asked ${questions.length} question(s); they show under your reply. End your turn now: the human's choice comes back as their next message.`;
|
|
7839
|
+
}
|
|
7840
|
+
},
|
|
7841
|
+
get_layers: {
|
|
7842
|
+
description: "The repo's layers (.topo/layers.json: named groups of files by path glob, which color the map) and which files are in none. With no layers yet, a folder-based draft. Lists every source file with its main blocks, so you can group them by what the code does.",
|
|
7843
|
+
inputSchema: { type: "object", properties: {} },
|
|
7844
|
+
run: (deps) => {
|
|
7845
|
+
const graph = deps.getReview().head;
|
|
7846
|
+
const doc = readLayers(deps.repo);
|
|
7847
|
+
const lines = [];
|
|
7848
|
+
if (doc) {
|
|
7849
|
+
const { byFile: byFile2 } = layerMap(doc, graph);
|
|
7850
|
+
lines.push("layers (.topo/layers.json):");
|
|
7851
|
+
for (const l of doc.layers) {
|
|
7852
|
+
const n = Object.values(byFile2).filter((x) => x === l.name).length;
|
|
7853
|
+
lines.push(`${l.name} ${l.paths.join(", ")} (${n} files)`);
|
|
7854
|
+
}
|
|
7855
|
+
for (const r of doc.rules)
|
|
7856
|
+
lines.push(`rule: ${r.from} may not reach ${r.notTo}${r.why ? ` (${r.why})` : ""}`);
|
|
7857
|
+
} else {
|
|
7858
|
+
lines.push("no .topo/layers.json yet; folder-based draft:");
|
|
7859
|
+
for (const l of suggestLayers(graph).layers) lines.push(`${l.name} ${l.paths.join(", ")}`);
|
|
7860
|
+
}
|
|
7861
|
+
const byFile = doc ? layerMap(doc, graph).byFile : {};
|
|
7862
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
7863
|
+
for (const n of graph.nodes)
|
|
7864
|
+
if (n.kind !== "file")
|
|
7865
|
+
blocks.set(n.file, [...blocks.get(n.file) ?? [], qualifiedName(n)]);
|
|
7866
|
+
const files = graph.nodes.filter((n) => n.kind === "file").map((n) => n.file).sort();
|
|
7867
|
+
lines.push("", `files (${files.length}):`);
|
|
7868
|
+
for (const f of files.slice(0, MAX_LAYER_FILES)) {
|
|
7869
|
+
const b = blocks.get(f) ?? [];
|
|
7870
|
+
const names = b.slice(0, 5).join(", ") + (b.length > 5 ? ` +${b.length - 5}` : "");
|
|
7871
|
+
lines.push(`${f} [${byFile[f] ?? (doc ? "no layer" : "-")}] ${names}`);
|
|
7872
|
+
}
|
|
7873
|
+
if (files.length > MAX_LAYER_FILES) lines.push(`\u2026 +${files.length - MAX_LAYER_FILES} more`);
|
|
7874
|
+
return lines.join("\n");
|
|
7875
|
+
}
|
|
7876
|
+
},
|
|
7877
|
+
set_layers: {
|
|
7878
|
+
access: "director",
|
|
7879
|
+
description: "Write .topo/layers.json: 3-8 layers named by what the code does (physics, rendering, input), each a list of path globs (src/physics/**, src/render*.cpp, exact files). A file belongs to the first layer that matches; aim for every source file landing in one. Replaces the layers; existing rules are kept unless you pass rules. The map colors blocks by layer right away.",
|
|
7880
|
+
inputSchema: {
|
|
7881
|
+
type: "object",
|
|
7882
|
+
properties: {
|
|
7883
|
+
layers: {
|
|
7884
|
+
type: "array",
|
|
7885
|
+
items: {
|
|
7886
|
+
type: "object",
|
|
7887
|
+
properties: {
|
|
7888
|
+
name: { type: "string" },
|
|
7889
|
+
paths: { type: "array", items: { type: "string" } }
|
|
7890
|
+
},
|
|
7891
|
+
required: ["name", "paths"]
|
|
7892
|
+
}
|
|
7893
|
+
},
|
|
7894
|
+
rules: {
|
|
7895
|
+
type: "array",
|
|
7896
|
+
items: {
|
|
7897
|
+
type: "object",
|
|
7898
|
+
properties: {
|
|
7899
|
+
from: { type: "string" },
|
|
7900
|
+
notTo: { type: "string" },
|
|
7901
|
+
why: { type: "string" }
|
|
7902
|
+
},
|
|
7903
|
+
required: ["from", "notTo"]
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7906
|
+
},
|
|
7907
|
+
required: ["layers"]
|
|
7908
|
+
},
|
|
7909
|
+
run: (deps, args) => {
|
|
7910
|
+
const rules = Array.isArray(args.rules) ? args.rules : readLayers(deps.repo)?.rules ?? [];
|
|
7911
|
+
const doc = parseLayers({ layers: args.layers, rules });
|
|
7912
|
+
if (!doc?.layers.length || doc.layers.some((l) => !l.name || !l.paths.length)) {
|
|
7913
|
+
throw new Error('"layers" must be a non-empty list of {name, paths: [glob, ...]}');
|
|
7914
|
+
}
|
|
7915
|
+
const map = layerMap(doc, deps.getReview().head);
|
|
7916
|
+
writeLayers(deps.repo, doc);
|
|
7917
|
+
deps.bus.emit({ type: "layers", layers: map });
|
|
7918
|
+
const files = deps.getReview().head.nodes.filter((n) => n.kind === "file").map((n) => n.file);
|
|
7919
|
+
const counts = doc.layers.map(
|
|
7920
|
+
(l) => `${l.name}: ${files.filter((f) => map.byFile[f] === l.name).length} files`
|
|
7921
|
+
);
|
|
7922
|
+
const loose = files.filter((f) => !map.byFile[f]);
|
|
7923
|
+
const looseNote = loose.length ? `
|
|
7924
|
+
${loose.length} file(s) in no layer: ${loose.slice(0, 20).join(", ")}${loose.length > 20 ? " \u2026" : ""}` : "";
|
|
7925
|
+
return `wrote .topo/layers.json; ${counts.join(", ")}${looseNote}`;
|
|
7926
|
+
}
|
|
7927
|
+
},
|
|
6702
7928
|
create_territory: {
|
|
6703
7929
|
access: "director",
|
|
6704
7930
|
description: "Create a territory (a region of the map) and optionally hand it to an agent. Use to carve out scope before launching a worker.",
|
|
@@ -6783,16 +8009,17 @@ var init_mcp = __esm({
|
|
|
6783
8009
|
},
|
|
6784
8010
|
launch_agent: {
|
|
6785
8011
|
access: "director",
|
|
6786
|
-
description: "Launch a worker agent on a territory. Use when the human asks to start work.",
|
|
8012
|
+
description: "Launch a worker agent on a territory. Use when the human asks to start work or asks for a code change (you can't edit code; the worker can). `agent` picks the CLI (claude, codex, gemini) when the human names one; otherwise the territory's own or the default.",
|
|
6787
8013
|
inputSchema: {
|
|
6788
8014
|
type: "object",
|
|
6789
|
-
properties: { territory: { type: "string" } },
|
|
8015
|
+
properties: { territory: { type: "string" }, agent: { type: "string" } },
|
|
6790
8016
|
required: ["territory"]
|
|
6791
8017
|
},
|
|
6792
8018
|
run: (deps, args) => {
|
|
6793
8019
|
if (!deps.agents?.available) throw new Error("agent launching isn't available right now");
|
|
6794
8020
|
const territory = requireString(args, "territory");
|
|
6795
|
-
const
|
|
8021
|
+
const agent = typeof args.agent === "string" && args.agent ? args.agent : void 0;
|
|
8022
|
+
const run8 = deps.agents.launch(territory, void 0, agent);
|
|
6796
8023
|
return `launched ${run8.label} (${run8.id}) on territory "${territory}"`;
|
|
6797
8024
|
}
|
|
6798
8025
|
},
|
|
@@ -6867,11 +8094,11 @@ var init_mcp = __esm({
|
|
|
6867
8094
|
});
|
|
6868
8095
|
|
|
6869
8096
|
// packages/cli/src/territories.ts
|
|
6870
|
-
import
|
|
6871
|
-
import
|
|
8097
|
+
import fs18 from "node:fs";
|
|
8098
|
+
import path21 from "node:path";
|
|
6872
8099
|
function readTerritories(repo) {
|
|
6873
8100
|
try {
|
|
6874
|
-
const doc = parseTerritoryDoc(JSON.parse(
|
|
8101
|
+
const doc = parseTerritoryDoc(JSON.parse(fs18.readFileSync(territoriesFile(repo), "utf8")));
|
|
6875
8102
|
if (doc) return doc;
|
|
6876
8103
|
} catch {
|
|
6877
8104
|
}
|
|
@@ -6879,11 +8106,11 @@ function readTerritories(repo) {
|
|
|
6879
8106
|
}
|
|
6880
8107
|
function writeTerritories(repo, doc) {
|
|
6881
8108
|
const file = territoriesFile(repo);
|
|
6882
|
-
|
|
8109
|
+
fs18.mkdirSync(path21.dirname(file), { recursive: true });
|
|
6883
8110
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
6884
|
-
|
|
8111
|
+
fs18.writeFileSync(tmp, `${JSON.stringify(doc, null, 2)}
|
|
6885
8112
|
`);
|
|
6886
|
-
|
|
8113
|
+
fs18.renameSync(tmp, file);
|
|
6887
8114
|
}
|
|
6888
8115
|
function parseTerritoryDoc(v) {
|
|
6889
8116
|
const o = v;
|
|
@@ -6899,6 +8126,7 @@ function parseTerritoryDoc(v) {
|
|
|
6899
8126
|
if (typeof r.label !== "string" || !isStrArr2(r.nodeIds)) return void 0;
|
|
6900
8127
|
if (r.agent !== void 0 && typeof r.agent !== "string") return void 0;
|
|
6901
8128
|
if (r.note !== void 0 && typeof r.note !== "string") return void 0;
|
|
8129
|
+
if (r.runner !== void 0 && typeof r.runner !== "string") return void 0;
|
|
6902
8130
|
if (r.status !== void 0 && !STATUSES.includes(r.status)) return void 0;
|
|
6903
8131
|
seen.add(r.id);
|
|
6904
8132
|
territories.push({
|
|
@@ -6907,6 +8135,7 @@ function parseTerritoryDoc(v) {
|
|
|
6907
8135
|
nodeIds: r.nodeIds,
|
|
6908
8136
|
...r.agent !== void 0 ? { agent: r.agent } : {},
|
|
6909
8137
|
...r.note !== void 0 ? { note: r.note } : {},
|
|
8138
|
+
...r.runner !== void 0 ? { runner: r.runner } : {},
|
|
6910
8139
|
...r.status !== void 0 ? { status: r.status } : {}
|
|
6911
8140
|
});
|
|
6912
8141
|
}
|
|
@@ -6917,7 +8146,7 @@ var init_territories = __esm({
|
|
|
6917
8146
|
"packages/cli/src/territories.ts"() {
|
|
6918
8147
|
"use strict";
|
|
6919
8148
|
SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6920
|
-
territoriesFile = (repo) =>
|
|
8149
|
+
territoriesFile = (repo) => path21.join(repo, ".topo", "territories.json");
|
|
6921
8150
|
isStrArr2 = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
6922
8151
|
STATUSES = ["proposed", "active", "review", "done"];
|
|
6923
8152
|
}
|
|
@@ -6925,9 +8154,9 @@ var init_territories = __esm({
|
|
|
6925
8154
|
|
|
6926
8155
|
// packages/cli/src/server.ts
|
|
6927
8156
|
import crypto from "node:crypto";
|
|
6928
|
-
import
|
|
8157
|
+
import fs19 from "node:fs";
|
|
6929
8158
|
import http from "node:http";
|
|
6930
|
-
import
|
|
8159
|
+
import path22 from "node:path";
|
|
6931
8160
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
6932
8161
|
function send(res, status, body, type) {
|
|
6933
8162
|
res.writeHead(status, { "content-type": type, "cache-control": "no-store" });
|
|
@@ -6966,27 +8195,27 @@ function parseView(v) {
|
|
|
6966
8195
|
}
|
|
6967
8196
|
function parseLayout(v) {
|
|
6968
8197
|
const o = v;
|
|
6969
|
-
if (!
|
|
8198
|
+
if (!isRecord4(o) || o.version !== 1 || !isRecord4(o.views) || !Object.values(o.views).every((pins) => isRecord4(pins) && Object.values(pins).every(isPos))) {
|
|
6970
8199
|
throw new HttpError(400, "invalid layout");
|
|
6971
8200
|
}
|
|
6972
8201
|
return { version: 1, views: o.views };
|
|
6973
8202
|
}
|
|
6974
8203
|
function serveStatic(res, staticDir, urlPath2) {
|
|
6975
|
-
const root =
|
|
6976
|
-
const index =
|
|
8204
|
+
const root = path22.resolve(staticDir);
|
|
8205
|
+
const index = path22.join(root, "index.html");
|
|
6977
8206
|
let decoded;
|
|
6978
8207
|
try {
|
|
6979
8208
|
decoded = decodeURIComponent(urlPath2);
|
|
6980
8209
|
} catch {
|
|
6981
8210
|
throw new HttpError(400, "bad path");
|
|
6982
8211
|
}
|
|
6983
|
-
let file =
|
|
6984
|
-
if (file !== root && !file.startsWith(root +
|
|
6985
|
-
if (!
|
|
6986
|
-
if (!
|
|
6987
|
-
const type = TYPES[
|
|
8212
|
+
let file = path22.resolve(root, `.${decoded}`);
|
|
8213
|
+
if (file !== root && !file.startsWith(root + path22.sep)) throw new HttpError(403, "forbidden");
|
|
8214
|
+
if (!fs19.statSync(file, { throwIfNoEntry: false })?.isFile()) file = index;
|
|
8215
|
+
if (!fs19.existsSync(file)) return send(res, 200, MISSING_DIST, TYPES[".html"]);
|
|
8216
|
+
const type = TYPES[path22.extname(file).toLowerCase()] ?? "application/octet-stream";
|
|
6988
8217
|
res.writeHead(200, { "content-type": type });
|
|
6989
|
-
|
|
8218
|
+
fs19.createReadStream(file).pipe(res);
|
|
6990
8219
|
}
|
|
6991
8220
|
function isLocalhostOrigin(origin) {
|
|
6992
8221
|
try {
|
|
@@ -7002,7 +8231,7 @@ function constantTimeEqual(a, b) {
|
|
|
7002
8231
|
}
|
|
7003
8232
|
function parseCommandContext(v) {
|
|
7004
8233
|
if (v === void 0) return void 0;
|
|
7005
|
-
if (!isRecordLike(v) || v.mode !== void 0 && typeof v.mode !== "string" || v.roots !== void 0 && !isStrArr3(v.roots) || v.selected !== void 0 && !isStrArr3(v.selected)) {
|
|
8234
|
+
if (!isRecordLike(v) || v.mode !== void 0 && typeof v.mode !== "string" || v.roots !== void 0 && !isStrArr3(v.roots) || v.depth !== void 0 && !Number.isInteger(v.depth) || v.selected !== void 0 && !isStrArr3(v.selected)) {
|
|
7006
8235
|
throw new HttpError(400, "invalid command context");
|
|
7007
8236
|
}
|
|
7008
8237
|
return v;
|
|
@@ -7023,12 +8252,15 @@ function createServer(opts) {
|
|
|
7023
8252
|
if (origin && !isLocalhostOrigin(origin)) throw new HttpError(403, "forbidden origin");
|
|
7024
8253
|
const rawRunId = req.headers["x-topo-run"];
|
|
7025
8254
|
const runId = typeof rawRunId === "string" && opts.agents?.get(rawRunId) ? rawRunId : void 0;
|
|
8255
|
+
const director = constantTimeEqual(
|
|
8256
|
+
req.headers["x-topo-director"],
|
|
8257
|
+
opts.directorToken
|
|
8258
|
+
);
|
|
8259
|
+
const command = req.headers["x-topo-command"];
|
|
7026
8260
|
const ctx = {
|
|
7027
8261
|
runId,
|
|
7028
|
-
director
|
|
7029
|
-
|
|
7030
|
-
opts.directorToken
|
|
7031
|
-
)
|
|
8262
|
+
director,
|
|
8263
|
+
...director && typeof command === "string" && { command }
|
|
7032
8264
|
};
|
|
7033
8265
|
const result = await opts.mcp(await readBody(req), ctx);
|
|
7034
8266
|
if (result.body === void 0) {
|
|
@@ -7040,7 +8272,7 @@ function createServer(opts) {
|
|
|
7040
8272
|
if (route === "GET /api/meta") {
|
|
7041
8273
|
return json(res, 200, {
|
|
7042
8274
|
api: API_VERSION,
|
|
7043
|
-
languages: ["typescript", "javascript", "luau", "python", "rust"],
|
|
8275
|
+
languages: ["typescript", "javascript", "luau", "python", "rust", "c", "cpp"],
|
|
7044
8276
|
mcp: "/mcp",
|
|
7045
8277
|
events: "/api/events",
|
|
7046
8278
|
docs: "https://github.com/zhn2605/topo/blob/main/docs/api.md"
|
|
@@ -7079,15 +8311,32 @@ function createServer(opts) {
|
|
|
7079
8311
|
opts.liveEvents?.emit({ type: "territories", territories: doc.territories });
|
|
7080
8312
|
return json(res, 200, doc);
|
|
7081
8313
|
}
|
|
8314
|
+
if (route === "GET /api/settings" && opts.getSettings)
|
|
8315
|
+
return json(res, 200, opts.getSettings());
|
|
8316
|
+
if (route === "PUT /api/settings" && opts.saveSettings) {
|
|
8317
|
+
const body = await readJson(req);
|
|
8318
|
+
if (!PERMISSION_MODES.includes(body?.permissions)) {
|
|
8319
|
+
throw new HttpError(400, `expected {permissions: ${PERMISSION_MODES.join("|")}}`);
|
|
8320
|
+
}
|
|
8321
|
+
return json(res, 200, opts.saveSettings(body));
|
|
8322
|
+
}
|
|
7082
8323
|
if (route === "GET /api/inbox" && opts.inbox) return json(res, 200, opts.inbox.list());
|
|
7083
8324
|
const inboxDecide = req.method === "POST" && /^\/api\/inbox\/[^/]+$/.test(url.pathname);
|
|
7084
8325
|
if (inboxDecide && opts.inbox) {
|
|
7085
8326
|
const id = decodeURIComponent(url.pathname.slice("/api/inbox/".length));
|
|
7086
8327
|
const body = await readJson(req);
|
|
7087
|
-
|
|
7088
|
-
|
|
8328
|
+
const decision = body?.decision;
|
|
8329
|
+
if (decision !== "approve" && decision !== "always" && decision !== "deny" && decision !== "dismiss") {
|
|
8330
|
+
throw new HttpError(400, "expected {decision: approve|always|deny|dismiss}");
|
|
7089
8331
|
}
|
|
7090
|
-
|
|
8332
|
+
if (decision === "always") {
|
|
8333
|
+
const open = opts.inbox.list().find((it) => it.id === id);
|
|
8334
|
+
if (!open?.rule || !open.actions.includes("always") || !opts.allowAlways) {
|
|
8335
|
+
throw new HttpError(400, "this item has no rule to always allow");
|
|
8336
|
+
}
|
|
8337
|
+
opts.allowAlways(open);
|
|
8338
|
+
}
|
|
8339
|
+
const item = opts.inbox.decide(id, decision === "always" ? "approve" : decision);
|
|
7091
8340
|
if (!item) throw new HttpError(404, "no such inbox item");
|
|
7092
8341
|
return json(res, 200, item);
|
|
7093
8342
|
}
|
|
@@ -7097,10 +8346,11 @@ function createServer(opts) {
|
|
|
7097
8346
|
try {
|
|
7098
8347
|
const body = await readJson(req).catch(() => null);
|
|
7099
8348
|
const reply = typeof body?.reply === "string" && body.reply.trim() ? body.reply : void 0;
|
|
8349
|
+
const agent = typeof body?.agent === "string" && body.agent ? body.agent : void 0;
|
|
7100
8350
|
return json(
|
|
7101
8351
|
res,
|
|
7102
8352
|
200,
|
|
7103
|
-
opts.agents.launch(decodeURIComponent(launchMatch[1]), reply)
|
|
8353
|
+
opts.agents.launch(decodeURIComponent(launchMatch[1]), reply, agent)
|
|
7104
8354
|
);
|
|
7105
8355
|
} catch (e) {
|
|
7106
8356
|
throw new HttpError(400, e.message);
|
|
@@ -7120,6 +8370,10 @@ function createServer(opts) {
|
|
|
7120
8370
|
const id = opts.agents.runCommand(body.text, parseCommandContext(body.context));
|
|
7121
8371
|
return json(res, 200, { id });
|
|
7122
8372
|
}
|
|
8373
|
+
if (route === "POST /api/command/new" && opts.agents) {
|
|
8374
|
+
opts.agents.newConversation();
|
|
8375
|
+
return json(res, 200, { ok: true });
|
|
8376
|
+
}
|
|
7123
8377
|
if (route === "POST /api/open") {
|
|
7124
8378
|
const body = await readJson(req);
|
|
7125
8379
|
const line = Number(body?.line ?? 1);
|
|
@@ -7153,11 +8407,11 @@ function createServer(opts) {
|
|
|
7153
8407
|
}
|
|
7154
8408
|
if (route === "POST /api/blocks/review" && opts.reviewBlock) {
|
|
7155
8409
|
const b = await readJson(req);
|
|
7156
|
-
const decision = b?.decision;
|
|
7157
|
-
if (typeof b?.nodeId !== "string" || decision !== "
|
|
8410
|
+
const decision = b?.decision === "approved" ? "viewed" : b?.decision;
|
|
8411
|
+
if (typeof b?.nodeId !== "string" || decision !== "viewed" && decision !== "changes_requested" && decision !== "pending" || b.note !== void 0 && (typeof b.note !== "string" || b.note.length > 4e3)) {
|
|
7158
8412
|
throw new HttpError(
|
|
7159
8413
|
400,
|
|
7160
|
-
"expected {nodeId, decision:
|
|
8414
|
+
"expected {nodeId, decision: viewed|changes_requested|pending, note?}"
|
|
7161
8415
|
);
|
|
7162
8416
|
}
|
|
7163
8417
|
if (!opts.reviewBlock(b.nodeId, decision, b.note)) {
|
|
@@ -7165,6 +8419,15 @@ function createServer(opts) {
|
|
|
7165
8419
|
}
|
|
7166
8420
|
return json(res, 200, { ok: true });
|
|
7167
8421
|
}
|
|
8422
|
+
if (route === "POST /api/blocks/ask" && opts.askChanges) {
|
|
8423
|
+
const b = await readJson(req);
|
|
8424
|
+
if (typeof b?.nodeId !== "string" || typeof b.note !== "string" || !b.note.trim() || b.note.length > 4e3) {
|
|
8425
|
+
throw new HttpError(400, "expected {nodeId, note} with a non-empty note");
|
|
8426
|
+
}
|
|
8427
|
+
const result = opts.askChanges(b.nodeId, b.note.trim());
|
|
8428
|
+
if (!result) throw new HttpError(404, "no such changed block");
|
|
8429
|
+
return json(res, 200, result);
|
|
8430
|
+
}
|
|
7168
8431
|
if (route === "POST /api/activity" && opts.postActivity) {
|
|
7169
8432
|
const body = await readJson(req);
|
|
7170
8433
|
const accepted = opts.postActivity(body);
|
|
@@ -7188,17 +8451,18 @@ function createServer(opts) {
|
|
|
7188
8451
|
}
|
|
7189
8452
|
});
|
|
7190
8453
|
}
|
|
7191
|
-
var KEEPALIVE_MS, REPO_WEB_DIST, DEFAULT_STATIC_DIR, MAX_BODY, TYPES, MISSING_DIST, HttpError, isStrArr3, isPos,
|
|
8454
|
+
var KEEPALIVE_MS, REPO_WEB_DIST, DEFAULT_STATIC_DIR, MAX_BODY, TYPES, MISSING_DIST, HttpError, isStrArr3, isPos, isRecord4, seenShapes, isRecordLike;
|
|
7192
8455
|
var init_server = __esm({
|
|
7193
8456
|
async "packages/cli/src/server.ts"() {
|
|
7194
8457
|
"use strict";
|
|
7195
8458
|
await init_src();
|
|
7196
8459
|
init_editor();
|
|
8460
|
+
init_permissions();
|
|
7197
8461
|
init_territories();
|
|
7198
8462
|
KEEPALIVE_MS = 15e3;
|
|
7199
8463
|
REPO_WEB_DIST = fileURLToPath3(new URL("../../../apps/web/dist/", import.meta.url));
|
|
7200
8464
|
DEFAULT_STATIC_DIR = [fileURLToPath3(new URL("./web/", import.meta.url)), REPO_WEB_DIST].find(
|
|
7201
|
-
(d) =>
|
|
8465
|
+
(d) => fs19.existsSync(path22.join(d, "index.html"))
|
|
7202
8466
|
) ?? REPO_WEB_DIST;
|
|
7203
8467
|
MAX_BODY = 1024 * 1024;
|
|
7204
8468
|
TYPES = {
|
|
@@ -7229,15 +8493,15 @@ var init_server = __esm({
|
|
|
7229
8493
|
};
|
|
7230
8494
|
isStrArr3 = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
7231
8495
|
isPos = (p) => Array.isArray(p) && p.length === 2 && p.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
7232
|
-
|
|
8496
|
+
isRecord4 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7233
8497
|
seenShapes = /* @__PURE__ */ new Set();
|
|
7234
8498
|
isRecordLike = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7235
8499
|
}
|
|
7236
8500
|
});
|
|
7237
8501
|
|
|
7238
8502
|
// packages/cli/src/watch.ts
|
|
7239
|
-
import
|
|
7240
|
-
import
|
|
8503
|
+
import fs20 from "node:fs";
|
|
8504
|
+
import path23 from "node:path";
|
|
7241
8505
|
function isIndexable(relPath) {
|
|
7242
8506
|
const parts = relPath.split(/[\\/]/).filter(Boolean);
|
|
7243
8507
|
const base = parts.at(-1);
|
|
@@ -7279,14 +8543,14 @@ function watchRepo(root, onBatch, opts = {}) {
|
|
|
7279
8543
|
if (watchers.has(dir)) return;
|
|
7280
8544
|
let w;
|
|
7281
8545
|
try {
|
|
7282
|
-
w =
|
|
8546
|
+
w = fs20.watch(dir, (_event, filename) => {
|
|
7283
8547
|
if (!filename) return;
|
|
7284
|
-
const abs =
|
|
7285
|
-
if (!watchers.has(abs) &&
|
|
7286
|
-
if (!isIgnoredDir(
|
|
8548
|
+
const abs = path23.join(dir, filename.toString());
|
|
8549
|
+
if (!watchers.has(abs) && fs20.statSync(abs, { throwIfNoEntry: false })?.isDirectory()) {
|
|
8550
|
+
if (!isIgnoredDir(path23.basename(abs))) walkDirs(abs);
|
|
7287
8551
|
return;
|
|
7288
8552
|
}
|
|
7289
|
-
const rel =
|
|
8553
|
+
const rel = path23.relative(root, abs).split(path23.sep).join("/");
|
|
7290
8554
|
if (!isIndexable(rel)) return;
|
|
7291
8555
|
pending.add(rel);
|
|
7292
8556
|
scheduleFlush();
|
|
@@ -7304,11 +8568,11 @@ function watchRepo(root, onBatch, opts = {}) {
|
|
|
7304
8568
|
watchDir(dir);
|
|
7305
8569
|
let entries = [];
|
|
7306
8570
|
try {
|
|
7307
|
-
entries =
|
|
8571
|
+
entries = fs20.readdirSync(dir, { withFileTypes: true });
|
|
7308
8572
|
} catch {
|
|
7309
8573
|
}
|
|
7310
8574
|
for (const e of entries)
|
|
7311
|
-
if (e.isDirectory() && !isIgnoredDir(e.name)) walkDirs(
|
|
8575
|
+
if (e.isDirectory() && !isIgnoredDir(e.name)) walkDirs(path23.join(dir, e.name));
|
|
7312
8576
|
};
|
|
7313
8577
|
walkDirs(root);
|
|
7314
8578
|
return {
|
|
@@ -7333,8 +8597,8 @@ var init_watch = __esm({
|
|
|
7333
8597
|
|
|
7334
8598
|
// packages/cli/src/worktrees.ts
|
|
7335
8599
|
import { execFileSync } from "node:child_process";
|
|
7336
|
-
import
|
|
7337
|
-
import
|
|
8600
|
+
import fs21 from "node:fs";
|
|
8601
|
+
import path24 from "node:path";
|
|
7338
8602
|
function listWorktrees(repo) {
|
|
7339
8603
|
let out;
|
|
7340
8604
|
try {
|
|
@@ -7348,17 +8612,17 @@ function listWorktrees(repo) {
|
|
|
7348
8612
|
}
|
|
7349
8613
|
const roots = out.split("\n").filter((l) => l.startsWith("worktree ")).map((l) => l.slice("worktree ".length));
|
|
7350
8614
|
const own = roots.find((r) => isInside2(r, repo));
|
|
7351
|
-
return roots.filter((r) => r !== own).map((root) => ({ root, name:
|
|
8615
|
+
return roots.filter((r) => r !== own).map((root) => ({ root, name: path24.basename(root) }));
|
|
7352
8616
|
}
|
|
7353
8617
|
function mapWorktreePath(repo, worktrees, file) {
|
|
7354
8618
|
const wt = worktrees.filter((w) => isInside2(w.root, file)).sort((a, b) => b.root.length - a.root.length)[0];
|
|
7355
8619
|
if (!wt) return void 0;
|
|
7356
8620
|
const mainRoot = gitRoot(repo);
|
|
7357
|
-
const sub = mainRoot ?
|
|
7358
|
-
const inTree =
|
|
7359
|
-
const rel = sub ?
|
|
7360
|
-
if (rel.startsWith("..") ||
|
|
7361
|
-
return { rel: rel.split(
|
|
8621
|
+
const sub = mainRoot ? path24.relative(mainRoot, repo) : "";
|
|
8622
|
+
const inTree = path24.relative(wt.root, file);
|
|
8623
|
+
const rel = sub ? path24.relative(sub, inTree) : inTree;
|
|
8624
|
+
if (rel.startsWith("..") || path24.isAbsolute(rel)) return void 0;
|
|
8625
|
+
return { rel: rel.split(path24.sep).join("/"), worktree: wt };
|
|
7362
8626
|
}
|
|
7363
8627
|
function gitRoot(dir) {
|
|
7364
8628
|
try {
|
|
@@ -7372,7 +8636,7 @@ function gitRoot(dir) {
|
|
|
7372
8636
|
}
|
|
7373
8637
|
}
|
|
7374
8638
|
function changedLines(base, changed) {
|
|
7375
|
-
if (!
|
|
8639
|
+
if (!fs21.existsSync(base) || !fs21.existsSync(changed)) return [];
|
|
7376
8640
|
let text = "";
|
|
7377
8641
|
try {
|
|
7378
8642
|
execFileSync("git", ["diff", "--no-index", "-U0", "--no-color", base, changed], {
|
|
@@ -7405,8 +8669,8 @@ var init_worktrees = __esm({
|
|
|
7405
8669
|
"packages/cli/src/worktrees.ts"() {
|
|
7406
8670
|
"use strict";
|
|
7407
8671
|
isInside2 = (dir, p) => {
|
|
7408
|
-
const rel =
|
|
7409
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
8672
|
+
const rel = path24.relative(dir, p);
|
|
8673
|
+
return rel === "" || !rel.startsWith("..") && !path24.isAbsolute(rel);
|
|
7410
8674
|
};
|
|
7411
8675
|
}
|
|
7412
8676
|
});
|
|
@@ -7422,8 +8686,8 @@ __export(review_exports, {
|
|
|
7422
8686
|
});
|
|
7423
8687
|
import { spawn as spawn2 } from "node:child_process";
|
|
7424
8688
|
import crypto2 from "node:crypto";
|
|
7425
|
-
import
|
|
7426
|
-
import
|
|
8689
|
+
import fs22 from "node:fs";
|
|
8690
|
+
import path25 from "node:path";
|
|
7427
8691
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
7428
8692
|
function openBrowser(url) {
|
|
7429
8693
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
@@ -7462,7 +8726,7 @@ function run4(argv) {
|
|
|
7462
8726
|
if (!base) throw new Error(`missing <base> ref
|
|
7463
8727
|
|
|
7464
8728
|
${HELP4}`);
|
|
7465
|
-
const repo =
|
|
8729
|
+
const repo = path25.resolve(values.repo ?? process.cwd());
|
|
7466
8730
|
return serve({
|
|
7467
8731
|
compute: () => computeReview(repo, base, values.head),
|
|
7468
8732
|
repo,
|
|
@@ -7477,7 +8741,7 @@ ${HELP4}`);
|
|
|
7477
8741
|
}
|
|
7478
8742
|
function readLayout(repo) {
|
|
7479
8743
|
try {
|
|
7480
|
-
const doc = JSON.parse(
|
|
8744
|
+
const doc = JSON.parse(fs22.readFileSync(layoutFile(repo), "utf8"));
|
|
7481
8745
|
if (doc?.version === 1 && doc.views && typeof doc.views === "object") return doc;
|
|
7482
8746
|
} catch {
|
|
7483
8747
|
}
|
|
@@ -7485,12 +8749,12 @@ function readLayout(repo) {
|
|
|
7485
8749
|
}
|
|
7486
8750
|
function writeLayout(repo, doc) {
|
|
7487
8751
|
const file = layoutFile(repo);
|
|
7488
|
-
|
|
8752
|
+
fs22.mkdirSync(path25.dirname(file), { recursive: true });
|
|
7489
8753
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
7490
8754
|
const text = JSON.stringify(doc, null, 2).replace(/\[\s+(-?\d+),\s+(-?\d+)\s+\]/g, "[$1, $2]");
|
|
7491
|
-
|
|
8755
|
+
fs22.writeFileSync(tmp, `${text}
|
|
7492
8756
|
`);
|
|
7493
|
-
|
|
8757
|
+
fs22.renameSync(tmp, file);
|
|
7494
8758
|
}
|
|
7495
8759
|
function serve(opts) {
|
|
7496
8760
|
const port = Number(opts.port);
|
|
@@ -7499,6 +8763,7 @@ function serve(opts) {
|
|
|
7499
8763
|
const { repo } = opts;
|
|
7500
8764
|
const editorCommand = editorTemplate(opts.editor);
|
|
7501
8765
|
const tracking = { blockAgent: /* @__PURE__ */ new Map(), rationale: /* @__PURE__ */ new Map(), reviews: /* @__PURE__ */ new Map() };
|
|
8766
|
+
let marksFor;
|
|
7502
8767
|
const compute = () => {
|
|
7503
8768
|
const review = opts.compute();
|
|
7504
8769
|
tagHubs(review.head);
|
|
@@ -7509,6 +8774,11 @@ function serve(opts) {
|
|
|
7509
8774
|
lenses: loadLenses(repo),
|
|
7510
8775
|
territories
|
|
7511
8776
|
};
|
|
8777
|
+
const key = reviewKey(payload.changeSet);
|
|
8778
|
+
if (key !== marksFor) {
|
|
8779
|
+
marksFor = key;
|
|
8780
|
+
tracking.reviews = readReviews(repo, key);
|
|
8781
|
+
}
|
|
7512
8782
|
payload.changeSet = annotateChangeSet(payload.changeSet, { territories, tracking, index });
|
|
7513
8783
|
return { review, payload, index };
|
|
7514
8784
|
};
|
|
@@ -7539,7 +8809,7 @@ function serve(opts) {
|
|
|
7539
8809
|
const nodeIds = blocksAt(
|
|
7540
8810
|
state.index,
|
|
7541
8811
|
parsed.file,
|
|
7542
|
-
changedLines(
|
|
8812
|
+
changedLines(path25.join(repo, parsed.file), parsed.absFile)
|
|
7543
8813
|
);
|
|
7544
8814
|
const run9 = agents.list().findLast((r) => r.agent === parsed.agent || r.agent === parsed.session);
|
|
7545
8815
|
const { absFile: _abs, worktree, ...activity } = parsed;
|
|
@@ -7571,6 +8841,7 @@ function serve(opts) {
|
|
|
7571
8841
|
return true;
|
|
7572
8842
|
};
|
|
7573
8843
|
const directorToken = crypto2.randomBytes(24).toString("hex");
|
|
8844
|
+
const permissionMode = () => opts.skipPermissions ? "never" : readSettings(repo).permissions;
|
|
7574
8845
|
let boundPort = port;
|
|
7575
8846
|
const agents = createAgentManager({
|
|
7576
8847
|
repo,
|
|
@@ -7581,7 +8852,9 @@ function serve(opts) {
|
|
|
7581
8852
|
saveTerritories: saveTerritoriesAndEmit,
|
|
7582
8853
|
directorToken,
|
|
7583
8854
|
getIndex: () => state.index,
|
|
7584
|
-
|
|
8855
|
+
permissionMode,
|
|
8856
|
+
// Agents without Claude's edit hook report their edits in their own stream.
|
|
8857
|
+
onEdit: (agent, file) => postActivity({ agent, file })
|
|
7585
8858
|
});
|
|
7586
8859
|
let live;
|
|
7587
8860
|
let watcher;
|
|
@@ -7620,15 +8893,47 @@ function serve(opts) {
|
|
|
7620
8893
|
saveTerritories: saveTerritoriesAndEmit,
|
|
7621
8894
|
inbox,
|
|
7622
8895
|
agents,
|
|
7623
|
-
tracking
|
|
8896
|
+
tracking,
|
|
8897
|
+
autoAllowRules: () => {
|
|
8898
|
+
const mode = permissionMode();
|
|
8899
|
+
return mode === "never" ? [] : [...mode === "safe" ? SAFE_RULES : [], ...readLocalAllow(repo)];
|
|
8900
|
+
}
|
|
7624
8901
|
});
|
|
8902
|
+
const markBlock = (nodeId2, decision, note) => {
|
|
8903
|
+
const block2 = state.payload.changeSet.blocks.find((b) => b.nodeId === nodeId2);
|
|
8904
|
+
if (!block2) return false;
|
|
8905
|
+
if (decision === "pending") tracking.reviews.delete(nodeId2);
|
|
8906
|
+
else
|
|
8907
|
+
tracking.reviews.set(nodeId2, {
|
|
8908
|
+
decision,
|
|
8909
|
+
...note && { note },
|
|
8910
|
+
...decision === "viewed" && { fp: fingerprint(block2, state.index.byId.get(nodeId2)) }
|
|
8911
|
+
});
|
|
8912
|
+
block2.review = decision;
|
|
8913
|
+
delete block2.viewedStale;
|
|
8914
|
+
if (note && decision === "changes_requested") block2.reviewNote = note;
|
|
8915
|
+
else delete block2.reviewNote;
|
|
8916
|
+
try {
|
|
8917
|
+
writeReviews(repo, reviewKey(state.payload.changeSet), tracking.reviews);
|
|
8918
|
+
} catch (e) {
|
|
8919
|
+
console.error(
|
|
8920
|
+
`topo: couldn't save .topo/reviews.json: ${e instanceof Error ? e.message : e}`
|
|
8921
|
+
);
|
|
8922
|
+
}
|
|
8923
|
+
return true;
|
|
8924
|
+
};
|
|
7625
8925
|
const server = createServer({
|
|
7626
8926
|
getPayload: (refresh) => {
|
|
7627
8927
|
if (refresh) state = compute();
|
|
7628
8928
|
state.payload.inbox = inbox.list();
|
|
7629
8929
|
state.payload.runs = agents.list();
|
|
7630
|
-
state.payload.commandBar = agents.
|
|
8930
|
+
state.payload.commandBar = agents.director ? "agent" : "search";
|
|
8931
|
+
state.payload.agentClis = agents.agents();
|
|
8932
|
+
state.payload.exchanges = agents.exchanges();
|
|
7631
8933
|
if (opts.skipPermissions) state.payload.skipPermissions = true;
|
|
8934
|
+
state.payload.agentPermissions = permissionMode();
|
|
8935
|
+
const layers = readLayers(repo);
|
|
8936
|
+
state.payload.layers = layers && layerMap(layers, state.review.head);
|
|
7632
8937
|
return state.payload;
|
|
7633
8938
|
},
|
|
7634
8939
|
index: () => state.index,
|
|
@@ -7641,22 +8946,39 @@ function serve(opts) {
|
|
|
7641
8946
|
directorToken,
|
|
7642
8947
|
inbox,
|
|
7643
8948
|
agents,
|
|
8949
|
+
allowAlways: (item) => item.rule && addLocalAllowRule(repo, item.rule),
|
|
8950
|
+
getSettings: () => readSettings(repo),
|
|
8951
|
+
saveSettings: (raw) => {
|
|
8952
|
+
const s = parseSettings(raw);
|
|
8953
|
+
writeSettings(repo, s);
|
|
8954
|
+
return s;
|
|
8955
|
+
},
|
|
7644
8956
|
liveEvents: bus,
|
|
7645
8957
|
postActivity,
|
|
7646
|
-
reviewBlock:
|
|
8958
|
+
reviewBlock: markBlock,
|
|
8959
|
+
askChanges: (nodeId2, note) => {
|
|
8960
|
+
if (!markBlock(nodeId2, "changes_requested", note)) return void 0;
|
|
7647
8961
|
const block2 = state.payload.changeSet.blocks.find((b) => b.nodeId === nodeId2);
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
8962
|
+
try {
|
|
8963
|
+
const own = block2?.agentTerritory;
|
|
8964
|
+
if (own) {
|
|
8965
|
+
agents.launch(own, askReply([{ nodeId: nodeId2, note }]));
|
|
8966
|
+
return { to: "agent", territoryId: own };
|
|
8967
|
+
}
|
|
8968
|
+
const territories = getTerritories();
|
|
8969
|
+
const label = `Fix ${state.index.byId.get(nodeId2)?.name ?? nodeId2}`;
|
|
8970
|
+
const id = uniqueSlug(slugify(label), territories);
|
|
8971
|
+
saveTerritoriesAndEmit([
|
|
8972
|
+
...territories,
|
|
8973
|
+
{ id, label, nodeIds: [nodeId2], note: `${note}
|
|
8974
|
+
|
|
8975
|
+
The block: ${nodeId2}` }
|
|
8976
|
+
]);
|
|
8977
|
+
agents.launch(id);
|
|
8978
|
+
return { to: "worker", territoryId: id };
|
|
8979
|
+
} catch (e) {
|
|
8980
|
+
return { to: "saved", error: e instanceof Error ? e.message : String(e) };
|
|
7658
8981
|
}
|
|
7659
|
-
return true;
|
|
7660
8982
|
},
|
|
7661
8983
|
staticDir: DEFAULT_STATIC_DIR,
|
|
7662
8984
|
editorCommand,
|
|
@@ -7695,6 +9017,8 @@ var init_review2 = __esm({
|
|
|
7695
9017
|
init_inbox();
|
|
7696
9018
|
await init_live2();
|
|
7697
9019
|
await init_mcp();
|
|
9020
|
+
init_permissions();
|
|
9021
|
+
init_reviews();
|
|
7698
9022
|
await init_server();
|
|
7699
9023
|
init_territories();
|
|
7700
9024
|
init_watch();
|
|
@@ -7716,7 +9040,7 @@ Options:
|
|
|
7716
9040
|
--dangerously-skip-permissions launched agents run with no permission prompts (like Claude Code's
|
|
7717
9041
|
flag of the same name): nothing waits in the Inbox. For this server session only.
|
|
7718
9042
|
-h, --help`;
|
|
7719
|
-
layoutFile = (repo) =>
|
|
9043
|
+
layoutFile = (repo) => path25.join(repo, ".topo", "layout.json");
|
|
7720
9044
|
}
|
|
7721
9045
|
});
|
|
7722
9046
|
|
|
@@ -7727,8 +9051,8 @@ __export(export_exports, {
|
|
|
7727
9051
|
run: () => run5,
|
|
7728
9052
|
snapshotPage: () => snapshotPage
|
|
7729
9053
|
});
|
|
7730
|
-
import
|
|
7731
|
-
import
|
|
9054
|
+
import fs23 from "node:fs";
|
|
9055
|
+
import path26 from "node:path";
|
|
7732
9056
|
import { parseArgs as parseArgs5 } from "node:util";
|
|
7733
9057
|
function snapshotPage(template, data) {
|
|
7734
9058
|
const json2 = JSON.stringify(data).replaceAll("<", "\\u003c");
|
|
@@ -7758,7 +9082,7 @@ function run5(argv) {
|
|
|
7758
9082
|
if (!base) throw new Error(`missing <base> ref
|
|
7759
9083
|
|
|
7760
9084
|
${HELP5}`);
|
|
7761
|
-
const repo =
|
|
9085
|
+
const repo = path26.resolve(values.repo ?? process.cwd());
|
|
7762
9086
|
const review = computeReview(repo, base, values.head);
|
|
7763
9087
|
const payload = { ...toPayload(repo, review, ""), live: void 0 };
|
|
7764
9088
|
const sources = {};
|
|
@@ -7768,14 +9092,14 @@ ${HELP5}`);
|
|
|
7768
9092
|
if (s) sources[`${side} ${b.nodeId}`] = s;
|
|
7769
9093
|
}
|
|
7770
9094
|
}
|
|
7771
|
-
const templateFile =
|
|
7772
|
-
if (!
|
|
9095
|
+
const templateFile = path26.join(DEFAULT_STATIC_DIR, "snapshot.html");
|
|
9096
|
+
if (!fs23.existsSync(templateFile)) {
|
|
7773
9097
|
throw new Error("the web UI isn't built: run `pnpm --filter @topo/web build` first");
|
|
7774
9098
|
}
|
|
7775
|
-
const template =
|
|
7776
|
-
const out =
|
|
7777
|
-
|
|
7778
|
-
const kb = Math.round(
|
|
9099
|
+
const template = fs23.readFileSync(templateFile, "utf8");
|
|
9100
|
+
const out = path26.resolve(values.out);
|
|
9101
|
+
fs23.writeFileSync(out, snapshotPage(template, { payload, sources }));
|
|
9102
|
+
const kb = Math.round(fs23.statSync(out).size / 1024);
|
|
7779
9103
|
console.log(`topo: ${review.changeSet.blocks.length} changed block(s) \u2192 ${out} (${kb} kB)`);
|
|
7780
9104
|
}
|
|
7781
9105
|
var HELP5;
|
|
@@ -7804,8 +9128,8 @@ __export(layers_exports, {
|
|
|
7804
9128
|
HELP: () => HELP6,
|
|
7805
9129
|
run: () => run6
|
|
7806
9130
|
});
|
|
7807
|
-
import
|
|
7808
|
-
import
|
|
9131
|
+
import fs24 from "node:fs";
|
|
9132
|
+
import path27 from "node:path";
|
|
7809
9133
|
import { parseArgs as parseArgs6 } from "node:util";
|
|
7810
9134
|
function run6(argv) {
|
|
7811
9135
|
const { values, positionals } = parseArgs6({
|
|
@@ -7817,18 +9141,18 @@ function run6(argv) {
|
|
|
7817
9141
|
console.log(HELP6);
|
|
7818
9142
|
return;
|
|
7819
9143
|
}
|
|
7820
|
-
const repo =
|
|
9144
|
+
const repo = path27.resolve(positionals[0] ?? process.cwd());
|
|
7821
9145
|
const graph = indexRepo(repo);
|
|
7822
9146
|
let doc = readLayers(repo);
|
|
7823
9147
|
if (values.suggest) {
|
|
7824
9148
|
if (doc) {
|
|
7825
9149
|
console.log(
|
|
7826
|
-
`topo: ${
|
|
9150
|
+
`topo: ${path27.relative(process.cwd(), layersFile(repo)) || layersFile(repo)} already exists; edit it instead.`
|
|
7827
9151
|
);
|
|
7828
9152
|
} else {
|
|
7829
9153
|
doc = suggestLayers(graph);
|
|
7830
|
-
|
|
7831
|
-
|
|
9154
|
+
fs24.mkdirSync(path27.dirname(layersFile(repo)), { recursive: true });
|
|
9155
|
+
fs24.writeFileSync(layersFile(repo), `${JSON.stringify(doc, null, 2)}
|
|
7832
9156
|
`);
|
|
7833
9157
|
console.log(
|
|
7834
9158
|
`topo: wrote ${doc.layers.length} suggested layer(s) to ${layersFile(repo)}; name them, merge them, and add rules.`
|
|
@@ -7878,7 +9202,7 @@ __export(map_exports, {
|
|
|
7878
9202
|
HELP: () => HELP7,
|
|
7879
9203
|
run: () => run7
|
|
7880
9204
|
});
|
|
7881
|
-
import
|
|
9205
|
+
import path28 from "node:path";
|
|
7882
9206
|
import { parseArgs as parseArgs7 } from "node:util";
|
|
7883
9207
|
function run7(argv) {
|
|
7884
9208
|
const { values, positionals } = parseArgs7({
|
|
@@ -7901,7 +9225,7 @@ function run7(argv) {
|
|
|
7901
9225
|
return;
|
|
7902
9226
|
}
|
|
7903
9227
|
if (!values.cache) process.env.TOPO_NO_CACHE = "1";
|
|
7904
|
-
const dir =
|
|
9228
|
+
const dir = path28.resolve(positionals[0] ?? process.cwd());
|
|
7905
9229
|
return serve({
|
|
7906
9230
|
compute: () => computeExplore(dir),
|
|
7907
9231
|
repo: dir,
|
|
@@ -7984,7 +9308,7 @@ async function main(argv) {
|
|
|
7984
9308
|
${HELP8}`);
|
|
7985
9309
|
await (await cmd()).run(rest);
|
|
7986
9310
|
}
|
|
7987
|
-
if (
|
|
9311
|
+
if (true) {
|
|
7988
9312
|
main(process.argv.slice(2)).catch((e) => {
|
|
7989
9313
|
console.error(`topo: ${friendlyError(e)}`);
|
|
7990
9314
|
process.exit(1);
|