@tikoci/rosetta 0.5.2 → 0.6.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 +3 -1
- package/package.json +1 -1
- package/src/browse.ts +90 -0
- package/src/db.ts +130 -7
- package/src/extract-commands.ts +55 -10
- package/src/extract-dude.ts +409 -0
- package/src/mcp-http.test.ts +7 -5
- package/src/mcp.ts +278 -1
- package/src/paths.ts +1 -1
- package/src/query.test.ts +124 -6
- package/src/query.ts +93 -2
- package/src/release.test.ts +20 -0
package/README.md
CHANGED
|
@@ -269,7 +269,7 @@ Ask your AI assistant questions like:
|
|
|
269
269
|
|
|
270
270
|
## MCP Tools
|
|
271
271
|
|
|
272
|
-
The server exposes
|
|
272
|
+
The server exposes 16 tools designed to work together — agents start with `routeros_search` and drill into specific data as needed:
|
|
273
273
|
|
|
274
274
|
| Tool | What it does |
|
|
275
275
|
|------|-------------|
|
|
@@ -285,6 +285,8 @@ The server exposes 14 tools designed to work together — agents start with `rou
|
|
|
285
285
|
| `routeros_device_lookup` | Hardware specs — filter by architecture, RAM, PoE, wireless, etc. |
|
|
286
286
|
| `routeros_search_tests` | Cross-device ethernet and IPSec benchmarks |
|
|
287
287
|
| `routeros_search_videos` | YouTube transcript search with chapter timestamps |
|
|
288
|
+
| `routeros_dude_search` | FTS across archived Dude wiki docs (separate from RouterOS search) |
|
|
289
|
+
| `routeros_dude_get_page` | Full Dude wiki page by ID or title, with screenshot metadata |
|
|
288
290
|
| `routeros_stats` | Database health and coverage stats |
|
|
289
291
|
| `routeros_current_versions` | Live-fetch current RouterOS versions from MikroTik |
|
|
290
292
|
|
package/package.json
CHANGED
package/src/browse.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
ChangelogResult,
|
|
20
20
|
DeviceResult,
|
|
21
21
|
DeviceTestRow,
|
|
22
|
+
DudeSearchResult,
|
|
22
23
|
SearchResponse,
|
|
23
24
|
SearchResult,
|
|
24
25
|
SectionTocEntry,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
checkCommandVersions,
|
|
30
31
|
diffCommandVersions,
|
|
31
32
|
fetchCurrentVersions,
|
|
33
|
+
getDudePage,
|
|
32
34
|
getPage,
|
|
33
35
|
getTestResultMeta,
|
|
34
36
|
lookupProperty,
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
searchChangelogs,
|
|
37
39
|
searchDevices,
|
|
38
40
|
searchDeviceTests,
|
|
41
|
+
searchDude,
|
|
39
42
|
searchPages,
|
|
40
43
|
searchProperties,
|
|
41
44
|
searchVideos,
|
|
@@ -196,6 +199,7 @@ type Context =
|
|
|
196
199
|
| { type: "callouts"; query: string; results: CalloutResult[] }
|
|
197
200
|
| { type: "changelogs"; results: ChangelogResult[] }
|
|
198
201
|
| { type: "videos"; query: string; results: VideoSearchResult[] }
|
|
202
|
+
| { type: "dude"; query: string; results: DudeSearchResult[] }
|
|
199
203
|
| { type: "properties"; query: string; pageId?: number; results: Array<{ name: string; page_id: number; page_title: string }> }
|
|
200
204
|
| { type: "diff" }
|
|
201
205
|
| { type: "vcheck"; path: string };
|
|
@@ -235,6 +239,7 @@ function contextLabel(c: Context): string {
|
|
|
235
239
|
case "callouts": return c.query ? truncate(`cal: "${c.query}"`, 30) : "callouts";
|
|
236
240
|
case "changelogs": return "changelogs";
|
|
237
241
|
case "videos": return truncate(`vid: "${c.query}"`, 30);
|
|
242
|
+
case "dude": return truncate(`dude: "${c.query}"`, 30);
|
|
238
243
|
case "diff": return "diff";
|
|
239
244
|
case "vcheck": return truncate(`vc: ${c.path}`, 30);
|
|
240
245
|
}
|
|
@@ -662,6 +667,65 @@ function renderVideos(results: VideoSearchResult[]): string {
|
|
|
662
667
|
return out.join("\n");
|
|
663
668
|
}
|
|
664
669
|
|
|
670
|
+
function renderDudeResults(results: DudeSearchResult[]): string {
|
|
671
|
+
const out: string[] = [];
|
|
672
|
+
if (results.length === 0) {
|
|
673
|
+
out.push(` ${dim("No Dude wiki results found.")}`);
|
|
674
|
+
return out.join("\n");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
for (let i = 0; i < results.length; i++) {
|
|
678
|
+
const d = results[i];
|
|
679
|
+
const num = dim(`${String(i + 1).padStart(3)} `);
|
|
680
|
+
const title = bold(truncate(d.title, 60));
|
|
681
|
+
const ver = dim(`[${d.version}]`);
|
|
682
|
+
const imgs = d.image_count > 0 ? dim(`📷 ${d.image_count}`) : "";
|
|
683
|
+
out.push(`${num}${title} ${ver} ${imgs}`);
|
|
684
|
+
out.push(` ${dim(d.path)}`);
|
|
685
|
+
const excerpt = d.excerpt.replace(/\*\*/g, `${ESC}[1m`);
|
|
686
|
+
out.push(` ${dim(truncate(excerpt, termWidth() - 8))}`);
|
|
687
|
+
out.push("");
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
out.push(` ${cyan("[<n>]")} view page ${cyan("[s <query>]")} search docs ${cyan("[b]")} back`);
|
|
691
|
+
return out.join("\n");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function renderDudePage(page: import("./query.ts").DudePageResult): string {
|
|
695
|
+
const out: string[] = [];
|
|
696
|
+
out.push(` ${bold(page.title)} ${dim(`[${page.version}]`)}`);
|
|
697
|
+
out.push(` ${dim(page.path)}`);
|
|
698
|
+
out.push(` ${cyan(link(page.url))}`);
|
|
699
|
+
if (page.wayback_url) out.push(` ${dim(`Archived: ${page.wayback_url}`)}`);
|
|
700
|
+
out.push("");
|
|
701
|
+
|
|
702
|
+
if (page.images.length > 0) {
|
|
703
|
+
out.push(` ${bold("Screenshots:")} ${page.images.length}`);
|
|
704
|
+
for (const img of page.images) {
|
|
705
|
+
out.push(` ${dim("•")} ${img.filename}${img.caption ? ` ${dim(img.caption)}` : ""}`);
|
|
706
|
+
out.push(` ${dim(img.local_path)}`);
|
|
707
|
+
}
|
|
708
|
+
out.push("");
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Truncate long pages for TUI display
|
|
712
|
+
const maxChars = 8000;
|
|
713
|
+
const text = page.text.length > maxChars
|
|
714
|
+
? `${page.text.slice(0, maxChars)}\n\n ${dim(`... truncated (${page.text.length} chars total)`)}`
|
|
715
|
+
: page.text;
|
|
716
|
+
out.push(text);
|
|
717
|
+
|
|
718
|
+
if (page.code) {
|
|
719
|
+
out.push("");
|
|
720
|
+
out.push(` ${bold("Code:")}`);
|
|
721
|
+
out.push(page.code.length > 2000 ? `${page.code.slice(0, 2000)}\n ${dim("... truncated")}` : page.code);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
out.push("");
|
|
725
|
+
out.push(` ${cyan("[dude <query>]")} search dude ${cyan("[s <query>]")} search docs ${cyan("[b]")} back`);
|
|
726
|
+
return out.join("\n");
|
|
727
|
+
}
|
|
728
|
+
|
|
665
729
|
function renderDiff(result: ReturnType<typeof diffCommandVersions>): string {
|
|
666
730
|
const out: string[] = [];
|
|
667
731
|
out.push(` ${bold("Command diff:")} ${result.from_version} → ${result.to_version}`);
|
|
@@ -775,6 +839,7 @@ function renderHelp(): string {
|
|
|
775
839
|
cmd("callouts [query]", "cal", "Search callouts (type filter: cal warning)", "routeros_search_callouts");
|
|
776
840
|
cmd("changelog [query]", "cl", "Search changelogs (cl 7.22, cl breaking)", "routeros_search_changelogs");
|
|
777
841
|
cmd("videos <query>", "vid", "Search video transcripts", "routeros_search_videos");
|
|
842
|
+
cmd("dude <query>", "", "Search archived Dude wiki docs", "routeros_dude_search");
|
|
778
843
|
cmd("diff <from> <to> [path]", "", "Command tree diff between versions", "routeros_command_diff");
|
|
779
844
|
cmd("vcheck <path>", "vc", "Version range for a command path", "routeros_command_version_check");
|
|
780
845
|
cmd("versions", "ver", "Live-fetch current RouterOS versions", "routeros_current_versions");
|
|
@@ -920,6 +985,12 @@ async function dispatch(input: string): Promise<void> {
|
|
|
920
985
|
return;
|
|
921
986
|
}
|
|
922
987
|
|
|
988
|
+
case "dude": {
|
|
989
|
+
if (!rest) { console.log(dim(" Usage: dude <query>")); return; }
|
|
990
|
+
await doSearchDude(rest);
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
923
994
|
case "diff": {
|
|
924
995
|
const diffParts = rest.split(/\s+/);
|
|
925
996
|
if (diffParts.length < 2) {
|
|
@@ -989,6 +1060,15 @@ async function handleNumberSelect(idx: number): Promise<void> {
|
|
|
989
1060
|
console.log(` ${cyan(link(timeUrl))}\n`);
|
|
990
1061
|
return;
|
|
991
1062
|
}
|
|
1063
|
+
if (ctx.type === "dude" && ctx.results[idx]) {
|
|
1064
|
+
const d = ctx.results[idx];
|
|
1065
|
+
const page = getDudePage(d.id);
|
|
1066
|
+
if (page) {
|
|
1067
|
+
await paged(renderDudePage(page));
|
|
1068
|
+
pushCtx({ type: "dude", query: ctx.query, results: ctx.results });
|
|
1069
|
+
}
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
992
1072
|
if (ctx.type === "properties" && ctx.results[idx]) {
|
|
993
1073
|
const p = ctx.results[idx];
|
|
994
1074
|
await doPage(String(p.page_id));
|
|
@@ -1222,6 +1302,16 @@ async function doSearchVideos(query: string): Promise<void> {
|
|
|
1222
1302
|
pushCtx({ type: "videos", query, results });
|
|
1223
1303
|
}
|
|
1224
1304
|
|
|
1305
|
+
async function doSearchDude(query: string): Promise<void> {
|
|
1306
|
+
const results = searchDude(query, 10);
|
|
1307
|
+
if (results.length === 0) {
|
|
1308
|
+
console.log(dim(` No Dude wiki results for "${query}".`));
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
await paged(` ${bold(String(results.length))} Dude wiki results for ${cyan(`"${query}"`)}\n\n${renderDudeResults(results)}`);
|
|
1312
|
+
pushCtx({ type: "dude", query, results });
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1225
1315
|
async function doDiff(from: string, to: string, pathPrefix?: string): Promise<void> {
|
|
1226
1316
|
const result = diffCommandVersions(from, to, pathPrefix);
|
|
1227
1317
|
await paged(renderDiff(result));
|
package/src/db.ts
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
* videos_fts — FTS5 over title, description
|
|
22
22
|
* video_segments — transcript segments (one per chapter, or full video if no chapters)
|
|
23
23
|
* video_segments_fts — FTS5 over chapter_title, transcript
|
|
24
|
+
* dude_pages — The Dude documentation pages (archived from wiki.mikrotik.com via Wayback Machine)
|
|
25
|
+
* dude_pages_fts — FTS5 over title, path, text, code
|
|
26
|
+
* dude_images — screenshot images from The Dude wiki pages
|
|
24
27
|
*/
|
|
25
28
|
|
|
26
29
|
import sqlite from "bun:sqlite";
|
|
@@ -205,17 +208,83 @@ export function initDb() {
|
|
|
205
208
|
db.run(`CREATE INDEX IF NOT EXISTS idx_commands_type ON commands(type);`);
|
|
206
209
|
|
|
207
210
|
// -- Command version tracking --
|
|
211
|
+
//
|
|
212
|
+
// ros_versions is keyed on (version, arch) — restraml emits per-arch
|
|
213
|
+
// deep-inspect.<arch>.json (x86 vs arm64; arm64 carries ~1K extra nodes
|
|
214
|
+
// for wifi-qcom etc.). _meta fields capture provenance from the inspect
|
|
215
|
+
// file (generatedAt, crashPaths, completionStats). _attrs is a JSON
|
|
216
|
+
// catch-all for forward-compat: anything restraml later emits lands here
|
|
217
|
+
// first and gets promoted to a column once shape is stable.
|
|
218
|
+
//
|
|
219
|
+
// command_versions intentionally has no FK to ros_versions — the composite
|
|
220
|
+
// PK on the parent makes a single-column FK invalid, and command_versions
|
|
221
|
+
// is slated for replacement by schema_node_presence in the upcoming
|
|
222
|
+
// schema_nodes refactor (BACKLOG.md "Multi-arch schema import").
|
|
223
|
+
|
|
224
|
+
// Migration: legacy ros_versions had `version TEXT PRIMARY KEY` with no
|
|
225
|
+
// arch column. Detect via PRAGMA table_info and rebuild both tables in
|
|
226
|
+
// place — the FK on command_versions.ros_version requires the dance.
|
|
227
|
+
// Idempotent: only fires when arch column is missing.
|
|
228
|
+
{
|
|
229
|
+
const rvCols = db.prepare("PRAGMA table_info(ros_versions)").all() as Array<{ name: string }>;
|
|
230
|
+
if (rvCols.length > 0 && !rvCols.some((c) => c.name === "arch")) {
|
|
231
|
+
db.run("PRAGMA foreign_keys=OFF;");
|
|
232
|
+
db.run("BEGIN;");
|
|
233
|
+
try {
|
|
234
|
+
db.run(`CREATE TABLE ros_versions_new (
|
|
235
|
+
version TEXT NOT NULL,
|
|
236
|
+
arch TEXT NOT NULL DEFAULT 'x86',
|
|
237
|
+
channel TEXT,
|
|
238
|
+
extra_packages INTEGER NOT NULL DEFAULT 0,
|
|
239
|
+
extracted_at TEXT NOT NULL,
|
|
240
|
+
generated_at TEXT,
|
|
241
|
+
crash_paths_tested TEXT,
|
|
242
|
+
crash_paths_crashed TEXT,
|
|
243
|
+
completion_stats TEXT,
|
|
244
|
+
source_url TEXT,
|
|
245
|
+
_attrs TEXT,
|
|
246
|
+
PRIMARY KEY (version, arch)
|
|
247
|
+
);`);
|
|
248
|
+
db.run(`INSERT INTO ros_versions_new (version, arch, channel, extra_packages, extracted_at)
|
|
249
|
+
SELECT version, 'x86', channel, extra_packages, extracted_at FROM ros_versions;`);
|
|
250
|
+
db.run("DROP TABLE ros_versions;");
|
|
251
|
+
db.run("ALTER TABLE ros_versions_new RENAME TO ros_versions;");
|
|
252
|
+
|
|
253
|
+
db.run(`CREATE TABLE command_versions_new (
|
|
254
|
+
command_path TEXT NOT NULL,
|
|
255
|
+
ros_version TEXT NOT NULL,
|
|
256
|
+
PRIMARY KEY (command_path, ros_version)
|
|
257
|
+
);`);
|
|
258
|
+
db.run("INSERT INTO command_versions_new SELECT command_path, ros_version FROM command_versions;");
|
|
259
|
+
db.run("DROP TABLE command_versions;");
|
|
260
|
+
db.run("ALTER TABLE command_versions_new RENAME TO command_versions;");
|
|
261
|
+
db.run("COMMIT;");
|
|
262
|
+
} catch (e) {
|
|
263
|
+
db.run("ROLLBACK;");
|
|
264
|
+
throw e;
|
|
265
|
+
}
|
|
266
|
+
db.run("PRAGMA foreign_keys=ON;");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
208
269
|
|
|
209
270
|
db.run(`CREATE TABLE IF NOT EXISTS ros_versions (
|
|
210
|
-
version
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
271
|
+
version TEXT NOT NULL,
|
|
272
|
+
arch TEXT NOT NULL DEFAULT 'x86',
|
|
273
|
+
channel TEXT,
|
|
274
|
+
extra_packages INTEGER NOT NULL DEFAULT 0,
|
|
275
|
+
extracted_at TEXT NOT NULL,
|
|
276
|
+
generated_at TEXT,
|
|
277
|
+
crash_paths_tested TEXT,
|
|
278
|
+
crash_paths_crashed TEXT,
|
|
279
|
+
completion_stats TEXT,
|
|
280
|
+
source_url TEXT,
|
|
281
|
+
_attrs TEXT,
|
|
282
|
+
PRIMARY KEY (version, arch)
|
|
214
283
|
);`);
|
|
215
284
|
|
|
216
285
|
db.run(`CREATE TABLE IF NOT EXISTS command_versions (
|
|
217
286
|
command_path TEXT NOT NULL,
|
|
218
|
-
ros_version TEXT NOT NULL
|
|
287
|
+
ros_version TEXT NOT NULL,
|
|
219
288
|
PRIMARY KEY (command_path, ros_version)
|
|
220
289
|
);`);
|
|
221
290
|
|
|
@@ -426,6 +495,58 @@ export function initDb() {
|
|
|
426
495
|
END;`);
|
|
427
496
|
|
|
428
497
|
db.run(`CREATE INDEX IF NOT EXISTS idx_video_segs_video ON video_segments(video_id);`);
|
|
498
|
+
|
|
499
|
+
// -- The Dude documentation (archived from MikroTik wiki via Wayback Machine) --
|
|
500
|
+
|
|
501
|
+
db.run(`CREATE TABLE IF NOT EXISTS dude_pages (
|
|
502
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
503
|
+
slug TEXT NOT NULL UNIQUE,
|
|
504
|
+
title TEXT NOT NULL,
|
|
505
|
+
path TEXT NOT NULL,
|
|
506
|
+
version TEXT NOT NULL DEFAULT 'v6',
|
|
507
|
+
url TEXT NOT NULL,
|
|
508
|
+
wayback_url TEXT NOT NULL,
|
|
509
|
+
text TEXT NOT NULL,
|
|
510
|
+
code TEXT,
|
|
511
|
+
last_edited TEXT,
|
|
512
|
+
word_count INTEGER
|
|
513
|
+
);`);
|
|
514
|
+
|
|
515
|
+
db.run(`CREATE VIRTUAL TABLE IF NOT EXISTS dude_pages_fts USING fts5(
|
|
516
|
+
title, path, text, code,
|
|
517
|
+
content=dude_pages,
|
|
518
|
+
content_rowid=id,
|
|
519
|
+
tokenize='porter unicode61'
|
|
520
|
+
);`);
|
|
521
|
+
|
|
522
|
+
db.run(`CREATE TRIGGER IF NOT EXISTS dude_pages_ai AFTER INSERT ON dude_pages BEGIN
|
|
523
|
+
INSERT INTO dude_pages_fts(rowid, title, path, text, code)
|
|
524
|
+
VALUES (new.id, new.title, new.path, new.text, new.code);
|
|
525
|
+
END;`);
|
|
526
|
+
db.run(`CREATE TRIGGER IF NOT EXISTS dude_pages_ad AFTER DELETE ON dude_pages BEGIN
|
|
527
|
+
INSERT INTO dude_pages_fts(dude_pages_fts, rowid, title, path, text, code)
|
|
528
|
+
VALUES('delete', old.id, old.title, old.path, old.text, old.code);
|
|
529
|
+
END;`);
|
|
530
|
+
db.run(`CREATE TRIGGER IF NOT EXISTS dude_pages_au AFTER UPDATE ON dude_pages BEGIN
|
|
531
|
+
INSERT INTO dude_pages_fts(dude_pages_fts, rowid, title, path, text, code)
|
|
532
|
+
VALUES('delete', old.id, old.title, old.path, old.text, old.code);
|
|
533
|
+
INSERT INTO dude_pages_fts(rowid, title, path, text, code)
|
|
534
|
+
VALUES (new.id, new.title, new.path, new.text, new.code);
|
|
535
|
+
END;`);
|
|
536
|
+
|
|
537
|
+
db.run(`CREATE TABLE IF NOT EXISTS dude_images (
|
|
538
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
539
|
+
page_id INTEGER NOT NULL REFERENCES dude_pages(id),
|
|
540
|
+
filename TEXT NOT NULL,
|
|
541
|
+
alt_text TEXT,
|
|
542
|
+
caption TEXT,
|
|
543
|
+
local_path TEXT NOT NULL,
|
|
544
|
+
original_url TEXT,
|
|
545
|
+
wayback_url TEXT,
|
|
546
|
+
sort_order INTEGER NOT NULL
|
|
547
|
+
);`);
|
|
548
|
+
|
|
549
|
+
db.run(`CREATE INDEX IF NOT EXISTS idx_dude_images_page ON dude_images(page_id);`);
|
|
429
550
|
}
|
|
430
551
|
|
|
431
552
|
/**
|
|
@@ -470,12 +591,14 @@ export function getDbStats() {
|
|
|
470
591
|
devices_with_tests: count("SELECT COUNT(DISTINCT device_id) AS c FROM device_test_results"),
|
|
471
592
|
changelogs: count("SELECT COUNT(*) AS c FROM changelogs"),
|
|
472
593
|
changelog_versions: count("SELECT COUNT(DISTINCT version) AS c FROM changelogs"),
|
|
473
|
-
ros_versions: count("SELECT COUNT(
|
|
594
|
+
ros_versions: count("SELECT COUNT(DISTINCT version) AS c FROM ros_versions"),
|
|
474
595
|
videos: count("SELECT COUNT(*) AS c FROM videos"),
|
|
475
596
|
video_segments: count("SELECT COUNT(*) AS c FROM video_segments"),
|
|
597
|
+
dude_pages: count("SELECT COUNT(*) AS c FROM dude_pages"),
|
|
598
|
+
dude_images: count("SELECT COUNT(*) AS c FROM dude_images"),
|
|
476
599
|
...(() => {
|
|
477
600
|
// Semantic version sort — SQL MIN/MAX is lexicographic ("7.10" < "7.9")
|
|
478
|
-
const versions = (db.prepare("SELECT version FROM ros_versions").all() as Array<{ version: string }>).map((r) => r.version);
|
|
601
|
+
const versions = (db.prepare("SELECT DISTINCT version FROM ros_versions").all() as Array<{ version: string }>).map((r) => r.version);
|
|
479
602
|
if (versions.length === 0) return { ros_version_min: null, ros_version_max: null };
|
|
480
603
|
const norm = (v: string) => {
|
|
481
604
|
const clean = v.replace(/beta\d*/, "").replace(/rc\d*/, "");
|
package/src/extract-commands.ts
CHANGED
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* extract-commands.ts — Load RouterOS command tree from inspect.json
|
|
3
|
+
* extract-commands.ts — Load RouterOS command tree from inspect.json (or
|
|
4
|
+
* deep-inspect.<arch>.json) into SQLite.
|
|
4
5
|
*
|
|
5
6
|
* Walks the nested JSON tree and flattens it into a `commands` table with:
|
|
6
7
|
* path, name, type (dir|cmd|arg), parent_path, description
|
|
7
8
|
*
|
|
8
|
-
* Also populates command_versions for version tracking
|
|
9
|
+
* Also populates command_versions for version tracking, and ros_versions
|
|
10
|
+
* with provenance (arch, _meta.generatedAt, crashPaths, completionStats)
|
|
11
|
+
* when the source file is a deep-inspect.json (i.e. has a `_meta` envelope).
|
|
9
12
|
*
|
|
10
13
|
* Usage:
|
|
11
|
-
* bun run src/extract-commands.ts [
|
|
12
|
-
* bun run src/extract-commands.ts --accumulate [
|
|
14
|
+
* bun run src/extract-commands.ts [path-or-url] [--version=7.22] [--channel=stable] [--extra] [--arch=x86|arm64]
|
|
15
|
+
* bun run src/extract-commands.ts --accumulate [path] [--version=X]
|
|
13
16
|
*
|
|
14
17
|
* In default mode: replaces commands table and sets as primary version.
|
|
15
18
|
* With --accumulate: only adds to command_versions, does not touch commands table.
|
|
19
|
+
*
|
|
20
|
+
* --arch defaults to x86 (back-compat for legacy single-arch inspect.json).
|
|
21
|
+
* deep-inspect.x86.json / deep-inspect.arm64.json filenames auto-derive arch.
|
|
16
22
|
*/
|
|
17
23
|
|
|
18
24
|
import { db, initDb } from "./db.ts";
|
|
@@ -33,6 +39,13 @@ const flagArgs = Object.fromEntries(
|
|
|
33
39
|
const DEFAULT_INSPECT_URL = `${RESTRAML_PAGES_URL}/7.22.1/extra/inspect.json`;
|
|
34
40
|
const INSPECT_SOURCE = positional[0] || DEFAULT_INSPECT_URL;
|
|
35
41
|
|
|
42
|
+
function deriveArch(filepath: string, flag: string | undefined): "x86" | "arm64" {
|
|
43
|
+
if (flag === "x86" || flag === "arm64") return flag;
|
|
44
|
+
if (/deep-inspect\.arm64\b/.test(filepath)) return "arm64";
|
|
45
|
+
if (/deep-inspect\.x86\b/.test(filepath)) return "x86";
|
|
46
|
+
return "x86";
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
// Derive version from path if not explicitly set
|
|
37
50
|
function deriveVersion(filepath: string): string {
|
|
38
51
|
const match = filepath.match(/\/(\d+\.\d+(?:\.\d+)?(?:beta\d+|rc\d+)?)\//);
|
|
@@ -48,11 +61,30 @@ function deriveChannel(version: string): string {
|
|
|
48
61
|
|
|
49
62
|
const version = flagArgs.version || deriveVersion(INSPECT_SOURCE);
|
|
50
63
|
const channel = flagArgs.channel || deriveChannel(version);
|
|
64
|
+
const arch = deriveArch(INSPECT_SOURCE, flagArgs.arch);
|
|
51
65
|
|
|
52
66
|
console.log(`Loading inspect.json from ${INSPECT_SOURCE}...`);
|
|
53
|
-
console.log(`Version: ${version}, Channel: ${channel}, Extra: ${extraPackages}, Accumulate: ${accumulate}`);
|
|
67
|
+
console.log(`Version: ${version}, Channel: ${channel}, Arch: ${arch}, Extra: ${extraPackages}, Accumulate: ${accumulate}`);
|
|
54
68
|
const inspectData = await loadJson<Record<string, unknown>>(INSPECT_SOURCE);
|
|
55
69
|
|
|
70
|
+
// deep-inspect.json files carry a `_meta` envelope; vanilla inspect.json does not.
|
|
71
|
+
interface DeepInspectMeta {
|
|
72
|
+
version?: string;
|
|
73
|
+
generatedAt?: string;
|
|
74
|
+
crashPathsTested?: string[];
|
|
75
|
+
crashPathsCrashed?: string[];
|
|
76
|
+
completionStats?: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
const meta = (inspectData._meta && typeof inspectData._meta === "object")
|
|
79
|
+
? (inspectData._meta as DeepInspectMeta)
|
|
80
|
+
: null;
|
|
81
|
+
if (meta) {
|
|
82
|
+
console.log(` _meta.version=${meta.version} generatedAt=${meta.generatedAt}`);
|
|
83
|
+
if (meta.completionStats) {
|
|
84
|
+
console.log(` completionStats=${JSON.stringify(meta.completionStats)}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
56
88
|
interface CommandRow {
|
|
57
89
|
path: string;
|
|
58
90
|
name: string;
|
|
@@ -65,7 +97,7 @@ const rows: CommandRow[] = [];
|
|
|
65
97
|
|
|
66
98
|
function walk(obj: Record<string, unknown>, parentPath: string) {
|
|
67
99
|
for (const [key, value] of Object.entries(obj)) {
|
|
68
|
-
if (key === "_type" || key === "desc") continue;
|
|
100
|
+
if (key === "_type" || key === "desc" || key === "_meta") continue;
|
|
69
101
|
if (typeof value !== "object" || value === null) continue;
|
|
70
102
|
|
|
71
103
|
const node = value as Record<string, unknown>;
|
|
@@ -104,11 +136,24 @@ console.log(` dirs: ${dirs}, cmds: ${cmds}, args: ${args}`);
|
|
|
104
136
|
// Initialize DB and insert
|
|
105
137
|
initDb();
|
|
106
138
|
|
|
107
|
-
// Register this version
|
|
139
|
+
// Register this version (per-arch row). _meta fields are populated when the
|
|
140
|
+
// source is a deep-inspect.json; legacy inspect.json leaves them NULL.
|
|
108
141
|
db.run(
|
|
109
|
-
`INSERT OR REPLACE INTO ros_versions
|
|
110
|
-
|
|
111
|
-
|
|
142
|
+
`INSERT OR REPLACE INTO ros_versions
|
|
143
|
+
(version, arch, channel, extra_packages, extracted_at,
|
|
144
|
+
generated_at, crash_paths_tested, crash_paths_crashed, completion_stats, source_url)
|
|
145
|
+
VALUES (?, ?, ?, ?, datetime('now'), ?, ?, ?, ?, ?)`,
|
|
146
|
+
[
|
|
147
|
+
version,
|
|
148
|
+
arch,
|
|
149
|
+
channel,
|
|
150
|
+
extraPackages ? 1 : 0,
|
|
151
|
+
meta?.generatedAt ?? null,
|
|
152
|
+
meta?.crashPathsTested ? JSON.stringify(meta.crashPathsTested) : null,
|
|
153
|
+
meta?.crashPathsCrashed ? JSON.stringify(meta.crashPathsCrashed) : null,
|
|
154
|
+
meta?.completionStats ? JSON.stringify(meta.completionStats) : null,
|
|
155
|
+
INSPECT_SOURCE,
|
|
156
|
+
],
|
|
112
157
|
);
|
|
113
158
|
|
|
114
159
|
if (!accumulate) {
|