@tikoci/rosetta 0.5.1 → 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 +143 -188
- package/src/db.ts +146 -7
- package/src/extract-commands.ts +55 -10
- package/src/extract-devices.ts +5 -1
- package/src/extract-dude.ts +409 -0
- package/src/extract-html.test.ts +67 -0
- package/src/extract-html.ts +232 -112
- package/src/mcp-http.test.ts +7 -5
- package/src/mcp.ts +280 -15
- package/src/paths.ts +1 -1
- package/src/query.test.ts +124 -6
- package/src/query.ts +115 -20
- package/src/release.test.ts +20 -0
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
|
/**
|
|
@@ -441,8 +562,24 @@ export function checkSchemaVersion(): { ok: boolean; actual: number; expected: n
|
|
|
441
562
|
export function getDbStats() {
|
|
442
563
|
const count = (sql: string) =>
|
|
443
564
|
Number((db.prepare(sql).get() as { c: number }).c ?? 0);
|
|
565
|
+
const dbSizeBytes = (() => {
|
|
566
|
+
try {
|
|
567
|
+
return Bun.file(DB_PATH).size;
|
|
568
|
+
} catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
})();
|
|
572
|
+
const schemaVersion = (() => {
|
|
573
|
+
try {
|
|
574
|
+
return (db.prepare("PRAGMA user_version").get() as { user_version: number }).user_version;
|
|
575
|
+
} catch {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
})();
|
|
444
579
|
return {
|
|
445
580
|
db_path: DB_PATH,
|
|
581
|
+
db_size_bytes: dbSizeBytes,
|
|
582
|
+
schema_version: schemaVersion,
|
|
446
583
|
pages: count("SELECT COUNT(*) AS c FROM pages"),
|
|
447
584
|
sections: count("SELECT COUNT(*) AS c FROM sections"),
|
|
448
585
|
properties: count("SELECT COUNT(*) AS c FROM properties"),
|
|
@@ -454,12 +591,14 @@ export function getDbStats() {
|
|
|
454
591
|
devices_with_tests: count("SELECT COUNT(DISTINCT device_id) AS c FROM device_test_results"),
|
|
455
592
|
changelogs: count("SELECT COUNT(*) AS c FROM changelogs"),
|
|
456
593
|
changelog_versions: count("SELECT COUNT(DISTINCT version) AS c FROM changelogs"),
|
|
457
|
-
ros_versions: count("SELECT COUNT(
|
|
594
|
+
ros_versions: count("SELECT COUNT(DISTINCT version) AS c FROM ros_versions"),
|
|
458
595
|
videos: count("SELECT COUNT(*) AS c FROM videos"),
|
|
459
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"),
|
|
460
599
|
...(() => {
|
|
461
600
|
// Semantic version sort — SQL MIN/MAX is lexicographic ("7.10" < "7.9")
|
|
462
|
-
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);
|
|
463
602
|
if (versions.length === 0) return { ros_version_min: null, ros_version_max: null };
|
|
464
603
|
const norm = (v: string) => {
|
|
465
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) {
|
package/src/extract-devices.ts
CHANGED
|
@@ -120,7 +120,11 @@ if (lines.length < 2) {
|
|
|
120
120
|
// Skip header row
|
|
121
121
|
const dataLines = lines.slice(1);
|
|
122
122
|
|
|
123
|
-
// Idempotent: clear existing data (FTS triggers handle cleanup)
|
|
123
|
+
// Idempotent: clear existing data (FTS triggers handle cleanup).
|
|
124
|
+
// device_test_results FKs into devices — wipe it first to avoid a FOREIGN KEY
|
|
125
|
+
// constraint failure on re-run over a populated DB. extract-test-results runs
|
|
126
|
+
// later in the pipeline and repopulates it.
|
|
127
|
+
db.run("DELETE FROM device_test_results");
|
|
124
128
|
db.run("DELETE FROM devices");
|
|
125
129
|
|
|
126
130
|
const insert = db.prepare(`INSERT INTO devices (
|