@tikoci/rosetta 0.6.6 → 0.6.8
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 +2 -0
- package/package.json +1 -1
- package/src/browse.ts +211 -21
- package/src/db.ts +113 -0
- package/src/extract-all-versions.ts +83 -21
- package/src/extract-schema.ts +596 -0
- package/src/extract-skills.ts +390 -0
- package/src/mcp.ts +107 -8
- package/src/paths.ts +9 -2
- package/src/query.test.ts +128 -1
- package/src/query.ts +157 -9
- package/src/release.test.ts +29 -0
- package/src/schema-roundtrip.test.ts +373 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* extract-schema.ts — Import RouterOS command tree from deep-inspect JSON files
|
|
4
|
+
* into the schema_nodes + schema_node_presence tables.
|
|
5
|
+
*
|
|
6
|
+
* Processes both x86 and arm64 deep-inspect files for a single version,
|
|
7
|
+
* merges path sets, marks arch-specific nodes, parses desc_raw into
|
|
8
|
+
* structured columns (data_type, enum_values, etc.), extracts _completion
|
|
9
|
+
* data into _attrs, and derives dir_role.
|
|
10
|
+
*
|
|
11
|
+
* Also regenerates the legacy `commands` and `command_versions` tables
|
|
12
|
+
* for backward compatibility with existing query functions.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* bun run src/extract-schema.ts --x86=<path-or-url> --arm64=<path-or-url> [--version=7.22.1]
|
|
16
|
+
* bun run src/extract-schema.ts --x86=<path-or-url> # x86 only (no arm64)
|
|
17
|
+
* bun run src/extract-schema.ts --accumulate --x86=... --arm64=... # junction only
|
|
18
|
+
*
|
|
19
|
+
* Flags:
|
|
20
|
+
* --version=X Override version (auto-derived from _meta or filename)
|
|
21
|
+
* --channel=X Override channel (auto-derived from version string)
|
|
22
|
+
* --extra Mark as extra-packages build
|
|
23
|
+
* --accumulate Only add to schema_node_presence + command_versions; don't rebuild schema_nodes/commands
|
|
24
|
+
* --x86=<src> Path or URL to deep-inspect.x86.json (or inspect.json)
|
|
25
|
+
* --arm64=<src> Path or URL to deep-inspect.arm64.json
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Types
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
interface DeepInspectMeta {
|
|
33
|
+
version?: string;
|
|
34
|
+
generatedAt?: string;
|
|
35
|
+
architecture?: string;
|
|
36
|
+
apiTransport?: string;
|
|
37
|
+
enrichmentDurationMs?: number;
|
|
38
|
+
crashPathsTested?: string[];
|
|
39
|
+
crashPathsCrashed?: string[];
|
|
40
|
+
crashPathsSafe?: string[];
|
|
41
|
+
completionStats?: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface CompletionValue {
|
|
45
|
+
style?: string;
|
|
46
|
+
preference?: number;
|
|
47
|
+
desc?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FlatNode {
|
|
51
|
+
path: string;
|
|
52
|
+
name: string;
|
|
53
|
+
type: string; // 'dir' | 'cmd' | 'arg'
|
|
54
|
+
parentPath: string | null;
|
|
55
|
+
descRaw: string | null;
|
|
56
|
+
completion: Record<string, CompletionValue> | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ParsedDesc {
|
|
60
|
+
dataType: string | null;
|
|
61
|
+
enumValues: string | null; // JSON array
|
|
62
|
+
enumMulti: number | null;
|
|
63
|
+
typeTag: string | null;
|
|
64
|
+
rangeMin: string | null;
|
|
65
|
+
rangeMax: string | null;
|
|
66
|
+
maxLength: number | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Helpers — exported for testing
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
export function nodeKey(n: FlatNode): string {
|
|
74
|
+
return `${n.path}\0${n.type}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Tree walker — flatten inspect JSON into FlatNode[]
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
export function walk(obj: Record<string, unknown>, parentPath: string, nodes: FlatNode[]) {
|
|
82
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
83
|
+
if (key === "_type" || key === "desc" || key === "_meta" || key === "_completion") continue;
|
|
84
|
+
if (typeof value !== "object" || value === null) continue;
|
|
85
|
+
|
|
86
|
+
const node = value as Record<string, unknown>;
|
|
87
|
+
const nodeType = node._type as string | undefined;
|
|
88
|
+
if (!nodeType) continue;
|
|
89
|
+
|
|
90
|
+
const currentPath = parentPath ? `${parentPath}/${key}` : `/${key}`;
|
|
91
|
+
const descRaw = typeof node.desc === "string" ? node.desc : null;
|
|
92
|
+
const normalizedType = nodeType === "path" ? "dir" : nodeType;
|
|
93
|
+
|
|
94
|
+
// Extract _completion for args
|
|
95
|
+
let completion: Record<string, CompletionValue> | null = null;
|
|
96
|
+
if (node._completion && typeof node._completion === "object") {
|
|
97
|
+
completion = node._completion as Record<string, CompletionValue>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
nodes.push({
|
|
101
|
+
path: currentPath,
|
|
102
|
+
name: key,
|
|
103
|
+
type: normalizedType,
|
|
104
|
+
parentPath: parentPath || null,
|
|
105
|
+
descRaw,
|
|
106
|
+
completion,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Recurse into children (dirs and cmds have children)
|
|
110
|
+
if (normalizedType === "dir" || normalizedType === "cmd") {
|
|
111
|
+
walk(node, currentPath, nodes);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Merge two arch node sets — shared, x86-only, arm64-only
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
export function mergeArchNodes(
|
|
121
|
+
x86Nodes: FlatNode[],
|
|
122
|
+
arm64Nodes: FlatNode[],
|
|
123
|
+
): Array<FlatNode & { arch: string | null }> {
|
|
124
|
+
const x86Map = new Map<string, FlatNode>();
|
|
125
|
+
const arm64Map = new Map<string, FlatNode>();
|
|
126
|
+
for (const n of x86Nodes) x86Map.set(nodeKey(n), n);
|
|
127
|
+
for (const n of arm64Nodes) arm64Map.set(nodeKey(n), n);
|
|
128
|
+
|
|
129
|
+
const allKeys = new Set([...x86Map.keys(), ...arm64Map.keys()]);
|
|
130
|
+
const merged: Array<FlatNode & { arch: string | null }> = [];
|
|
131
|
+
|
|
132
|
+
for (const key of allKeys) {
|
|
133
|
+
const x86Node = x86Map.get(key);
|
|
134
|
+
const arm64Node = arm64Map.get(key);
|
|
135
|
+
|
|
136
|
+
if (x86Node && arm64Node) {
|
|
137
|
+
// Shared — prefer x86 data but merge completion from both if needed
|
|
138
|
+
const m = { ...x86Node, arch: null as string | null };
|
|
139
|
+
if (!m.completion && arm64Node.completion) m.completion = arm64Node.completion;
|
|
140
|
+
if (!m.descRaw && arm64Node.descRaw) m.descRaw = arm64Node.descRaw;
|
|
141
|
+
merged.push(m);
|
|
142
|
+
} else if (x86Node) {
|
|
143
|
+
merged.push({ ...x86Node, arch: "x86" });
|
|
144
|
+
} else if (arm64Node) {
|
|
145
|
+
merged.push({ ...arm64Node, arch: "arm64" });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return merged;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// desc_raw parser — decompose into structured fields
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
export function parseDesc(desc: string | null): ParsedDesc {
|
|
157
|
+
const result: ParsedDesc = {
|
|
158
|
+
dataType: null,
|
|
159
|
+
enumValues: null,
|
|
160
|
+
enumMulti: null,
|
|
161
|
+
typeTag: null,
|
|
162
|
+
rangeMin: null,
|
|
163
|
+
rangeMax: null,
|
|
164
|
+
maxLength: null,
|
|
165
|
+
};
|
|
166
|
+
if (!desc) return result;
|
|
167
|
+
|
|
168
|
+
const trimmed = desc.trim();
|
|
169
|
+
|
|
170
|
+
// "script"
|
|
171
|
+
if (trimmed === "script") {
|
|
172
|
+
result.dataType = "script";
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// "string value, max length N"
|
|
177
|
+
const strMaxMatch = trimmed.match(/^string value,\s*max length\s+(\d+)$/i);
|
|
178
|
+
if (strMaxMatch) {
|
|
179
|
+
result.dataType = "string";
|
|
180
|
+
result.maxLength = Number(strMaxMatch[1]);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// "string value"
|
|
185
|
+
if (trimmed === "string value") {
|
|
186
|
+
result.dataType = "string";
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// "time interval" (bare)
|
|
191
|
+
if (trimmed === "time interval") {
|
|
192
|
+
result.dataType = "time";
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// "A..B (time interval)" — range with time qualifier
|
|
197
|
+
const timeRangeMatch = trimmed.match(/^(\S+)\.\.(\S+)\s+\(time interval\)$/);
|
|
198
|
+
if (timeRangeMatch) {
|
|
199
|
+
result.dataType = "time";
|
|
200
|
+
result.rangeMin = timeRangeMatch[1];
|
|
201
|
+
result.rangeMax = timeRangeMatch[2];
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// "A..B (integer)" — range with integer qualifier
|
|
206
|
+
const intRangeMatch = trimmed.match(/^(\S+)\.\.(\S+)\s+\(integer\)$/);
|
|
207
|
+
if (intRangeMatch) {
|
|
208
|
+
result.dataType = "integer";
|
|
209
|
+
result.rangeMin = intRangeMatch[1];
|
|
210
|
+
result.rangeMax = intRangeMatch[2];
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Generic range: "A..B" without qualifier
|
|
215
|
+
const genericRangeMatch = trimmed.match(/^(\S+)\.\.(\S+)$/);
|
|
216
|
+
if (genericRangeMatch) {
|
|
217
|
+
result.dataType = "range";
|
|
218
|
+
result.rangeMin = genericRangeMatch[1];
|
|
219
|
+
result.rangeMax = genericRangeMatch[2];
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Enum with optional multi marker: "a|b|c[,TypeTag*]"
|
|
224
|
+
// Also matches "a|b|c" (no bracket)
|
|
225
|
+
const enumMatch = trimmed.match(/^([a-zA-Z0-9_-]+(?:\|[a-zA-Z0-9_-]+)+)(?:\[,([^\]*]+)\*?\])?$/);
|
|
226
|
+
if (enumMatch) {
|
|
227
|
+
const values = enumMatch[1].split("|");
|
|
228
|
+
result.dataType = "enum";
|
|
229
|
+
result.enumValues = JSON.stringify(values);
|
|
230
|
+
if (enumMatch[2]) {
|
|
231
|
+
result.enumMulti = 1;
|
|
232
|
+
result.typeTag = enumMatch[2];
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// "integer" (bare)
|
|
238
|
+
if (trimmed === "integer") {
|
|
239
|
+
result.dataType = "integer";
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// If none matched, leave all NULL — desc_raw is preserved
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
// dir_role derivation
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
function deriveDirRoles(nodes: FlatNode[]): Map<string, string> {
|
|
252
|
+
const dirRoles = new Map<string, string>();
|
|
253
|
+
|
|
254
|
+
// Build a map of path → child types
|
|
255
|
+
const childTypes = new Map<string, Set<string>>();
|
|
256
|
+
for (const node of nodes) {
|
|
257
|
+
if (!node.parentPath) continue;
|
|
258
|
+
const set = childTypes.get(node.parentPath);
|
|
259
|
+
if (set) set.add(node.type);
|
|
260
|
+
else childTypes.set(node.parentPath, new Set([node.type]));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const node of nodes) {
|
|
264
|
+
if (node.type !== "dir") continue;
|
|
265
|
+
const children = childTypes.get(node.path);
|
|
266
|
+
if (!children || children.size === 0) {
|
|
267
|
+
dirRoles.set(node.path, "namespace"); // empty dir = namespace
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const hasCmd = children.has("cmd");
|
|
272
|
+
const hasDir = children.has("dir");
|
|
273
|
+
// Args are children of cmds, not dirs directly, but some dirs have args too
|
|
274
|
+
const hasArg = children.has("arg");
|
|
275
|
+
|
|
276
|
+
if (hasCmd && hasDir) {
|
|
277
|
+
dirRoles.set(node.path, "hybrid");
|
|
278
|
+
} else if (hasCmd || hasArg) {
|
|
279
|
+
dirRoles.set(node.path, "list");
|
|
280
|
+
} else {
|
|
281
|
+
dirRoles.set(node.path, "namespace");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return dirRoles;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Version derivation helpers (shared with extract-commands.ts)
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
function deriveVersion(filepath: string): string {
|
|
293
|
+
const match = filepath.match(/\/(\d+\.\d+(?:\.\d+)?(?:beta\d+|rc\d+)?)\//);
|
|
294
|
+
return match?.[1] ?? "unknown";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function deriveChannel(version: string): string {
|
|
298
|
+
if (version.includes("beta") || version.includes("rc")) return "development";
|
|
299
|
+
return "stable";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// Import function — used by CLI and tests
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
interface ImportOptions {
|
|
307
|
+
accumulate: boolean;
|
|
308
|
+
extraPackages: boolean;
|
|
309
|
+
channel: string;
|
|
310
|
+
x86Source: string | null;
|
|
311
|
+
arm64Source: string | null;
|
|
312
|
+
x86Meta?: DeepInspectMeta;
|
|
313
|
+
arm64Meta?: DeepInspectMeta;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function importSchemaNodes(
|
|
317
|
+
// biome-ignore lint/suspicious/noExplicitAny: duck-typed to accept bun:sqlite Database or test stubs
|
|
318
|
+
db: any,
|
|
319
|
+
mergedNodes: Array<FlatNode & { arch: string | null }>,
|
|
320
|
+
version: string,
|
|
321
|
+
opts: ImportOptions,
|
|
322
|
+
) {
|
|
323
|
+
const { accumulate, extraPackages, channel, x86Source, arm64Source, x86Meta, arm64Meta } = opts;
|
|
324
|
+
|
|
325
|
+
// Derive dir_role for all dirs
|
|
326
|
+
const dirRoles = deriveDirRoles(mergedNodes);
|
|
327
|
+
|
|
328
|
+
// Parse desc for all nodes
|
|
329
|
+
const parsedDescs = new Map<string, ParsedDesc>();
|
|
330
|
+
for (const node of mergedNodes) {
|
|
331
|
+
parsedDescs.set(nodeKey(node), parseDesc(node.descRaw));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Register versions in ros_versions (one row per arch file loaded)
|
|
335
|
+
const registerVersion = (
|
|
336
|
+
archMeta: DeepInspectMeta | undefined,
|
|
337
|
+
archName: string,
|
|
338
|
+
sourceUrl: string | null,
|
|
339
|
+
) => {
|
|
340
|
+
if (!sourceUrl) return;
|
|
341
|
+
db.run(
|
|
342
|
+
`INSERT OR REPLACE INTO ros_versions
|
|
343
|
+
(version, arch, channel, extra_packages, extracted_at,
|
|
344
|
+
generated_at, crash_paths_tested, crash_paths_crashed, crash_paths_safe,
|
|
345
|
+
completion_stats, source_url, api_transport, enrichment_duration_ms, _attrs)
|
|
346
|
+
VALUES (?, ?, ?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
347
|
+
[
|
|
348
|
+
version,
|
|
349
|
+
archName,
|
|
350
|
+
channel,
|
|
351
|
+
extraPackages ? 1 : 0,
|
|
352
|
+
archMeta?.generatedAt ?? null,
|
|
353
|
+
archMeta?.crashPathsTested ? JSON.stringify(archMeta.crashPathsTested) : null,
|
|
354
|
+
archMeta?.crashPathsCrashed ? JSON.stringify(archMeta.crashPathsCrashed) : null,
|
|
355
|
+
archMeta?.crashPathsSafe ? JSON.stringify(archMeta.crashPathsSafe) : null,
|
|
356
|
+
archMeta?.completionStats ? JSON.stringify(archMeta.completionStats) : null,
|
|
357
|
+
sourceUrl,
|
|
358
|
+
archMeta?.apiTransport ?? null,
|
|
359
|
+
archMeta?.enrichmentDurationMs ?? null,
|
|
360
|
+
null, // _attrs — reserved for future catch-all
|
|
361
|
+
],
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
if (x86Source) registerVersion(x86Meta, "x86", x86Source);
|
|
366
|
+
if (arm64Source) registerVersion(arm64Meta, "arm64", arm64Source);
|
|
367
|
+
|
|
368
|
+
if (!accumulate) {
|
|
369
|
+
// Primary mode: rebuild schema_nodes and commands
|
|
370
|
+
db.run("DELETE FROM schema_node_presence;");
|
|
371
|
+
db.run("DELETE FROM schema_nodes;");
|
|
372
|
+
|
|
373
|
+
// Insert schema_nodes
|
|
374
|
+
const insertNode = db.prepare(`
|
|
375
|
+
INSERT INTO schema_nodes
|
|
376
|
+
(path, name, type, parent_path, dir_role, desc_raw,
|
|
377
|
+
data_type, enum_values, enum_multi, type_tag,
|
|
378
|
+
range_min, range_max, max_length,
|
|
379
|
+
_arch, _package, _attrs)
|
|
380
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
381
|
+
`);
|
|
382
|
+
|
|
383
|
+
const insertNodes = db.transaction(() => {
|
|
384
|
+
for (const node of mergedNodes) {
|
|
385
|
+
const parsed = parsedDescs.get(nodeKey(node)) ?? parseDesc(node.descRaw);
|
|
386
|
+
const attrs = node.completion
|
|
387
|
+
? JSON.stringify({ completion: node.completion })
|
|
388
|
+
: null;
|
|
389
|
+
|
|
390
|
+
insertNode.run(
|
|
391
|
+
node.path,
|
|
392
|
+
node.name,
|
|
393
|
+
node.type,
|
|
394
|
+
node.parentPath,
|
|
395
|
+
node.type === "dir" ? (dirRoles.get(node.path) ?? null) : null,
|
|
396
|
+
node.descRaw,
|
|
397
|
+
parsed.dataType,
|
|
398
|
+
parsed.enumValues,
|
|
399
|
+
parsed.enumMulti,
|
|
400
|
+
parsed.typeTag,
|
|
401
|
+
parsed.rangeMin,
|
|
402
|
+
parsed.rangeMax,
|
|
403
|
+
parsed.maxLength,
|
|
404
|
+
node.arch,
|
|
405
|
+
null, // _package
|
|
406
|
+
attrs,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
insertNodes();
|
|
411
|
+
|
|
412
|
+
// Set parent_id via self-join — no type filter: args have cmd parents, dirs/cmds have dir parents
|
|
413
|
+
db.run(`
|
|
414
|
+
UPDATE schema_nodes SET parent_id = (
|
|
415
|
+
SELECT p.id FROM schema_nodes p
|
|
416
|
+
WHERE p.path = schema_nodes.parent_path
|
|
417
|
+
LIMIT 1
|
|
418
|
+
)
|
|
419
|
+
WHERE parent_path IS NOT NULL;
|
|
420
|
+
`);
|
|
421
|
+
|
|
422
|
+
const nodeCount = (db.prepare("SELECT COUNT(*) as c FROM schema_nodes").get() as { c: number }).c;
|
|
423
|
+
console.log(`\nInserted ${nodeCount} schema_nodes (primary: ${version})`);
|
|
424
|
+
|
|
425
|
+
// Show type breakdown
|
|
426
|
+
const typeBreakdown = db
|
|
427
|
+
.prepare("SELECT type, COUNT(*) as c FROM schema_nodes GROUP BY type ORDER BY c DESC")
|
|
428
|
+
.all() as Array<{ type: string; c: number }>;
|
|
429
|
+
for (const t of typeBreakdown) {
|
|
430
|
+
console.log(` ${t.type}: ${t.c}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Show arch breakdown
|
|
434
|
+
const archBreakdown = db
|
|
435
|
+
.prepare(
|
|
436
|
+
"SELECT COALESCE(_arch, 'both') as arch, COUNT(*) as c FROM schema_nodes GROUP BY _arch ORDER BY c DESC",
|
|
437
|
+
)
|
|
438
|
+
.all() as Array<{ arch: string; c: number }>;
|
|
439
|
+
for (const a of archBreakdown) {
|
|
440
|
+
console.log(` arch=${a.arch}: ${a.c}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Show completion stats
|
|
444
|
+
const completionCount = (
|
|
445
|
+
db.prepare("SELECT COUNT(*) as c FROM schema_nodes WHERE _attrs IS NOT NULL").get() as { c: number }
|
|
446
|
+
).c;
|
|
447
|
+
console.log(` nodes with _attrs (completion): ${completionCount}`);
|
|
448
|
+
|
|
449
|
+
// -----------------------------------------------------------------------
|
|
450
|
+
// Regenerate legacy `commands` table for backward compatibility
|
|
451
|
+
// -----------------------------------------------------------------------
|
|
452
|
+
db.run("DELETE FROM commands;");
|
|
453
|
+
|
|
454
|
+
const insertCmd = db.prepare(`
|
|
455
|
+
INSERT OR IGNORE INTO commands (path, name, type, parent_path, description, ros_version)
|
|
456
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
457
|
+
`);
|
|
458
|
+
|
|
459
|
+
const insertCmds = db.transaction(() => {
|
|
460
|
+
for (const node of mergedNodes) {
|
|
461
|
+
insertCmd.run(node.path, node.name, node.type, node.parentPath, node.descRaw, version);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
insertCmds();
|
|
465
|
+
|
|
466
|
+
const cmdCount = (db.prepare("SELECT COUNT(*) as c FROM commands").get() as { c: number }).c;
|
|
467
|
+
console.log(`\nRegenerated ${cmdCount} commands (compat layer)`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Always populate schema_node_presence and command_versions for this version
|
|
471
|
+
{
|
|
472
|
+
// schema_node_presence
|
|
473
|
+
db.run("DELETE FROM schema_node_presence WHERE version = ?", [version]);
|
|
474
|
+
|
|
475
|
+
const insertPresence = db.prepare(`
|
|
476
|
+
INSERT OR IGNORE INTO schema_node_presence (node_id, version)
|
|
477
|
+
SELECT id, ? FROM schema_nodes WHERE path = ? AND type = ?
|
|
478
|
+
`);
|
|
479
|
+
|
|
480
|
+
const insertPresences = db.transaction(() => {
|
|
481
|
+
// All merged nodes exist in this version
|
|
482
|
+
for (const node of mergedNodes) {
|
|
483
|
+
insertPresence.run(version, node.path, node.type);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
insertPresences();
|
|
487
|
+
|
|
488
|
+
const presenceCount = (
|
|
489
|
+
db.prepare("SELECT COUNT(*) as c FROM schema_node_presence WHERE version = ?").get(version) as { c: number }
|
|
490
|
+
).c;
|
|
491
|
+
console.log(`Recorded ${presenceCount} schema_node_presence entries for ${version}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
{
|
|
495
|
+
// command_versions (compat)
|
|
496
|
+
db.run("DELETE FROM command_versions WHERE ros_version = ?", [version]);
|
|
497
|
+
|
|
498
|
+
const insertVersion = db.prepare(`
|
|
499
|
+
INSERT OR IGNORE INTO command_versions (command_path, ros_version)
|
|
500
|
+
VALUES (?, ?)
|
|
501
|
+
`);
|
|
502
|
+
|
|
503
|
+
const insertVersions = db.transaction(() => {
|
|
504
|
+
for (const node of mergedNodes) {
|
|
505
|
+
insertVersion.run(node.path, version);
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
insertVersions();
|
|
509
|
+
|
|
510
|
+
const versionCount = (
|
|
511
|
+
db.prepare("SELECT COUNT(*) as c FROM command_versions WHERE ros_version = ?").get(version) as { c: number }
|
|
512
|
+
).c;
|
|
513
|
+
console.log(`Recorded ${versionCount} command_versions entries for ${version}`);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const totalVersions = (
|
|
517
|
+
db.prepare("SELECT COUNT(DISTINCT version) AS c FROM schema_node_presence").get() as { c: number }
|
|
518
|
+
).c;
|
|
519
|
+
console.log(`Total versions tracked: ${totalVersions}`);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
// CLI entry point
|
|
524
|
+
// ---------------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
if (import.meta.main) {
|
|
527
|
+
const { db, initDb } = await import("./db.ts");
|
|
528
|
+
const { loadJson } = await import("./restraml.ts");
|
|
529
|
+
|
|
530
|
+
const cliArgs = process.argv.slice(2);
|
|
531
|
+
const accumulate = cliArgs.includes("--accumulate");
|
|
532
|
+
const extraPackages = cliArgs.includes("--extra");
|
|
533
|
+
|
|
534
|
+
const flagArgs = Object.fromEntries(
|
|
535
|
+
cliArgs
|
|
536
|
+
.filter((a) => a.startsWith("--") && a.includes("="))
|
|
537
|
+
.map((a) => {
|
|
538
|
+
const [k, ...v] = a.slice(2).split("=");
|
|
539
|
+
return [k, v.join("=")];
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
const x86Source = flagArgs.x86 ?? null;
|
|
544
|
+
const arm64Source = flagArgs.arm64 ?? null;
|
|
545
|
+
|
|
546
|
+
if (!x86Source && !arm64Source) {
|
|
547
|
+
console.error("Error: at least one of --x86=<path> or --arm64=<path> is required");
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Load both arch files
|
|
552
|
+
const x86Data = x86Source ? await loadJson<Record<string, unknown>>(x86Source) : null;
|
|
553
|
+
const arm64Data = arm64Source ? await loadJson<Record<string, unknown>>(arm64Source) : null;
|
|
554
|
+
|
|
555
|
+
const x86Meta = x86Data?._meta as DeepInspectMeta | undefined;
|
|
556
|
+
const arm64Meta = arm64Data?._meta as DeepInspectMeta | undefined;
|
|
557
|
+
const meta = x86Meta ?? arm64Meta;
|
|
558
|
+
|
|
559
|
+
// Derive version
|
|
560
|
+
const version =
|
|
561
|
+
flagArgs.version ??
|
|
562
|
+
meta?.version ??
|
|
563
|
+
(x86Source ? deriveVersion(x86Source) : arm64Source ? deriveVersion(arm64Source) : "unknown");
|
|
564
|
+
const channel = flagArgs.channel ?? deriveChannel(version);
|
|
565
|
+
|
|
566
|
+
console.log(`extract-schema: version=${version} channel=${channel} accumulate=${accumulate}`);
|
|
567
|
+
if (x86Source) console.log(` x86: ${x86Source}`);
|
|
568
|
+
if (arm64Source) console.log(` arm64: ${arm64Source}`);
|
|
569
|
+
|
|
570
|
+
// Walk both trees
|
|
571
|
+
const x86Nodes: FlatNode[] = [];
|
|
572
|
+
const arm64Nodes: FlatNode[] = [];
|
|
573
|
+
if (x86Data) walk(x86Data, "", x86Nodes);
|
|
574
|
+
if (arm64Data) walk(arm64Data, "", arm64Nodes);
|
|
575
|
+
console.log(` x86 nodes: ${x86Nodes.length}, arm64 nodes: ${arm64Nodes.length}`);
|
|
576
|
+
|
|
577
|
+
// Merge
|
|
578
|
+
const mergedNodes = mergeArchNodes(x86Nodes, arm64Nodes);
|
|
579
|
+
const shared = mergedNodes.filter((n) => n.arch === null).length;
|
|
580
|
+
const x86Only = mergedNodes.filter((n) => n.arch === "x86").length;
|
|
581
|
+
const arm64Only = mergedNodes.filter((n) => n.arch === "arm64").length;
|
|
582
|
+
console.log(` merged: ${mergedNodes.length} (shared=${shared}, x86-only=${x86Only}, arm64-only=${arm64Only})`);
|
|
583
|
+
|
|
584
|
+
// Initialize DB and import
|
|
585
|
+
initDb();
|
|
586
|
+
|
|
587
|
+
importSchemaNodes(db, mergedNodes, version, {
|
|
588
|
+
accumulate,
|
|
589
|
+
extraPackages,
|
|
590
|
+
channel,
|
|
591
|
+
x86Source,
|
|
592
|
+
arm64Source,
|
|
593
|
+
x86Meta,
|
|
594
|
+
arm64Meta,
|
|
595
|
+
});
|
|
596
|
+
}
|