@tikoci/rosetta 0.2.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/LICENSE +21 -0
- package/README.md +333 -0
- package/bin/rosetta.js +34 -0
- package/matrix/2026-03-25/matrix.csv +145 -0
- package/matrix/CLAUDE.md +7 -0
- package/matrix/get-mikrotik-products-csv.sh +20 -0
- package/package.json +34 -0
- package/src/assess-html.ts +267 -0
- package/src/db.ts +360 -0
- package/src/extract-all-versions.ts +147 -0
- package/src/extract-changelogs.ts +266 -0
- package/src/extract-commands.ts +175 -0
- package/src/extract-devices.ts +194 -0
- package/src/extract-html.ts +379 -0
- package/src/extract-properties.ts +234 -0
- package/src/link-commands.ts +208 -0
- package/src/mcp.ts +725 -0
- package/src/query.test.ts +994 -0
- package/src/query.ts +990 -0
- package/src/release.test.ts +280 -0
- package/src/restraml.ts +65 -0
- package/src/search.ts +49 -0
- package/src/setup.ts +224 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* release.test.ts — Release readiness tests.
|
|
3
|
+
*
|
|
4
|
+
* Validates that project files are consistent and release artifacts
|
|
5
|
+
* will be built correctly. No network, no database — just file reads
|
|
6
|
+
* and structural checks.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
const ROOT = path.resolve(import.meta.dirname, "..");
|
|
13
|
+
|
|
14
|
+
function readText(relPath: string): string {
|
|
15
|
+
return readFileSync(path.join(ROOT, relPath), "utf-8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// package.json health
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
describe("package.json", () => {
|
|
23
|
+
const pkg = JSON.parse(readText("package.json"));
|
|
24
|
+
|
|
25
|
+
test("version is valid semver", () => {
|
|
26
|
+
expect(pkg.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("name is @tikoci/rosetta", () => {
|
|
30
|
+
expect(pkg.name).toBe("@tikoci/rosetta");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("repository URL contains tikoci/rosetta", () => {
|
|
34
|
+
expect(pkg.repository.url).toContain("tikoci/rosetta");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("no duplicate scripts that Makefile owns", () => {
|
|
38
|
+
const makefileOwned = ["start", "extract", "assess", "search"];
|
|
39
|
+
for (const name of makefileOwned) {
|
|
40
|
+
expect(pkg.scripts[name]).toBeUndefined();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("required scripts exist", () => {
|
|
45
|
+
expect(pkg.scripts.test).toBeDefined();
|
|
46
|
+
expect(pkg.scripts.typecheck).toBeDefined();
|
|
47
|
+
expect(pkg.scripts.lint).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("bin points to JS shim", () => {
|
|
51
|
+
expect(pkg.bin.rosetta).toBe("bin/rosetta.js");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("files includes bin/, src/, matrix/", () => {
|
|
55
|
+
expect(pkg.files).toContain("bin/");
|
|
56
|
+
expect(pkg.files).toContain("src/");
|
|
57
|
+
expect(pkg.files).toContain("matrix/");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// npm bin shim
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
describe("bin/rosetta.js", () => {
|
|
66
|
+
test("shim exists", () => {
|
|
67
|
+
expect(existsSync(path.join(ROOT, "bin/rosetta.js"))).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("has node shebang", () => {
|
|
71
|
+
const src = readText("bin/rosetta.js");
|
|
72
|
+
expect(src.startsWith("#!/usr/bin/env node")).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("detects Bun runtime", () => {
|
|
76
|
+
const src = readText("bin/rosetta.js");
|
|
77
|
+
expect(src).toContain('typeof Bun !== "undefined"');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("falls back to spawning bun for Node", () => {
|
|
81
|
+
const src = readText("bin/rosetta.js");
|
|
82
|
+
expect(src).toContain('spawn("bun"');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Build constants declarations
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
describe("build-time constants", () => {
|
|
91
|
+
test("mcp.ts declares VERSION", () => {
|
|
92
|
+
const src = readText("src/mcp.ts");
|
|
93
|
+
expect(src).toContain("declare const VERSION");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("mcp.ts declares IS_COMPILED", () => {
|
|
97
|
+
const src = readText("src/mcp.ts");
|
|
98
|
+
expect(src).toContain("declare const IS_COMPILED");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("db.ts declares IS_COMPILED", () => {
|
|
102
|
+
const src = readText("src/db.ts");
|
|
103
|
+
expect(src).toContain("declare const IS_COMPILED");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("setup.ts declares REPO_URL and VERSION", () => {
|
|
107
|
+
const src = readText("src/setup.ts");
|
|
108
|
+
expect(src).toContain("declare const REPO_URL");
|
|
109
|
+
expect(src).toContain("declare const VERSION");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("build script injects all three constants", () => {
|
|
113
|
+
const src = readText("scripts/build-release.ts");
|
|
114
|
+
expect(src).toContain("VERSION=");
|
|
115
|
+
expect(src).toContain("REPO_URL=");
|
|
116
|
+
expect(src).toContain("IS_COMPILED=");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Build script structure
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
describe("build-release.ts", () => {
|
|
125
|
+
test("script exists", () => {
|
|
126
|
+
expect(existsSync(path.join(ROOT, "scripts/build-release.ts"))).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("defines all 4 platform targets", () => {
|
|
130
|
+
const src = readText("scripts/build-release.ts");
|
|
131
|
+
expect(src).toContain("macos-arm64");
|
|
132
|
+
expect(src).toContain("macos-x64");
|
|
133
|
+
expect(src).toContain("windows-x64");
|
|
134
|
+
expect(src).toContain("linux-x64");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("uses bun build --compile", () => {
|
|
138
|
+
const src = readText("scripts/build-release.ts");
|
|
139
|
+
expect(src).toContain("--compile");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("compresses database", () => {
|
|
143
|
+
const src = readText("scripts/build-release.ts");
|
|
144
|
+
expect(src).toContain("ros-help.db.gz");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// setup.ts URL consistency
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
describe("setup.ts", () => {
|
|
153
|
+
test("REPO_URL fallback matches package.json repository", () => {
|
|
154
|
+
const src = readText("src/setup.ts");
|
|
155
|
+
const pkg = JSON.parse(readText("package.json"));
|
|
156
|
+
|
|
157
|
+
// Extract the fallback repo string: `? REPO_URL : "tikoci/rosetta"`
|
|
158
|
+
const match = src.match(/REPO_URL\s*:\s*"([^"]+)"/);
|
|
159
|
+
expect(match).not.toBeNull();
|
|
160
|
+
|
|
161
|
+
const fallbackRepo = match?.[1];
|
|
162
|
+
expect(pkg.repository.url).toContain(fallbackRepo);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("downloads from GitHub Releases URL", () => {
|
|
166
|
+
const src = readText("src/setup.ts");
|
|
167
|
+
expect(src).toContain("github.com/");
|
|
168
|
+
expect(src).toContain("/releases/latest/download/ros-help.db.gz");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Makefile has release targets
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
describe("Makefile", () => {
|
|
177
|
+
const makefile = readText("Makefile");
|
|
178
|
+
|
|
179
|
+
test("has preflight target", () => {
|
|
180
|
+
expect(makefile).toContain("preflight:");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("has build-release target", () => {
|
|
184
|
+
expect(makefile).toContain("build-release:");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("has release target", () => {
|
|
188
|
+
expect(makefile).toContain("release:");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("release depends on preflight", () => {
|
|
192
|
+
expect(makefile).toMatch(/^release:.*preflight/m);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("release depends on build-release", () => {
|
|
196
|
+
expect(makefile).toMatch(/^release:.*build-release/m);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("preflight checks dirty tree", () => {
|
|
200
|
+
expect(makefile).toContain("git diff --quiet");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("FORCE flag controls tag behavior", () => {
|
|
204
|
+
expect(makefile).toContain("FORCE");
|
|
205
|
+
expect(makefile).toContain("git tag -f");
|
|
206
|
+
expect(makefile).toContain("--clobber");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// CI release workflow
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
describe("release.yml", () => {
|
|
215
|
+
test("workflow file exists", () => {
|
|
216
|
+
expect(existsSync(path.join(ROOT, ".github/workflows/release.yml"))).toBe(
|
|
217
|
+
true,
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("has required inputs", () => {
|
|
222
|
+
const src = readText(".github/workflows/release.yml");
|
|
223
|
+
expect(src).toContain("html_url:");
|
|
224
|
+
expect(src).toContain("version:");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("tolerates Confluence zip with absolute path root entry", () => {
|
|
228
|
+
const src = readText(".github/workflows/release.yml");
|
|
229
|
+
// Confluence exports include a bare "/" entry → unzip exits 2
|
|
230
|
+
expect(src).toMatch(/unzip.*\|\|.*\$\?.*-eq.*2/);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("runs extraction pipeline", () => {
|
|
234
|
+
const src = readText(".github/workflows/release.yml");
|
|
235
|
+
expect(src).toContain("extract-html.ts");
|
|
236
|
+
expect(src).toContain("extract-properties.ts");
|
|
237
|
+
expect(src).toContain("extract-commands.ts");
|
|
238
|
+
expect(src).toContain("extract-devices.ts");
|
|
239
|
+
expect(src).toContain("extract-changelogs.ts");
|
|
240
|
+
expect(src).toContain("link-commands.ts");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("runs quality gate before release", () => {
|
|
244
|
+
const src = readText(".github/workflows/release.yml");
|
|
245
|
+
expect(src).toContain("bun run typecheck");
|
|
246
|
+
expect(src).toContain("bun test");
|
|
247
|
+
expect(src).toContain("bun run lint");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("creates GitHub Release", () => {
|
|
251
|
+
const src = readText(".github/workflows/release.yml");
|
|
252
|
+
expect(src).toContain("gh release create");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("publishes to npm", () => {
|
|
256
|
+
const src = readText(".github/workflows/release.yml");
|
|
257
|
+
expect(src).toContain("npm publish");
|
|
258
|
+
expect(src).toContain("NPM_TOKEN");
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// CLI flags in mcp.ts
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
describe("CLI flags", () => {
|
|
267
|
+
const src = readText("src/mcp.ts");
|
|
268
|
+
|
|
269
|
+
test("supports --version flag", () => {
|
|
270
|
+
expect(src).toContain("--version");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("supports --help flag", () => {
|
|
274
|
+
expect(src).toContain("--help");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("supports --setup flag", () => {
|
|
278
|
+
expect(src).toContain("--setup");
|
|
279
|
+
});
|
|
280
|
+
});
|
package/src/restraml.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* restraml.ts — Shared helpers for fetching data from tikoci/restraml.
|
|
3
|
+
*
|
|
4
|
+
* restraml publishes inspect.json files to GitHub Pages. Version discovery
|
|
5
|
+
* uses the GitHub API (1 call), but all inspect.json fetches go through
|
|
6
|
+
* GitHub Pages (no rate limit).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** GitHub Pages base URL — inspect.json files served here (no rate limit) */
|
|
10
|
+
export const RESTRAML_PAGES_URL = "https://tikoci.github.io/restraml";
|
|
11
|
+
|
|
12
|
+
/** GitHub API endpoint for version directory listing (60 req/hr unauthenticated) */
|
|
13
|
+
const RESTRAML_API_CONTENTS_URL = "https://api.github.com/repos/tikoci/restraml/contents/docs";
|
|
14
|
+
|
|
15
|
+
export function isHttpUrl(value: string): boolean {
|
|
16
|
+
return /^https?:\/\//i.test(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeBaseUrl(url: string): string {
|
|
20
|
+
return url.replace(/\/+$/, "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface GitHubContentEntry {
|
|
24
|
+
name: string;
|
|
25
|
+
type: "file" | "dir";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Discover available RouterOS versions from the restraml GitHub repo.
|
|
30
|
+
* Uses 1 GitHub API call to list the docs/ directory, returns version strings.
|
|
31
|
+
*/
|
|
32
|
+
export async function discoverRemoteVersions(): Promise<string[]> {
|
|
33
|
+
const response = await fetch(RESTRAML_API_CONTENTS_URL, {
|
|
34
|
+
headers: { Accept: "application/vnd.github.v3+json" },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
const rateLimitRemaining = response.headers.get("x-ratelimit-remaining");
|
|
39
|
+
const detail = rateLimitRemaining === "0"
|
|
40
|
+
? " (GitHub API rate limit exceeded — try again later or pass a local docs path)"
|
|
41
|
+
: "";
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Failed to list restraml versions: HTTP ${response.status}${detail}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const entries = (await response.json()) as GitHubContentEntry[];
|
|
48
|
+
return entries
|
|
49
|
+
.filter((e) => e.type === "dir" && /^\d+\.\d+/.test(e.name))
|
|
50
|
+
.map((e) => e.name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load a JSON file from a URL or local path.
|
|
55
|
+
*/
|
|
56
|
+
export async function loadJson<T = unknown>(source: string): Promise<T> {
|
|
57
|
+
if (isHttpUrl(source)) {
|
|
58
|
+
const response = await fetch(source);
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(`Failed to fetch ${source}: HTTP ${response.status}`);
|
|
61
|
+
}
|
|
62
|
+
return (await response.json()) as T;
|
|
63
|
+
}
|
|
64
|
+
return (await Bun.file(source).json()) as T;
|
|
65
|
+
}
|
package/src/search.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI search over RouterOS documentation database.
|
|
4
|
+
* Usage: bun run src/search.ts "firewall filter"
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { db, initDb } from "./db.ts";
|
|
8
|
+
|
|
9
|
+
const query = process.argv.slice(2).join(" ");
|
|
10
|
+
if (!query) {
|
|
11
|
+
console.error("Usage: bun run src/search.ts <query>");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
initDb();
|
|
16
|
+
|
|
17
|
+
type SearchResult = {
|
|
18
|
+
id: number;
|
|
19
|
+
title: string;
|
|
20
|
+
path: string;
|
|
21
|
+
url: string;
|
|
22
|
+
word_count: number;
|
|
23
|
+
code_lines: number;
|
|
24
|
+
excerpt: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const results = db
|
|
28
|
+
.query<SearchResult, [string]>(
|
|
29
|
+
`SELECT s.id, s.title, s.path, s.url, s.word_count, s.code_lines,
|
|
30
|
+
snippet(pages_fts, 2, '>>>', '<<<', '...', 30) as excerpt
|
|
31
|
+
FROM pages_fts fts
|
|
32
|
+
JOIN pages s ON s.id = fts.rowid
|
|
33
|
+
WHERE pages_fts MATCH ?
|
|
34
|
+
ORDER BY bm25(pages_fts, 3.0, 2.0, 1.0, 0.5) LIMIT 10`,
|
|
35
|
+
)
|
|
36
|
+
.all(query);
|
|
37
|
+
|
|
38
|
+
if (results.length === 0) {
|
|
39
|
+
console.log(`No results for "${query}"`);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`\n${results.length} results for "${query}":\n`);
|
|
44
|
+
for (const r of results) {
|
|
45
|
+
console.log(` [${r.id}] ${r.path}`);
|
|
46
|
+
console.log(` ${r.word_count} words, ${r.code_lines} code lines`);
|
|
47
|
+
console.log(` ${r.url}`);
|
|
48
|
+
console.log(` ${r.excerpt}\n`);
|
|
49
|
+
}
|
package/src/setup.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* setup.ts — Download the RouterOS documentation database and print MCP client config.
|
|
3
|
+
*
|
|
4
|
+
* Called by `rosetta --setup` (compiled binary) or `bun run src/setup.ts` (dev).
|
|
5
|
+
* Downloads ros-help.db.gz from the latest GitHub Release, decompresses it,
|
|
6
|
+
* validates the DB, and prints config snippets for each MCP client.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, writeFileSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { gunzipSync } from "bun";
|
|
12
|
+
|
|
13
|
+
declare const REPO_URL: string;
|
|
14
|
+
declare const VERSION: string;
|
|
15
|
+
|
|
16
|
+
const GITHUB_REPO =
|
|
17
|
+
typeof REPO_URL !== "undefined" ? REPO_URL : "tikoci/rosetta";
|
|
18
|
+
const RELEASE_VERSION =
|
|
19
|
+
typeof VERSION !== "undefined" ? VERSION : "dev";
|
|
20
|
+
|
|
21
|
+
/** Where the binary (or dev project root) lives */
|
|
22
|
+
function getBaseDir(): string {
|
|
23
|
+
// If IS_COMPILED is defined, use the executable's directory
|
|
24
|
+
// Otherwise use project root (one level up from src/)
|
|
25
|
+
try {
|
|
26
|
+
// @ts-expect-error IS_COMPILED defined at build time
|
|
27
|
+
if (typeof IS_COMPILED !== "undefined" && IS_COMPILED) {
|
|
28
|
+
return path.dirname(process.execPath);
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// not compiled
|
|
32
|
+
}
|
|
33
|
+
return path.resolve(import.meta.dirname, "..");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The binary/script path for MCP config */
|
|
37
|
+
function getServerCommand(): string {
|
|
38
|
+
try {
|
|
39
|
+
// @ts-expect-error IS_COMPILED defined at build time
|
|
40
|
+
if (typeof IS_COMPILED !== "undefined" && IS_COMPILED) {
|
|
41
|
+
return process.execPath;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// not compiled
|
|
45
|
+
}
|
|
46
|
+
// Dev mode — bun run src/mcp.ts
|
|
47
|
+
return path.resolve(import.meta.dirname, "mcp.ts");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Check if a DB file exists and has actual page data */
|
|
51
|
+
function dbHasData(dbPath: string): boolean {
|
|
52
|
+
if (!existsSync(dbPath)) return false;
|
|
53
|
+
try {
|
|
54
|
+
const { default: sqlite } = require("bun:sqlite");
|
|
55
|
+
const check = new sqlite(dbPath, { readonly: true });
|
|
56
|
+
const row = check.prepare("SELECT COUNT(*) AS c FROM pages").get() as { c: number };
|
|
57
|
+
check.close();
|
|
58
|
+
return row.c > 0;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Download ros-help.db.gz from GitHub Releases, decompress, and write to dbPath */
|
|
65
|
+
export async function downloadDb(
|
|
66
|
+
dbPath: string,
|
|
67
|
+
log: (msg: string) => void = console.log,
|
|
68
|
+
) {
|
|
69
|
+
const url = `https://github.com/${GITHUB_REPO}/releases/latest/download/ros-help.db.gz`;
|
|
70
|
+
log(`Downloading database from GitHub Releases...`);
|
|
71
|
+
log(` ${url}`);
|
|
72
|
+
|
|
73
|
+
const response = await fetch(url, { redirect: "follow" });
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const contentLength = response.headers.get("content-length");
|
|
79
|
+
const totalMB = contentLength ? (Number(contentLength) / 1024 / 1024).toFixed(1) : "?";
|
|
80
|
+
log(` Downloading ${totalMB} MB (compressed)...`);
|
|
81
|
+
|
|
82
|
+
const compressed = new Uint8Array(await response.arrayBuffer());
|
|
83
|
+
log(` Decompressing...`);
|
|
84
|
+
|
|
85
|
+
const decompressed = gunzipSync(compressed);
|
|
86
|
+
writeFileSync(dbPath, decompressed);
|
|
87
|
+
|
|
88
|
+
const sizeMB = (decompressed.byteLength / 1024 / 1024).toFixed(1);
|
|
89
|
+
log(` Wrote ${sizeMB} MB to ${dbPath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function runSetup(force = false) {
|
|
93
|
+
const baseDir = getBaseDir();
|
|
94
|
+
const dbPath = path.join(baseDir, "ros-help.db");
|
|
95
|
+
const serverCmd = getServerCommand();
|
|
96
|
+
const isCompiled = serverCmd === process.execPath;
|
|
97
|
+
|
|
98
|
+
console.log(`rosetta ${RELEASE_VERSION}`);
|
|
99
|
+
console.log();
|
|
100
|
+
|
|
101
|
+
// ── Download DB if needed ──
|
|
102
|
+
const needsDownload = force || !dbHasData(dbPath);
|
|
103
|
+
if (!needsDownload) {
|
|
104
|
+
console.log(`Database already exists: ${dbPath}`);
|
|
105
|
+
console.log(` (use --setup --force to re-download)`);
|
|
106
|
+
} else {
|
|
107
|
+
await downloadDb(dbPath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Validate DB ──
|
|
111
|
+
console.log();
|
|
112
|
+
try {
|
|
113
|
+
const { default: sqlite } = await import("bun:sqlite");
|
|
114
|
+
const db = new sqlite(dbPath, { readonly: true });
|
|
115
|
+
const row = db.prepare("SELECT COUNT(*) AS c FROM pages").get() as { c: number };
|
|
116
|
+
const cmdRow = db.prepare("SELECT COUNT(*) AS c FROM commands WHERE type='cmd'").get() as { c: number };
|
|
117
|
+
db.close();
|
|
118
|
+
console.log(`✓ Database ready (${row.c} pages, ${cmdRow.c} commands)`);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error(`✗ Database validation failed: ${e}`);
|
|
121
|
+
console.error(` Try re-downloading with: ${isCompiled ? path.basename(serverCmd) : "bun run src/setup.ts"} --setup --force`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Print config snippets ──
|
|
126
|
+
console.log();
|
|
127
|
+
console.log("─".repeat(60));
|
|
128
|
+
console.log("Configure your MCP client:");
|
|
129
|
+
console.log("─".repeat(60));
|
|
130
|
+
|
|
131
|
+
if (isCompiled) {
|
|
132
|
+
printCompiledConfig(serverCmd);
|
|
133
|
+
} else {
|
|
134
|
+
printDevConfig(baseDir);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function printCompiledConfig(serverCmd: string) {
|
|
139
|
+
const cmdJson = JSON.stringify(serverCmd);
|
|
140
|
+
|
|
141
|
+
// Claude Desktop
|
|
142
|
+
const isMac = process.platform === "darwin";
|
|
143
|
+
const configPath = isMac
|
|
144
|
+
? "~/Library/Application\\ Support/Claude/claude_desktop_config.json"
|
|
145
|
+
: "%APPDATA%\\Claude\\claude_desktop_config.json";
|
|
146
|
+
|
|
147
|
+
console.log();
|
|
148
|
+
console.log("▸ Claude Desktop");
|
|
149
|
+
console.log(` Edit: ${configPath}`);
|
|
150
|
+
console.log();
|
|
151
|
+
console.log(` {`);
|
|
152
|
+
console.log(` "mcpServers": {`);
|
|
153
|
+
console.log(` "rosetta": {`);
|
|
154
|
+
console.log(` "command": ${cmdJson}`);
|
|
155
|
+
console.log(` }`);
|
|
156
|
+
console.log(` }`);
|
|
157
|
+
console.log(` }`);
|
|
158
|
+
console.log();
|
|
159
|
+
console.log(` Then restart Claude Desktop.`);
|
|
160
|
+
|
|
161
|
+
// Claude Code
|
|
162
|
+
console.log();
|
|
163
|
+
console.log("▸ Claude Code");
|
|
164
|
+
console.log(` claude mcp add rosetta ${serverCmd}`);
|
|
165
|
+
|
|
166
|
+
// VS Code Copilot
|
|
167
|
+
console.log();
|
|
168
|
+
console.log("▸ VS Code Copilot (User Settings JSON)");
|
|
169
|
+
console.log();
|
|
170
|
+
console.log(` "mcp": {`);
|
|
171
|
+
console.log(` "servers": {`);
|
|
172
|
+
console.log(` "rosetta": {`);
|
|
173
|
+
console.log(` "command": ${cmdJson}`);
|
|
174
|
+
console.log(` }`);
|
|
175
|
+
console.log(` }`);
|
|
176
|
+
console.log(` }`);
|
|
177
|
+
console.log();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function printDevConfig(baseDir: string) {
|
|
181
|
+
const cwdJson = JSON.stringify(baseDir);
|
|
182
|
+
|
|
183
|
+
// Claude Desktop
|
|
184
|
+
const isMac = process.platform === "darwin";
|
|
185
|
+
const configPath = isMac
|
|
186
|
+
? "~/Library/Application\\ Support/Claude/claude_desktop_config.json"
|
|
187
|
+
: "%APPDATA%\\Claude\\claude_desktop_config.json";
|
|
188
|
+
|
|
189
|
+
console.log();
|
|
190
|
+
console.log("▸ Claude Desktop");
|
|
191
|
+
console.log(` Edit: ${configPath}`);
|
|
192
|
+
console.log();
|
|
193
|
+
console.log(` {`);
|
|
194
|
+
console.log(` "mcpServers": {`);
|
|
195
|
+
console.log(` "rosetta": {`);
|
|
196
|
+
console.log(` "command": "bun",`);
|
|
197
|
+
console.log(` "args": ["run", "src/mcp.ts"],`);
|
|
198
|
+
console.log(` "cwd": ${cwdJson}`);
|
|
199
|
+
console.log(` }`);
|
|
200
|
+
console.log(` }`);
|
|
201
|
+
console.log(` }`);
|
|
202
|
+
console.log();
|
|
203
|
+
console.log(` Then restart Claude Desktop.`);
|
|
204
|
+
|
|
205
|
+
// Claude Code
|
|
206
|
+
console.log();
|
|
207
|
+
console.log("▸ Claude Code");
|
|
208
|
+
console.log(` claude mcp add rosetta -- bun run src/mcp.ts`);
|
|
209
|
+
|
|
210
|
+
// VS Code Copilot
|
|
211
|
+
console.log();
|
|
212
|
+
console.log("▸ VS Code Copilot");
|
|
213
|
+
console.log(` The repo includes .vscode/mcp.json — just open the folder in VS Code.`);
|
|
214
|
+
console.log();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Run directly
|
|
218
|
+
if (import.meta.main) {
|
|
219
|
+
const force = process.argv.includes("--force");
|
|
220
|
+
runSetup(force).catch((e) => {
|
|
221
|
+
console.error(e);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
});
|
|
224
|
+
}
|