@tikoci/rosetta 0.4.1 → 0.4.2
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 +13 -1
- package/package.json +1 -1
- package/src/db.ts +18 -2
- package/src/extract-test-results.ts +86 -13
- package/src/mcp-http.test.ts +168 -23
- package/src/mcp.ts +130 -2
- package/src/paths.ts +8 -0
- package/src/query.test.ts +321 -14
- package/src/query.ts +327 -29
- package/src/setup.ts +7 -2
package/src/query.ts
CHANGED
|
@@ -25,6 +25,8 @@ export type SearchResponse = {
|
|
|
25
25
|
total: number;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
type CsvScalar = string | number | null;
|
|
29
|
+
|
|
28
30
|
const DEFAULT_LIMIT = 8;
|
|
29
31
|
const MAX_TERMS = 8;
|
|
30
32
|
const MIN_TERM_LENGTH = 2;
|
|
@@ -85,6 +87,21 @@ const COMPOUND_TERMS: [string, string][] = [
|
|
|
85
87
|
["address", "list"],
|
|
86
88
|
];
|
|
87
89
|
|
|
90
|
+
function escapeCsv(value: CsvScalar): string {
|
|
91
|
+
if (value === null) return "";
|
|
92
|
+
const text = String(value);
|
|
93
|
+
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function rowsToCsv<T extends Record<string, CsvScalar>>(
|
|
97
|
+
rows: T[],
|
|
98
|
+
columns: Array<keyof T & string>,
|
|
99
|
+
): string {
|
|
100
|
+
const header = columns.join(",");
|
|
101
|
+
const body = rows.map((row) => columns.map((column) => escapeCsv(row[column] ?? null)).join(",")).join("\n");
|
|
102
|
+
return body ? `${header}\n${body}\n` : `${header}\n`;
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
export function extractTerms(question: string): string[] {
|
|
89
106
|
return question
|
|
90
107
|
.toLowerCase()
|
|
@@ -577,6 +594,79 @@ function runCalloutsFtsQuery(
|
|
|
577
594
|
}
|
|
578
595
|
}
|
|
579
596
|
|
|
597
|
+
/** Diff two RouterOS versions — which command paths were added/removed between them. */
|
|
598
|
+
export type CommandDiffResult = {
|
|
599
|
+
from_version: string;
|
|
600
|
+
to_version: string;
|
|
601
|
+
path_prefix: string | null;
|
|
602
|
+
added: string[];
|
|
603
|
+
removed: string[];
|
|
604
|
+
added_count: number;
|
|
605
|
+
removed_count: number;
|
|
606
|
+
note: string | null;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
export function diffCommandVersions(
|
|
610
|
+
fromVersion: string,
|
|
611
|
+
toVersion: string,
|
|
612
|
+
pathPrefix?: string,
|
|
613
|
+
): CommandDiffResult {
|
|
614
|
+
const allVersionRows = db
|
|
615
|
+
.prepare("SELECT version FROM ros_versions")
|
|
616
|
+
.all() as Array<{ version: string }>;
|
|
617
|
+
const knownVersions = allVersionRows.map((r) => r.version).sort(compareVersions);
|
|
618
|
+
|
|
619
|
+
const notes: string[] = [];
|
|
620
|
+
if (knownVersions.length > 0 && !knownVersions.includes(fromVersion)) {
|
|
621
|
+
notes.push(`Version ${fromVersion} is not in the tracked range (${knownVersions[0]}–${knownVersions[knownVersions.length - 1]}). Results may be incomplete.`);
|
|
622
|
+
}
|
|
623
|
+
if (knownVersions.length > 0 && !knownVersions.includes(toVersion)) {
|
|
624
|
+
notes.push(`Version ${toVersion} is not in the tracked range (${knownVersions[0]}–${knownVersions[knownVersions.length - 1]}). Results may be incomplete.`);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const prefix = pathPrefix || null; // treat empty string same as undefined
|
|
628
|
+
// Match the prefix itself OR any sub-path under it
|
|
629
|
+
const prefixFilter = prefix ? " AND (command_path = ? OR command_path LIKE ? || '/%')" : "";
|
|
630
|
+
const prefixParams = (v: string) => prefix ? [v, prefix, prefix] : [v];
|
|
631
|
+
|
|
632
|
+
type Row = { command_path: string };
|
|
633
|
+
|
|
634
|
+
const addedRows = db
|
|
635
|
+
.prepare(
|
|
636
|
+
`SELECT DISTINCT cv_to.command_path
|
|
637
|
+
FROM command_versions cv_to
|
|
638
|
+
WHERE cv_to.ros_version = ?${prefixFilter}
|
|
639
|
+
AND cv_to.command_path NOT IN (
|
|
640
|
+
SELECT command_path FROM command_versions WHERE ros_version = ?${prefixFilter}
|
|
641
|
+
)
|
|
642
|
+
ORDER BY cv_to.command_path`,
|
|
643
|
+
)
|
|
644
|
+
.all(...prefixParams(toVersion), ...prefixParams(fromVersion)) as Row[];
|
|
645
|
+
|
|
646
|
+
const removedRows = db
|
|
647
|
+
.prepare(
|
|
648
|
+
`SELECT DISTINCT cv_from.command_path
|
|
649
|
+
FROM command_versions cv_from
|
|
650
|
+
WHERE cv_from.ros_version = ?${prefixFilter}
|
|
651
|
+
AND cv_from.command_path NOT IN (
|
|
652
|
+
SELECT command_path FROM command_versions WHERE ros_version = ?${prefixFilter}
|
|
653
|
+
)
|
|
654
|
+
ORDER BY cv_from.command_path`,
|
|
655
|
+
)
|
|
656
|
+
.all(...prefixParams(fromVersion), ...prefixParams(toVersion)) as Row[];
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
from_version: fromVersion,
|
|
660
|
+
to_version: toVersion,
|
|
661
|
+
path_prefix: prefix,
|
|
662
|
+
added: addedRows.map((r) => r.command_path),
|
|
663
|
+
removed: removedRows.map((r) => r.command_path),
|
|
664
|
+
added_count: addedRows.length,
|
|
665
|
+
removed_count: removedRows.length,
|
|
666
|
+
note: notes.length > 0 ? notes.join(" ") : null,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
580
670
|
/** Check which RouterOS versions include a given command path. */
|
|
581
671
|
export function checkCommandVersions(
|
|
582
672
|
commandPath: string,
|
|
@@ -678,6 +768,31 @@ export type DeviceTestResult = {
|
|
|
678
768
|
throughput_mbps: number | null;
|
|
679
769
|
};
|
|
680
770
|
|
|
771
|
+
/** Map Unicode superscript digits to ASCII equivalents for product name matching.
|
|
772
|
+
* MikroTik uses ² and ³ in product names (hAP ax³, hAP ac²), but users type ASCII. */
|
|
773
|
+
const SUPERSCRIPT_TO_ASCII: [string, string][] = [
|
|
774
|
+
['\u00B9', '1'], // ¹
|
|
775
|
+
['\u00B2', '2'], // ²
|
|
776
|
+
['\u00B3', '3'], // ³
|
|
777
|
+
];
|
|
778
|
+
|
|
779
|
+
/** SQL expression to normalize Unicode superscript digits in a column to ASCII.
|
|
780
|
+
* Wraps the column in nested REPLACE calls. */
|
|
781
|
+
const NORMALIZE_PRODUCT_NAME = (col: string) =>
|
|
782
|
+
SUPERSCRIPT_TO_ASCII.reduce(
|
|
783
|
+
(expr, [sup, ascii]) => `REPLACE(${expr}, '${sup}', '${ascii}')`,
|
|
784
|
+
col,
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
/** Normalize a device query: replace Unicode superscript digits with ASCII. */
|
|
788
|
+
export function normalizeDeviceQuery(query: string): string {
|
|
789
|
+
let result = query;
|
|
790
|
+
for (const [sup, ascii] of SUPERSCRIPT_TO_ASCII) {
|
|
791
|
+
result = result.replaceAll(sup, ascii);
|
|
792
|
+
}
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
|
|
681
796
|
export type DeviceResult = {
|
|
682
797
|
id: number;
|
|
683
798
|
product_name: string;
|
|
@@ -759,34 +874,79 @@ function buildDeviceFtsQuery(terms: string[], mode: "AND" | "OR"): string {
|
|
|
759
874
|
return parts.join(mode === "AND" ? " AND " : " OR ");
|
|
760
875
|
}
|
|
761
876
|
|
|
877
|
+
/** Generate a disambiguation note when multiple devices matched a partial query.
|
|
878
|
+
* Helps the MCP client present meaningful choices to the user. */
|
|
879
|
+
function disambiguationNote(query: string, results: DeviceResult[]): string {
|
|
880
|
+
const names = results.map((r) => r.product_name);
|
|
881
|
+
// Find common prefix
|
|
882
|
+
const shortest = names.reduce((a, b) => (a.length < b.length ? a : b));
|
|
883
|
+
let prefix = "";
|
|
884
|
+
for (let i = 0; i < shortest.length; i++) {
|
|
885
|
+
if (names.every((n) => n[i]?.toLowerCase() === shortest[i]?.toLowerCase())) {
|
|
886
|
+
prefix += shortest[i];
|
|
887
|
+
} else break;
|
|
888
|
+
}
|
|
889
|
+
prefix = prefix.trim();
|
|
890
|
+
// Summarize key differences
|
|
891
|
+
const diffs: string[] = [];
|
|
892
|
+
const enclosures = new Set(names.map((n) => {
|
|
893
|
+
if (/\bOUT\b/i.test(n)) return "outdoor";
|
|
894
|
+
if (/\bIN\b/i.test(n)) return "indoor";
|
|
895
|
+
return null;
|
|
896
|
+
}).filter(Boolean));
|
|
897
|
+
if (enclosures.size > 1) diffs.push("enclosure (indoor/outdoor)");
|
|
898
|
+
const hasPoe = results.map((r) => !!(r.poe_in || r.poe_out));
|
|
899
|
+
if (hasPoe.includes(true) && hasPoe.includes(false)) diffs.push("PoE support");
|
|
900
|
+
const hasPoeOut = results.map((r) => !!r.poe_out);
|
|
901
|
+
if (!diffs.includes("PoE support") && hasPoeOut.includes(true) && hasPoeOut.includes(false)) diffs.push("PoE output");
|
|
902
|
+
const hasWireless = results.map((r) => !!(r.wireless_24_chains || r.wireless_5_chains));
|
|
903
|
+
if (hasWireless.includes(true) && hasWireless.includes(false)) diffs.push("wireless");
|
|
904
|
+
const lteCapable = results.map((r) => (r.sim_slots ?? 0) > 0);
|
|
905
|
+
if (lteCapable.includes(true) && lteCapable.includes(false)) diffs.push("LTE/cellular");
|
|
906
|
+
const family = prefix || query;
|
|
907
|
+
const diffStr = diffs.length > 0 ? ` Key differences: ${diffs.join(", ")}.` : "";
|
|
908
|
+
return `${results.length} devices match "${family}".${diffStr} Use the full product name for a specific device.`;
|
|
909
|
+
}
|
|
910
|
+
|
|
762
911
|
/** Look up a device by exact name or product code, then fall back to LIKE/FTS + filters. */
|
|
763
912
|
export function searchDevices(
|
|
764
913
|
query: string,
|
|
765
914
|
filters: DeviceFilters = {},
|
|
766
915
|
limit = 10,
|
|
767
|
-
): { results: DeviceResult[]; mode: "exact" | "fts" | "like" | "filter" | "fts+or"; total: number; has_more: boolean } {
|
|
768
|
-
//
|
|
769
|
-
|
|
916
|
+
): { results: DeviceResult[]; mode: "exact" | "fts" | "like" | "filter" | "fts+or"; total: number; has_more: boolean; note?: string } {
|
|
917
|
+
// Normalize Unicode superscripts → ASCII digits for all matching stages.
|
|
918
|
+
// Users type "ax3" for "ax³", "ac2" for "ac²" — normalize once, use everywhere.
|
|
919
|
+
const q = normalizeDeviceQuery(query);
|
|
920
|
+
const normalizedName = NORMALIZE_PRODUCT_NAME('product_name');
|
|
921
|
+
|
|
922
|
+
// 1. Try exact match on product_name or product_code.
|
|
923
|
+
// Compares normalized query against both raw and superscript-normalized product_name.
|
|
924
|
+
if (q) {
|
|
770
925
|
const exact = db
|
|
771
|
-
.prepare(`${DEVICE_SELECT} WHERE product_name = ? COLLATE NOCASE OR product_code = ? COLLATE NOCASE`)
|
|
772
|
-
.all(
|
|
926
|
+
.prepare(`${DEVICE_SELECT} WHERE product_name = ? COLLATE NOCASE OR product_code = ? COLLATE NOCASE OR ${normalizedName} = ? COLLATE NOCASE`)
|
|
927
|
+
.all(q, q, q) as DeviceResult[];
|
|
773
928
|
if (exact.length > 0) {
|
|
774
929
|
return { results: attachTestResults(exact), mode: "exact", total: exact.length, has_more: false };
|
|
775
930
|
}
|
|
776
931
|
}
|
|
777
932
|
|
|
778
933
|
// 2. LIKE-based prefix/substring match on product_name and product_code.
|
|
934
|
+
// Splits on whitespace, dashes, and underscores so that dash-separated
|
|
935
|
+
// queries like "rb5009-out" still find "RB5009UPr+S+OUT".
|
|
779
936
|
// For 144 rows this is instant and catches model number substrings
|
|
780
937
|
// that FTS5 token matching misses (e.g. "RB1100" → "RB1100AHx4").
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
938
|
+
// Also normalizes product_name in SQL so "ax3" matches "ax³".
|
|
939
|
+
if (q) {
|
|
940
|
+
const rawTerms = q.trim().split(/[\s\-_]+/);
|
|
941
|
+
const longTerms = rawTerms.filter((t) => t.length >= 2);
|
|
942
|
+
// Preserve single-digit terms (version numbers like "3" in "hap ax 3")
|
|
943
|
+
// but only when accompanied by longer terms to avoid overly broad matches.
|
|
944
|
+
const digitTerms = rawTerms.filter((t) => t.length === 1 && /^\d$/.test(t));
|
|
945
|
+
const likeTerms = (longTerms.length > 0 ? [...longTerms, ...digitTerms] : longTerms)
|
|
786
946
|
.map((t) => `%${t}%`);
|
|
787
947
|
if (likeTerms.length > 0) {
|
|
788
948
|
const likeConditions = likeTerms.map(
|
|
789
|
-
() =>
|
|
949
|
+
() => `(${normalizedName} LIKE ? COLLATE NOCASE OR d.product_code LIKE ? COLLATE NOCASE)`,
|
|
790
950
|
);
|
|
791
951
|
const likeParams = likeTerms.flatMap((t) => [t, t]);
|
|
792
952
|
// Fetch limit+1 to detect truncation
|
|
@@ -799,7 +959,29 @@ export function searchDevices(
|
|
|
799
959
|
if (trimmed.length <= 5) attachTestResults(trimmed);
|
|
800
960
|
// Single match in any mode gets test results
|
|
801
961
|
else if (trimmed.length === 1) attachTestResults(trimmed);
|
|
802
|
-
|
|
962
|
+
const note = trimmed.length > 1 ? disambiguationNote(q, trimmed) : undefined;
|
|
963
|
+
return { results: trimmed, mode: "like", total: trimmed.length, has_more: hasMore, note };
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// 2b. Slug-normalized LIKE: strip all separators from both query and product_url slug.
|
|
969
|
+
// Handles concatenated AKAs ("hapax3", "fiberboxplus", "wapaxlte7") and superscript
|
|
970
|
+
// queries ("hap ax3" → slug hap_ax3 → stripped hapax3). Anchors to /product/ prefix
|
|
971
|
+
// to avoid matching domain or path components.
|
|
972
|
+
if (q) {
|
|
973
|
+
const slugQuery = q.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
974
|
+
if (slugQuery.length >= 5) {
|
|
975
|
+
const slugPattern = `%/product/%${slugQuery}%`;
|
|
976
|
+
const slugSql = `${DEVICE_SELECT} d WHERE d.product_url IS NOT NULL AND REPLACE(LOWER(d.product_url), '_', '') LIKE ? ORDER BY d.product_name LIMIT ?`;
|
|
977
|
+
const slugResults = db.prepare(slugSql).all(slugPattern, limit + 1) as DeviceResult[];
|
|
978
|
+
if (slugResults.length > 0) {
|
|
979
|
+
const hasMore = slugResults.length > limit;
|
|
980
|
+
const trimmed = hasMore ? slugResults.slice(0, limit) : slugResults;
|
|
981
|
+
if (trimmed.length <= 5) attachTestResults(trimmed);
|
|
982
|
+
else if (trimmed.length === 1) attachTestResults(trimmed);
|
|
983
|
+
const note = trimmed.length > 1 ? disambiguationNote(q, trimmed) : undefined;
|
|
984
|
+
return { results: trimmed, mode: "like", total: trimmed.length, has_more: hasMore, note };
|
|
803
985
|
}
|
|
804
986
|
}
|
|
805
987
|
}
|
|
@@ -834,7 +1016,7 @@ export function searchDevices(
|
|
|
834
1016
|
whereClauses.push("d.sim_slots > 0");
|
|
835
1017
|
}
|
|
836
1018
|
|
|
837
|
-
const terms =
|
|
1019
|
+
const terms = q ? extractTerms(q) : [];
|
|
838
1020
|
|
|
839
1021
|
if (terms.length > 0) {
|
|
840
1022
|
// FTS with filters — use prefix matching for device model numbers
|
|
@@ -897,9 +1079,6 @@ export type DeviceTestRow = {
|
|
|
897
1079
|
product_name: string;
|
|
898
1080
|
product_code: string | null;
|
|
899
1081
|
architecture: string;
|
|
900
|
-
cpu: string | null;
|
|
901
|
-
cpu_cores: number | null;
|
|
902
|
-
cpu_frequency: string | null;
|
|
903
1082
|
test_type: string;
|
|
904
1083
|
mode: string;
|
|
905
1084
|
configuration: string;
|
|
@@ -908,16 +1087,15 @@ export type DeviceTestRow = {
|
|
|
908
1087
|
throughput_mbps: number | null;
|
|
909
1088
|
};
|
|
910
1089
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
): { results: DeviceTestRow[]; total: number } {
|
|
1090
|
+
type DeviceTestFilters = {
|
|
1091
|
+
test_type?: string;
|
|
1092
|
+
mode?: string;
|
|
1093
|
+
configuration?: string;
|
|
1094
|
+
packet_size?: number;
|
|
1095
|
+
sort_by?: "mbps" | "kpps";
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
function buildTestWhereClause(filters: DeviceTestFilters): { whereClause: string; params: (string | number)[] } {
|
|
921
1099
|
const where: string[] = [];
|
|
922
1100
|
const params: (string | number)[] = [];
|
|
923
1101
|
|
|
@@ -938,7 +1116,17 @@ export function searchDeviceTests(
|
|
|
938
1116
|
params.push(filters.packet_size);
|
|
939
1117
|
}
|
|
940
1118
|
|
|
941
|
-
|
|
1119
|
+
return {
|
|
1120
|
+
whereClause: where.length > 0 ? `WHERE ${where.join(" AND ")}` : "",
|
|
1121
|
+
params,
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
export function searchDeviceTests(
|
|
1126
|
+
filters: DeviceTestFilters,
|
|
1127
|
+
limit = 50,
|
|
1128
|
+
): { results: DeviceTestRow[]; total: number } {
|
|
1129
|
+
const { whereClause, params } = buildTestWhereClause(filters);
|
|
942
1130
|
const orderCol = filters.sort_by === "kpps" ? "t.throughput_kpps" : "t.throughput_mbps";
|
|
943
1131
|
|
|
944
1132
|
// Total count (before limit)
|
|
@@ -946,8 +1134,7 @@ export function searchDeviceTests(
|
|
|
946
1134
|
JOIN devices d ON d.id = t.device_id ${whereClause}`;
|
|
947
1135
|
const total = Number((db.prepare(totalSql).get(...params) as { c: number }).c);
|
|
948
1136
|
|
|
949
|
-
const sql = `SELECT d.product_name, d.product_code, d.architecture,
|
|
950
|
-
d.cpu_cores, d.cpu_frequency,
|
|
1137
|
+
const sql = `SELECT d.product_name, d.product_code, d.architecture,
|
|
951
1138
|
t.test_type, t.mode, t.configuration, t.packet_size,
|
|
952
1139
|
t.throughput_kpps, t.throughput_mbps
|
|
953
1140
|
FROM device_test_results t
|
|
@@ -960,6 +1147,117 @@ export function searchDeviceTests(
|
|
|
960
1147
|
return { results, total };
|
|
961
1148
|
}
|
|
962
1149
|
|
|
1150
|
+
type DeviceTestCsvRow = {
|
|
1151
|
+
product_name: string;
|
|
1152
|
+
product_code: string | null;
|
|
1153
|
+
architecture: string | null;
|
|
1154
|
+
cpu: string | null;
|
|
1155
|
+
cpu_cores: number | null;
|
|
1156
|
+
cpu_frequency: string | null;
|
|
1157
|
+
test_type: string;
|
|
1158
|
+
mode: string;
|
|
1159
|
+
configuration: string;
|
|
1160
|
+
packet_size: number;
|
|
1161
|
+
throughput_kpps: number | null;
|
|
1162
|
+
throughput_mbps: number | null;
|
|
1163
|
+
product_url: string | null;
|
|
1164
|
+
};
|
|
1165
|
+
|
|
1166
|
+
export function exportDeviceTestsCsv(): string {
|
|
1167
|
+
const rows = db.prepare(`SELECT d.product_name, d.product_code, d.architecture,
|
|
1168
|
+
d.cpu, d.cpu_cores, d.cpu_frequency,
|
|
1169
|
+
t.test_type, t.mode, t.configuration, t.packet_size,
|
|
1170
|
+
t.throughput_kpps, t.throughput_mbps,
|
|
1171
|
+
d.product_url
|
|
1172
|
+
FROM device_test_results t
|
|
1173
|
+
JOIN devices d ON d.id = t.device_id
|
|
1174
|
+
ORDER BY d.product_name, t.test_type, t.mode, t.configuration, t.packet_size DESC`).all() as DeviceTestCsvRow[];
|
|
1175
|
+
|
|
1176
|
+
return rowsToCsv(rows, [
|
|
1177
|
+
"product_name",
|
|
1178
|
+
"product_code",
|
|
1179
|
+
"architecture",
|
|
1180
|
+
"cpu",
|
|
1181
|
+
"cpu_cores",
|
|
1182
|
+
"cpu_frequency",
|
|
1183
|
+
"test_type",
|
|
1184
|
+
"mode",
|
|
1185
|
+
"configuration",
|
|
1186
|
+
"packet_size",
|
|
1187
|
+
"throughput_kpps",
|
|
1188
|
+
"throughput_mbps",
|
|
1189
|
+
"product_url",
|
|
1190
|
+
]);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
type DeviceCsvRow = {
|
|
1194
|
+
product_name: string;
|
|
1195
|
+
product_code: string | null;
|
|
1196
|
+
architecture: string | null;
|
|
1197
|
+
cpu: string | null;
|
|
1198
|
+
cpu_cores: number | null;
|
|
1199
|
+
cpu_frequency: string | null;
|
|
1200
|
+
license_level: number | null;
|
|
1201
|
+
operating_system: string | null;
|
|
1202
|
+
ram: string | null;
|
|
1203
|
+
ram_mb: number | null;
|
|
1204
|
+
storage: string | null;
|
|
1205
|
+
storage_mb: number | null;
|
|
1206
|
+
dimensions: string | null;
|
|
1207
|
+
poe_in: string | null;
|
|
1208
|
+
poe_out: string | null;
|
|
1209
|
+
max_power_w: number | null;
|
|
1210
|
+
wireless_24_chains: number | null;
|
|
1211
|
+
wireless_5_chains: number | null;
|
|
1212
|
+
eth_fast: number | null;
|
|
1213
|
+
eth_gigabit: number | null;
|
|
1214
|
+
eth_2500: number | null;
|
|
1215
|
+
sfp_ports: number | null;
|
|
1216
|
+
sfp_plus_ports: number | null;
|
|
1217
|
+
eth_multigig: number | null;
|
|
1218
|
+
usb_ports: number | null;
|
|
1219
|
+
sim_slots: number | null;
|
|
1220
|
+
msrp_usd: number | null;
|
|
1221
|
+
product_url: string | null;
|
|
1222
|
+
block_diagram_url: string | null;
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
export function exportDevicesCsv(): string {
|
|
1226
|
+
const rows = db.prepare(`${DEVICE_SELECT} ORDER BY product_name`).all() as DeviceCsvRow[];
|
|
1227
|
+
|
|
1228
|
+
return rowsToCsv(rows, [
|
|
1229
|
+
"product_name",
|
|
1230
|
+
"product_code",
|
|
1231
|
+
"architecture",
|
|
1232
|
+
"cpu",
|
|
1233
|
+
"cpu_cores",
|
|
1234
|
+
"cpu_frequency",
|
|
1235
|
+
"license_level",
|
|
1236
|
+
"operating_system",
|
|
1237
|
+
"ram",
|
|
1238
|
+
"ram_mb",
|
|
1239
|
+
"storage",
|
|
1240
|
+
"storage_mb",
|
|
1241
|
+
"dimensions",
|
|
1242
|
+
"poe_in",
|
|
1243
|
+
"poe_out",
|
|
1244
|
+
"max_power_w",
|
|
1245
|
+
"wireless_24_chains",
|
|
1246
|
+
"wireless_5_chains",
|
|
1247
|
+
"eth_fast",
|
|
1248
|
+
"eth_gigabit",
|
|
1249
|
+
"eth_2500",
|
|
1250
|
+
"sfp_ports",
|
|
1251
|
+
"sfp_plus_ports",
|
|
1252
|
+
"eth_multigig",
|
|
1253
|
+
"usb_ports",
|
|
1254
|
+
"sim_slots",
|
|
1255
|
+
"msrp_usd",
|
|
1256
|
+
"product_url",
|
|
1257
|
+
"block_diagram_url",
|
|
1258
|
+
]);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
963
1261
|
/** Get distinct values for test result fields (for discovery). */
|
|
964
1262
|
export function getTestResultMeta(): {
|
|
965
1263
|
test_types: string[];
|
package/src/setup.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { execSync } from "node:child_process";
|
|
11
11
|
import { existsSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { gunzipSync } from "bun";
|
|
13
|
-
import { detectMode, resolveBaseDir, resolveDbPath, resolveVersion } from "./paths.ts";
|
|
13
|
+
import { detectMode, resolveBaseDir, resolveDbPath, resolveVersion, SCHEMA_VERSION } from "./paths.ts";
|
|
14
14
|
|
|
15
15
|
declare const REPO_URL: string;
|
|
16
16
|
|
|
@@ -84,8 +84,13 @@ export async function runSetup(force = false) {
|
|
|
84
84
|
const db = new sqlite(dbPath, { readonly: true });
|
|
85
85
|
const row = db.prepare("SELECT COUNT(*) AS c FROM pages").get() as { c: number };
|
|
86
86
|
const cmdRow = db.prepare("SELECT COUNT(*) AS c FROM commands WHERE type='cmd'").get() as { c: number };
|
|
87
|
+
const versionRow = db.prepare("PRAGMA user_version").get() as { user_version: number };
|
|
87
88
|
db.close();
|
|
88
|
-
|
|
89
|
+
if (versionRow.user_version !== SCHEMA_VERSION) {
|
|
90
|
+
console.warn(` Warning: DB schema version is ${versionRow.user_version}, expected ${SCHEMA_VERSION}.`);
|
|
91
|
+
console.warn(` The downloaded DB may be incompatible with this version of rosetta.`);
|
|
92
|
+
}
|
|
93
|
+
console.log(`✓ Database ready (${row.c} pages, ${cmdRow.c} commands, schema v${versionRow.user_version})`);
|
|
89
94
|
} catch (e) {
|
|
90
95
|
console.error(`✗ Database validation failed: ${e}`);
|
|
91
96
|
const retryCmd = mode === "compiled" ? "rosetta" : mode === "package" ? "bunx @tikoci/rosetta" : "bun run src/setup.ts";
|