@tikoci/rosetta 0.8.11 → 0.8.12
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 +1 -1
- package/package.json +1 -1
- package/src/mcp.ts +5 -5
- package/src/release.test.ts +7 -0
- package/src/setup.test.ts +51 -1
- package/src/setup.ts +76 -20
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ This downloads the database and prints config snippets for all supported MCP cli
|
|
|
118
118
|
Need to force a database reload later? Use:
|
|
119
119
|
|
|
120
120
|
```sh
|
|
121
|
-
bunx @tikoci/rosetta --refresh
|
|
121
|
+
bunx @tikoci/rosetta@latest --refresh
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
### Configure your MCP client
|
package/package.json
CHANGED
package/src/mcp.ts
CHANGED
|
@@ -61,14 +61,14 @@ function link(url: string, display?: string): string {
|
|
|
61
61
|
*/
|
|
62
62
|
async function ensureDbReady(log: (msg: string) => void): Promise<void> {
|
|
63
63
|
const { resolveDbPath, SCHEMA_VERSION, resolveVersion } = await import("./paths.ts");
|
|
64
|
-
const { downloadDb, hasMinimumDbContent, probeDb
|
|
64
|
+
const { cleanupAbandonedTempArtifacts, downloadDb, hasMinimumDbContent, probeDb } = await import("./setup.ts");
|
|
65
65
|
|
|
66
66
|
const dbPath = resolveDbPath(import.meta.dirname);
|
|
67
67
|
const runningVersion = resolveVersion(import.meta.dirname);
|
|
68
68
|
|
|
69
|
-
// Always clean
|
|
70
|
-
// whether a download is needed
|
|
71
|
-
|
|
69
|
+
// Always clean abandoned .tmp.* artifacts from previous failed runs when no
|
|
70
|
+
// active download lock exists, regardless of whether a download is needed now.
|
|
71
|
+
cleanupAbandonedTempArtifacts(dbPath);
|
|
72
72
|
|
|
73
73
|
let p = probeDb(dbPath);
|
|
74
74
|
|
|
@@ -80,7 +80,7 @@ async function ensureDbReady(log: (msg: string) => void): Promise<void> {
|
|
|
80
80
|
log("Database downloaded successfully.");
|
|
81
81
|
} catch (e) {
|
|
82
82
|
log(`Auto-download failed: ${e instanceof Error ? e.message : e}`);
|
|
83
|
-
log(`Close other rosetta clients and run: bunx @tikoci/rosetta --refresh`);
|
|
83
|
+
log(`Close other rosetta clients and run: bunx @tikoci/rosetta@latest --refresh`);
|
|
84
84
|
throw new Error(`Unable to start rosetta without a usable database at ${dbPath}.`);
|
|
85
85
|
}
|
|
86
86
|
}
|
package/src/release.test.ts
CHANGED
|
@@ -225,9 +225,16 @@ describe("setup.ts", () => {
|
|
|
225
225
|
test("setup.ts cleans temp DB artifacts instead of accumulating stale .tmp files", () => {
|
|
226
226
|
const src = readText("src/setup.ts");
|
|
227
227
|
expect(src).toContain("cleanupStaleTempArtifacts");
|
|
228
|
+
expect(src).toContain("cleanupAbandonedTempArtifacts");
|
|
228
229
|
expect(src).toContain("tryUnlinkDbSidecars(tmpPath)");
|
|
229
230
|
});
|
|
230
231
|
|
|
232
|
+
test("setup.ts finalizes probe statements before renaming temp DBs", () => {
|
|
233
|
+
const src = readText("src/setup.ts");
|
|
234
|
+
expect(src).toContain("stmt.finalize()");
|
|
235
|
+
expect(src).toContain("sqliteGet");
|
|
236
|
+
});
|
|
237
|
+
|
|
231
238
|
test("no DB open uses { readonly: true } (WAL-shm init trap on macOS)", () => {
|
|
232
239
|
// Freshly-renamed WAL-mode DBs fail to open readonly on macOS until a
|
|
233
240
|
// read-write connection initialises the .shm file. downloadDb explicitly
|
package/src/setup.test.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import sqlite from "bun:sqlite";
|
|
10
10
|
import { afterAll, describe, expect, test } from "bun:test";
|
|
11
|
-
import { existsSync, mkdtempSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdtempSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
|
|
@@ -21,6 +21,7 @@ afterAll(() => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
const {
|
|
24
|
+
cleanupAbandonedTempArtifacts,
|
|
24
25
|
cleanupStaleTempArtifacts,
|
|
25
26
|
dbDownloadUrls,
|
|
26
27
|
probeDb,
|
|
@@ -178,6 +179,19 @@ describe("probeDb", () => {
|
|
|
178
179
|
expect(probe?.releaseTag).toBe("v0.0.0-test");
|
|
179
180
|
});
|
|
180
181
|
|
|
182
|
+
test("closes statements so a probed temp DB can be renamed immediately", () => {
|
|
183
|
+
const dbFile = path.join(tmp, "probe-rename-source.db");
|
|
184
|
+
const renamed = path.join(tmp, "probe-rename-dest.db");
|
|
185
|
+
writeUsableDb(dbFile, "v0.0.0-rename");
|
|
186
|
+
|
|
187
|
+
const probe = probeDb(dbFile);
|
|
188
|
+
expect(probe?.releaseTag).toBe("v0.0.0-rename");
|
|
189
|
+
|
|
190
|
+
renameSync(dbFile, renamed);
|
|
191
|
+
expect(existsSync(dbFile)).toBe(false);
|
|
192
|
+
expect(probeDb(renamed)?.releaseTag).toBe("v0.0.0-rename");
|
|
193
|
+
});
|
|
194
|
+
|
|
181
195
|
test("opens a freshly-renamed WAL-mode DB with no .shm sibling", () => {
|
|
182
196
|
// Reproduces the exact state downloadDb leaves the DB in: journal_mode=WAL
|
|
183
197
|
// on disk, but the .wal/.shm siblings are deleted just before the rename.
|
|
@@ -242,3 +256,39 @@ describe("cleanupStaleTempArtifacts", () => {
|
|
|
242
256
|
}
|
|
243
257
|
});
|
|
244
258
|
});
|
|
259
|
+
|
|
260
|
+
describe("cleanupAbandonedTempArtifacts", () => {
|
|
261
|
+
test("removes fresh temp artifacts when no download lock exists", () => {
|
|
262
|
+
const dbFile = path.join(tmp, "cleanup-abandoned.db");
|
|
263
|
+
const artifacts = [
|
|
264
|
+
`${dbFile}.tmp.222`,
|
|
265
|
+
`${dbFile}.tmp.222-wal`,
|
|
266
|
+
`${dbFile}.tmp.222-shm`,
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
for (const artifact of artifacts) {
|
|
270
|
+
writeFileSync(artifact, "x");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
expect(cleanupAbandonedTempArtifacts(dbFile)).toBe(3);
|
|
274
|
+
for (const artifact of artifacts) {
|
|
275
|
+
expect(existsSync(artifact)).toBe(false);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("preserves fresh temp artifacts while a download lock exists", () => {
|
|
280
|
+
const dbFile = path.join(tmp, "cleanup-active.db");
|
|
281
|
+
const artifact = `${dbFile}.tmp.333`;
|
|
282
|
+
writeFileSync(artifact, "x");
|
|
283
|
+
const lock = tryAcquireDownloadLock(dbFile);
|
|
284
|
+
expect(lock).not.toBeNull();
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
expect(cleanupAbandonedTempArtifacts(dbFile)).toBe(0);
|
|
288
|
+
expect(existsSync(artifact)).toBe(true);
|
|
289
|
+
} finally {
|
|
290
|
+
releaseDownloadLock(lock);
|
|
291
|
+
unlinkSync(artifact);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
package/src/setup.ts
CHANGED
|
@@ -47,6 +47,18 @@ type DbProbe = {
|
|
|
47
47
|
releaseTag: string | null;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
type SQLiteStatement = {
|
|
51
|
+
get: (...params: unknown[]) => unknown;
|
|
52
|
+
finalize: () => void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type SQLiteDatabase = {
|
|
56
|
+
prepare: (sql: string) => SQLiteStatement;
|
|
57
|
+
close: () => void;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type SQLiteConstructor = new (path: string) => SQLiteDatabase;
|
|
61
|
+
|
|
50
62
|
type DownloadLockHandle = {
|
|
51
63
|
fd: number;
|
|
52
64
|
path: string;
|
|
@@ -98,20 +110,20 @@ export function probeDb(dbPath: string): {
|
|
|
98
110
|
} | null {
|
|
99
111
|
if (!looksLikeSqliteFile(dbPath)) return null;
|
|
100
112
|
|
|
113
|
+
let check: SQLiteDatabase | null = null;
|
|
101
114
|
try {
|
|
102
|
-
const { default: sqlite } = require("bun:sqlite");
|
|
103
|
-
|
|
104
|
-
const ver =
|
|
105
|
-
const pages = check
|
|
106
|
-
const cmds = check
|
|
115
|
+
const { default: sqlite } = require("bun:sqlite") as { default: SQLiteConstructor };
|
|
116
|
+
check = new sqlite(dbPath);
|
|
117
|
+
const ver = sqliteGet<{ user_version: number }>(check, "PRAGMA user_version");
|
|
118
|
+
const pages = sqliteGet<{ c: number }>(check, "SELECT COUNT(*) AS c FROM pages");
|
|
119
|
+
const cmds = sqliteGet<{ c: number }>(check, "SELECT COUNT(*) AS c FROM commands");
|
|
107
120
|
let releaseTag: string | null = null;
|
|
108
121
|
try {
|
|
109
|
-
const meta = check
|
|
122
|
+
const meta = sqliteGet<{ value: string } | null>(check, "SELECT value FROM db_meta WHERE key = 'release_tag'");
|
|
110
123
|
releaseTag = meta?.value ?? null;
|
|
111
124
|
} catch {
|
|
112
125
|
// db_meta missing — pre-v5 schema, leave releaseTag null
|
|
113
126
|
}
|
|
114
|
-
check.close();
|
|
115
127
|
return {
|
|
116
128
|
schemaVersion: ver.user_version,
|
|
117
129
|
pages: pages.c,
|
|
@@ -120,6 +132,23 @@ export function probeDb(dbPath: string): {
|
|
|
120
132
|
};
|
|
121
133
|
} catch {
|
|
122
134
|
return null;
|
|
135
|
+
} finally {
|
|
136
|
+
if (check) {
|
|
137
|
+
try {
|
|
138
|
+
check.close();
|
|
139
|
+
} catch {
|
|
140
|
+
// best-effort cleanup; a failed probe should never leave a DB handle open
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sqliteGet<T>(db: SQLiteDatabase, sql: string): T {
|
|
147
|
+
const stmt = db.prepare(sql);
|
|
148
|
+
try {
|
|
149
|
+
return stmt.get() as T;
|
|
150
|
+
} finally {
|
|
151
|
+
stmt.finalize();
|
|
123
152
|
}
|
|
124
153
|
}
|
|
125
154
|
|
|
@@ -258,18 +287,45 @@ export function cleanupStaleTempArtifacts(dbPath: string, staleMs = DOWNLOAD_LOC
|
|
|
258
287
|
return removed;
|
|
259
288
|
}
|
|
260
289
|
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return;
|
|
265
|
-
} catch (e) {
|
|
266
|
-
const code = e instanceof Error && "code" in e ? e.code : undefined;
|
|
267
|
-
// Windows can return EBUSY, EPERM, or EEXIST when the destination is open.
|
|
268
|
-
if (code !== "EBUSY" && code !== "EEXIST" && code !== "EPERM") throw e;
|
|
290
|
+
export function cleanupAbandonedTempArtifacts(dbPath: string): number {
|
|
291
|
+
if (existsSync(lockPathFor(dbPath))) {
|
|
292
|
+
return cleanupStaleTempArtifacts(dbPath);
|
|
269
293
|
}
|
|
294
|
+
return cleanupStaleTempArtifacts(dbPath, -1);
|
|
295
|
+
}
|
|
270
296
|
|
|
271
|
-
|
|
272
|
-
|
|
297
|
+
function isReplaceRaceError(e: unknown): boolean {
|
|
298
|
+
const code = e instanceof Error && "code" in e ? e.code : undefined;
|
|
299
|
+
// Windows can return EBUSY, EPERM, or EEXIST when the source or destination is open.
|
|
300
|
+
return code === "EBUSY" || code === "EEXIST" || code === "EPERM";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function replaceDbFile(tmpPath: string, dbPath: string): Promise<void> {
|
|
304
|
+
const deadline = Date.now() + 30_000;
|
|
305
|
+
let lastError: unknown = null;
|
|
306
|
+
|
|
307
|
+
while (Date.now() <= deadline) {
|
|
308
|
+
try {
|
|
309
|
+
renameSync(tmpPath, dbPath);
|
|
310
|
+
return;
|
|
311
|
+
} catch (e) {
|
|
312
|
+
if (!isReplaceRaceError(e)) throw e;
|
|
313
|
+
lastError = e;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
tryUnlink(dbPath);
|
|
317
|
+
try {
|
|
318
|
+
renameSync(tmpPath, dbPath);
|
|
319
|
+
return;
|
|
320
|
+
} catch (e) {
|
|
321
|
+
if (!isReplaceRaceError(e)) throw e;
|
|
322
|
+
lastError = e;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
await Bun.sleep(250);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
throw lastError;
|
|
273
329
|
}
|
|
274
330
|
|
|
275
331
|
/** Build the version-pinned download URL. Falls back to /latest/ when no version.
|
|
@@ -418,9 +474,9 @@ export async function downloadDb(
|
|
|
418
474
|
if (probe.schemaVersion !== SCHEMA_VERSION) {
|
|
419
475
|
cleanupDbArtifacts(tmpPath);
|
|
420
476
|
lastError = new Error(
|
|
421
|
-
|
|
477
|
+
`Downloaded DB schema=${probe.schemaVersion} does not match this rosetta build (expected ${SCHEMA_VERSION}). ` +
|
|
422
478
|
`This usually means the cached package version is older than the published DB. ` +
|
|
423
|
-
`Run
|
|
479
|
+
`Run: bunx @tikoci/rosetta@latest --refresh`,
|
|
424
480
|
);
|
|
425
481
|
if (isLast) throw lastError;
|
|
426
482
|
log(` ${lastError.message}`);
|
|
@@ -440,7 +496,7 @@ export async function downloadDb(
|
|
|
440
496
|
try {
|
|
441
497
|
tryUnlinkDbSidecars(tmpPath);
|
|
442
498
|
tryUnlinkDbSidecars(dbPath);
|
|
443
|
-
replaceDbFile(tmpPath, dbPath);
|
|
499
|
+
await replaceDbFile(tmpPath, dbPath);
|
|
444
500
|
} catch (e) {
|
|
445
501
|
const existingProbe = probeDb(dbPath);
|
|
446
502
|
if (hasMinimumDbContent(existingProbe) && existingProbe.schemaVersion === probe.schemaVersion && existingProbe.releaseTag === probe.releaseTag) {
|