@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.
@@ -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
+ }