@tikoci/rosetta 0.8.10 → 0.8.11
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/package.json +1 -1
- package/src/mcp.ts +14 -37
- package/src/release.test.ts +7 -1
- package/src/setup.test.ts +45 -2
- package/src/setup.ts +111 -43
package/package.json
CHANGED
package/src/mcp.ts
CHANGED
|
@@ -61,44 +61,23 @@ 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 } = await import("./setup.ts");
|
|
65
|
-
const { default: sqlite } = await import("bun:sqlite");
|
|
64
|
+
const { downloadDb, hasMinimumDbContent, probeDb, cleanupStaleTempArtifacts } = await import("./setup.ts");
|
|
66
65
|
|
|
67
66
|
const dbPath = resolveDbPath(import.meta.dirname);
|
|
68
67
|
const runningVersion = resolveVersion(import.meta.dirname);
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
* shared-memory file. Same gotcha as setup.ts::probeDb. */
|
|
74
|
-
function probe(): { pages: number; schemaVersion: number; releaseTag: string | null } | null {
|
|
75
|
-
try {
|
|
76
|
-
const check = new sqlite(dbPath);
|
|
77
|
-
const pages = (check.prepare("SELECT COUNT(*) AS c FROM pages").get() as { c: number }).c;
|
|
78
|
-
const ver = (check.prepare("PRAGMA user_version").get() as { user_version: number }).user_version;
|
|
79
|
-
let releaseTag: string | null = null;
|
|
80
|
-
try {
|
|
81
|
-
const meta = check.prepare("SELECT value FROM db_meta WHERE key = 'release_tag'").get() as { value: string } | null;
|
|
82
|
-
releaseTag = meta?.value ?? null;
|
|
83
|
-
} catch {
|
|
84
|
-
// db_meta missing on pre-v5 DBs — leave null
|
|
85
|
-
}
|
|
86
|
-
check.close();
|
|
87
|
-
return { pages, schemaVersion: ver, releaseTag };
|
|
88
|
-
} catch {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
69
|
+
// Always clean stale .tmp.* artifacts from previous failed runs, regardless of
|
|
70
|
+
// whether a download is needed this time.
|
|
71
|
+
cleanupStaleTempArtifacts(dbPath);
|
|
92
72
|
|
|
93
|
-
let p =
|
|
73
|
+
let p = probeDb(dbPath);
|
|
94
74
|
|
|
95
75
|
// Case 1: DB missing or empty → first-time download.
|
|
96
|
-
if (!p
|
|
76
|
+
if (!hasMinimumDbContent(p)) {
|
|
97
77
|
log(`No usable database at ${dbPath} — downloading...`);
|
|
98
78
|
try {
|
|
99
|
-
await downloadDb(dbPath, log);
|
|
79
|
+
p = await downloadDb(dbPath, log);
|
|
100
80
|
log("Database downloaded successfully.");
|
|
101
|
-
p = probe();
|
|
102
81
|
} catch (e) {
|
|
103
82
|
log(`Auto-download failed: ${e instanceof Error ? e.message : e}`);
|
|
104
83
|
log(`Close other rosetta clients and run: bunx @tikoci/rosetta --refresh`);
|
|
@@ -110,8 +89,8 @@ async function ensureDbReady(log: (msg: string) => void): Promise<void> {
|
|
|
110
89
|
throw new Error(`Database probe failed after download: ${dbPath}`);
|
|
111
90
|
}
|
|
112
91
|
|
|
113
|
-
if (p
|
|
114
|
-
throw new Error(`Database remained
|
|
92
|
+
if (!hasMinimumDbContent(p)) {
|
|
93
|
+
throw new Error(`Database remained incomplete after recovery: ${dbPath}`);
|
|
115
94
|
}
|
|
116
95
|
|
|
117
96
|
// Case 2: Schema mismatch → re-download, then re-probe and fail hard if
|
|
@@ -122,27 +101,25 @@ async function ensureDbReady(log: (msg: string) => void): Promise<void> {
|
|
|
122
101
|
`Re-downloading database...`,
|
|
123
102
|
);
|
|
124
103
|
try {
|
|
125
|
-
await downloadDb(dbPath, log);
|
|
104
|
+
p = await downloadDb(dbPath, log);
|
|
126
105
|
} catch (e) {
|
|
127
106
|
log(`✗ Auto-recovery download failed: ${e instanceof Error ? e.message : e}`);
|
|
128
107
|
log(
|
|
129
108
|
` This rosetta build (v${runningVersion}) cannot use the existing DB. ` +
|
|
130
|
-
`Close other rosetta clients
|
|
109
|
+
`Close other rosetta clients and run: bunx @tikoci/rosetta@latest --refresh`,
|
|
131
110
|
);
|
|
132
111
|
throw new Error(`Unable to recover an incompatible database at ${dbPath}.`);
|
|
133
112
|
}
|
|
134
|
-
|
|
135
|
-
if (!p2 || p2.schemaVersion !== SCHEMA_VERSION) {
|
|
113
|
+
if (!p || !hasMinimumDbContent(p) || p.schemaVersion !== SCHEMA_VERSION) {
|
|
136
114
|
log(
|
|
137
|
-
`✗ Still incompatible after re-download (DB=${
|
|
115
|
+
`✗ Still incompatible after re-download (DB=${p?.schemaVersion ?? "unreadable"}, expected=${SCHEMA_VERSION}).`,
|
|
138
116
|
);
|
|
139
117
|
log(
|
|
140
118
|
` The published database does not match this rosetta build (v${runningVersion}). ` +
|
|
141
|
-
`Run
|
|
119
|
+
`Run: bunx @tikoci/rosetta@latest --refresh`,
|
|
142
120
|
);
|
|
143
121
|
throw new Error(`Database remained incompatible after recovery: ${dbPath}`);
|
|
144
122
|
}
|
|
145
|
-
p = p2;
|
|
146
123
|
}
|
|
147
124
|
|
|
148
125
|
// Quietly emit a one-line provenance banner so MCP-client logs show what's loaded.
|
package/src/release.test.ts
CHANGED
|
@@ -219,7 +219,13 @@ describe("setup.ts", () => {
|
|
|
219
219
|
test("mcp.ts aborts startup instead of continuing with an empty DB", () => {
|
|
220
220
|
const src = readText("src/mcp.ts");
|
|
221
221
|
expect(src).toContain("Unable to start rosetta without a usable database");
|
|
222
|
-
expect(src).toContain("Database remained
|
|
222
|
+
expect(src).toContain("Database remained incomplete after recovery");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("setup.ts cleans temp DB artifacts instead of accumulating stale .tmp files", () => {
|
|
226
|
+
const src = readText("src/setup.ts");
|
|
227
|
+
expect(src).toContain("cleanupStaleTempArtifacts");
|
|
228
|
+
expect(src).toContain("tryUnlinkDbSidecars(tmpPath)");
|
|
223
229
|
});
|
|
224
230
|
|
|
225
231
|
test("no DB open uses { readonly: true } (WAL-shm init trap on macOS)", () => {
|
package/src/setup.test.ts
CHANGED
|
@@ -20,7 +20,14 @@ afterAll(() => {
|
|
|
20
20
|
} catch {}
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
cleanupStaleTempArtifacts,
|
|
25
|
+
dbDownloadUrls,
|
|
26
|
+
probeDb,
|
|
27
|
+
releaseDownloadLock,
|
|
28
|
+
tryAcquireDownloadLock,
|
|
29
|
+
waitForUsableDb,
|
|
30
|
+
} = await import("./setup.ts");
|
|
24
31
|
const { SCHEMA_VERSION } = await import("./paths.ts");
|
|
25
32
|
|
|
26
33
|
function writeUsableDb(dbFile: string, releaseTag = "v0.0.0-test"): void {
|
|
@@ -115,6 +122,20 @@ describe("download lock helpers", () => {
|
|
|
115
122
|
expect(probe?.pages).toBe(100);
|
|
116
123
|
expect(probe?.commands).toBe(1000);
|
|
117
124
|
});
|
|
125
|
+
|
|
126
|
+
test("waitForUsableDb does not create a missing canonical DB while waiting", async () => {
|
|
127
|
+
const dbFile = path.join(tmp, "wait-no-create.db");
|
|
128
|
+
const lock = tryAcquireDownloadLock(dbFile);
|
|
129
|
+
expect(lock).not.toBeNull();
|
|
130
|
+
|
|
131
|
+
const waiter = waitForUsableDb(dbFile, () => {}, 2_000);
|
|
132
|
+
await Bun.sleep(100);
|
|
133
|
+
expect(existsSync(dbFile)).toBe(false);
|
|
134
|
+
|
|
135
|
+
releaseDownloadLock(lock);
|
|
136
|
+
expect(await waiter).toBe(false);
|
|
137
|
+
expect(existsSync(dbFile)).toBe(false);
|
|
138
|
+
});
|
|
118
139
|
});
|
|
119
140
|
|
|
120
141
|
// ---------------------------------------------------------------------------
|
|
@@ -123,7 +144,9 @@ describe("download lock helpers", () => {
|
|
|
123
144
|
|
|
124
145
|
describe("probeDb", () => {
|
|
125
146
|
test("returns null for a missing file", () => {
|
|
126
|
-
|
|
147
|
+
const missing = path.join(tmp, "does-not-exist.db");
|
|
148
|
+
expect(probeDb(missing)).toBeNull();
|
|
149
|
+
expect(existsSync(missing)).toBe(false);
|
|
127
150
|
});
|
|
128
151
|
|
|
129
152
|
test("returns null for a non-SQLite file", () => {
|
|
@@ -199,3 +222,23 @@ describe("probeDb", () => {
|
|
|
199
222
|
expect(probe?.releaseTag).toBeNull();
|
|
200
223
|
});
|
|
201
224
|
});
|
|
225
|
+
|
|
226
|
+
describe("cleanupStaleTempArtifacts", () => {
|
|
227
|
+
test("removes stale temp DB files and sidecars", () => {
|
|
228
|
+
const dbFile = path.join(tmp, "cleanup.db");
|
|
229
|
+
const artifacts = [
|
|
230
|
+
`${dbFile}.tmp.111`,
|
|
231
|
+
`${dbFile}.tmp.111-wal`,
|
|
232
|
+
`${dbFile}.tmp.111-shm`,
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
for (const artifact of artifacts) {
|
|
236
|
+
writeFileSync(artifact, "x");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
expect(cleanupStaleTempArtifacts(dbFile, -1)).toBe(3);
|
|
240
|
+
for (const artifact of artifacts) {
|
|
241
|
+
expect(existsSync(artifact)).toBe(false);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
package/src/setup.ts
CHANGED
|
@@ -12,11 +12,14 @@ import {
|
|
|
12
12
|
closeSync,
|
|
13
13
|
existsSync,
|
|
14
14
|
openSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
readSync,
|
|
15
17
|
renameSync,
|
|
16
18
|
statSync,
|
|
17
19
|
unlinkSync,
|
|
18
20
|
writeFileSync,
|
|
19
21
|
} from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
20
23
|
import { gunzipSync } from "bun";
|
|
21
24
|
import { detectMode, resolveBaseDir, resolveDbPath, resolveVersion, SCHEMA_VERSION } from "./paths.ts";
|
|
22
25
|
|
|
@@ -53,15 +56,31 @@ type DownloadLockHandle = {
|
|
|
53
56
|
* Opens read-write — see probeDb's note: freshly written WAL-mode files can
|
|
54
57
|
* fail to open readonly on macOS when the .shm file is missing. */
|
|
55
58
|
function dbHasData(dbPath: string): boolean {
|
|
59
|
+
return hasMinimumDbContent(probeDb(dbPath));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function looksLikeSqliteFile(dbPath: string): boolean {
|
|
56
63
|
if (!existsSync(dbPath)) return false;
|
|
64
|
+
|
|
65
|
+
let fd: number | null = null;
|
|
57
66
|
try {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
const stats = statSync(dbPath);
|
|
68
|
+
if (!stats.isFile() || stats.size < SQLITE_MAGIC.length) return false;
|
|
69
|
+
|
|
70
|
+
fd = openSync(dbPath, "r");
|
|
71
|
+
const header = Buffer.alloc(SQLITE_MAGIC.length);
|
|
72
|
+
const bytesRead = readSync(fd, header, 0, header.byteLength, 0);
|
|
73
|
+
return bytesRead === header.byteLength && header.toString("utf8") === SQLITE_MAGIC;
|
|
63
74
|
} catch {
|
|
64
75
|
return false;
|
|
76
|
+
} finally {
|
|
77
|
+
if (fd !== null) {
|
|
78
|
+
try {
|
|
79
|
+
closeSync(fd);
|
|
80
|
+
} catch {
|
|
81
|
+
// best-effort cleanup
|
|
82
|
+
}
|
|
83
|
+
}
|
|
65
84
|
}
|
|
66
85
|
}
|
|
67
86
|
|
|
@@ -77,6 +96,8 @@ export function probeDb(dbPath: string): {
|
|
|
77
96
|
commands: number;
|
|
78
97
|
releaseTag: string | null;
|
|
79
98
|
} | null {
|
|
99
|
+
if (!looksLikeSqliteFile(dbPath)) return null;
|
|
100
|
+
|
|
80
101
|
try {
|
|
81
102
|
const { default: sqlite } = require("bun:sqlite");
|
|
82
103
|
const check = new sqlite(dbPath);
|
|
@@ -102,8 +123,12 @@ export function probeDb(dbPath: string): {
|
|
|
102
123
|
}
|
|
103
124
|
}
|
|
104
125
|
|
|
105
|
-
function
|
|
106
|
-
return !!probe && probe.
|
|
126
|
+
export function hasMinimumDbContent(probe: DbProbe | null): probe is DbProbe {
|
|
127
|
+
return !!probe && probe.pages >= MIN_PAGES && probe.commands >= MIN_COMMANDS;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isUsableDbProbe(probe: DbProbe | null): probe is DbProbe {
|
|
131
|
+
return hasMinimumDbContent(probe) && probe.schemaVersion === SCHEMA_VERSION;
|
|
107
132
|
}
|
|
108
133
|
|
|
109
134
|
function lockPathFor(dbPath: string): string {
|
|
@@ -123,11 +148,11 @@ export function tryAcquireDownloadLock(dbPath: string): DownloadLockHandle | nul
|
|
|
123
148
|
const fd = openSync(lockPath, "wx");
|
|
124
149
|
writeFileSync(
|
|
125
150
|
fd,
|
|
126
|
-
JSON.stringify({
|
|
151
|
+
`${JSON.stringify({
|
|
127
152
|
pid: process.pid,
|
|
128
153
|
created_at: new Date().toISOString(),
|
|
129
154
|
db_path: dbPath,
|
|
130
|
-
})
|
|
155
|
+
})}\n`,
|
|
131
156
|
);
|
|
132
157
|
return { fd, path: lockPath };
|
|
133
158
|
} catch (e) {
|
|
@@ -171,10 +196,9 @@ export async function waitForUsableDb(
|
|
|
171
196
|
let announced = false;
|
|
172
197
|
|
|
173
198
|
while (Date.now() < deadline) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (!existsSync(lockPath)) return false;
|
|
199
|
+
if (!existsSync(lockPath)) {
|
|
200
|
+
return isUsableDbProbe(probeDb(dbPath));
|
|
201
|
+
}
|
|
178
202
|
|
|
179
203
|
if (!announced) {
|
|
180
204
|
log(` Another rosetta process is preparing ${dbPath}; waiting...`);
|
|
@@ -189,13 +213,59 @@ export async function waitForUsableDb(
|
|
|
189
213
|
return false;
|
|
190
214
|
}
|
|
191
215
|
|
|
216
|
+
function tryUnlinkDbSidecars(dbPath: string): void {
|
|
217
|
+
tryUnlink(`${dbPath}-wal`);
|
|
218
|
+
tryUnlink(`${dbPath}-shm`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function cleanupDbArtifacts(dbPath: string): void {
|
|
222
|
+
tryUnlinkDbSidecars(dbPath);
|
|
223
|
+
tryUnlink(dbPath);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function cleanupStaleTempArtifacts(dbPath: string, staleMs = DOWNLOAD_LOCK_STALE_MS): number {
|
|
227
|
+
const dir = path.dirname(dbPath);
|
|
228
|
+
const prefix = `${path.basename(dbPath)}.tmp.`;
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
let removed = 0;
|
|
231
|
+
|
|
232
|
+
let entries: string[];
|
|
233
|
+
try {
|
|
234
|
+
entries = readdirSync(dir);
|
|
235
|
+
} catch {
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const entry of entries) {
|
|
240
|
+
if (!entry.startsWith(prefix)) continue;
|
|
241
|
+
|
|
242
|
+
const fullPath = path.join(dir, entry);
|
|
243
|
+
try {
|
|
244
|
+
const ageMs = now - statSync(fullPath).mtimeMs;
|
|
245
|
+
if (ageMs <= staleMs) continue;
|
|
246
|
+
} catch {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
unlinkSync(fullPath);
|
|
252
|
+
removed++;
|
|
253
|
+
} catch {
|
|
254
|
+
// best-effort cleanup
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return removed;
|
|
259
|
+
}
|
|
260
|
+
|
|
192
261
|
function replaceDbFile(tmpPath: string, dbPath: string): void {
|
|
193
262
|
try {
|
|
194
263
|
renameSync(tmpPath, dbPath);
|
|
195
264
|
return;
|
|
196
265
|
} catch (e) {
|
|
197
266
|
const code = e instanceof Error && "code" in e ? e.code : undefined;
|
|
198
|
-
|
|
267
|
+
// Windows can return EBUSY, EPERM, or EEXIST when the destination is open.
|
|
268
|
+
if (code !== "EBUSY" && code !== "EEXIST" && code !== "EPERM") throw e;
|
|
199
269
|
}
|
|
200
270
|
|
|
201
271
|
tryUnlink(dbPath);
|
|
@@ -230,21 +300,23 @@ export function dbDownloadUrls(version: string): string[] {
|
|
|
230
300
|
export async function downloadDb(
|
|
231
301
|
dbPath: string,
|
|
232
302
|
log: (msg: string) => void = console.log,
|
|
233
|
-
) {
|
|
303
|
+
): Promise<DbProbe> {
|
|
234
304
|
let lock = tryAcquireDownloadLock(dbPath);
|
|
235
305
|
if (!lock) {
|
|
236
306
|
const reused = await waitForUsableDb(dbPath, log);
|
|
237
307
|
if (reused) {
|
|
238
308
|
const probe = probeDb(dbPath);
|
|
239
|
-
if (probe)
|
|
240
|
-
|
|
309
|
+
if (probe) {
|
|
310
|
+
log(` Reused existing database: ${formatProbeSummary(probe)}`);
|
|
311
|
+
return probe;
|
|
312
|
+
}
|
|
241
313
|
}
|
|
242
314
|
|
|
243
315
|
// Re-probe once — the lock may have been released with a healthy DB
|
|
244
316
|
const fallbackProbe = probeDb(dbPath);
|
|
245
317
|
if (isUsableDbProbe(fallbackProbe)) {
|
|
246
318
|
log(` Reused existing database: ${formatProbeSummary(fallbackProbe)}`);
|
|
247
|
-
return;
|
|
319
|
+
return fallbackProbe;
|
|
248
320
|
}
|
|
249
321
|
|
|
250
322
|
lock = tryAcquireDownloadLock(dbPath);
|
|
@@ -260,6 +332,11 @@ export async function downloadDb(
|
|
|
260
332
|
let lastError: Error | null = null;
|
|
261
333
|
|
|
262
334
|
try {
|
|
335
|
+
const cleaned = cleanupStaleTempArtifacts(dbPath);
|
|
336
|
+
if (cleaned > 0) {
|
|
337
|
+
log(` Removed ${cleaned} stale temp DB artifact${cleaned === 1 ? "" : "s"}.`);
|
|
338
|
+
}
|
|
339
|
+
|
|
263
340
|
for (let i = 0; i < urls.length; i++) {
|
|
264
341
|
const url = urls[i];
|
|
265
342
|
const isLast = i === urls.length - 1;
|
|
@@ -332,14 +409,14 @@ export async function downloadDb(
|
|
|
332
409
|
|
|
333
410
|
const probe = probeDb(tmpPath);
|
|
334
411
|
if (!probe) {
|
|
335
|
-
|
|
412
|
+
cleanupDbArtifacts(tmpPath);
|
|
336
413
|
lastError = new Error("Downloaded DB failed to open with SQLite");
|
|
337
414
|
if (isLast) throw lastError;
|
|
338
415
|
log(` ${lastError.message} — trying fallback...`);
|
|
339
416
|
continue;
|
|
340
417
|
}
|
|
341
418
|
if (probe.schemaVersion !== SCHEMA_VERSION) {
|
|
342
|
-
|
|
419
|
+
cleanupDbArtifacts(tmpPath);
|
|
343
420
|
lastError = new Error(
|
|
344
421
|
`Downloaded DB schema=${probe.schemaVersion} does not match this rosetta build (expected ${SCHEMA_VERSION}). ` +
|
|
345
422
|
`This usually means the cached package version is older than the published DB. ` +
|
|
@@ -350,7 +427,7 @@ export async function downloadDb(
|
|
|
350
427
|
continue;
|
|
351
428
|
}
|
|
352
429
|
if (probe.pages < MIN_PAGES || probe.commands < MIN_COMMANDS) {
|
|
353
|
-
|
|
430
|
+
cleanupDbArtifacts(tmpPath);
|
|
354
431
|
lastError = new Error(
|
|
355
432
|
`Downloaded DB content looks incomplete (pages=${probe.pages}, commands=${probe.commands})`,
|
|
356
433
|
);
|
|
@@ -361,25 +438,19 @@ export async function downloadDb(
|
|
|
361
438
|
|
|
362
439
|
// Validation passed — drop stale WAL/SHM and atomically swap.
|
|
363
440
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
441
|
+
tryUnlinkDbSidecars(tmpPath);
|
|
442
|
+
tryUnlinkDbSidecars(dbPath);
|
|
366
443
|
replaceDbFile(tmpPath, dbPath);
|
|
367
444
|
} catch (e) {
|
|
368
445
|
const existingProbe = probeDb(dbPath);
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
existingProbe.schemaVersion === probe.schemaVersion &&
|
|
372
|
-
existingProbe.releaseTag === probe.releaseTag &&
|
|
373
|
-
existingProbe.pages >= MIN_PAGES &&
|
|
374
|
-
existingProbe.commands >= MIN_COMMANDS
|
|
375
|
-
) {
|
|
376
|
-
tryUnlink(tmpPath);
|
|
446
|
+
if (hasMinimumDbContent(existingProbe) && existingProbe.schemaVersion === probe.schemaVersion && existingProbe.releaseTag === probe.releaseTag) {
|
|
447
|
+
cleanupDbArtifacts(tmpPath);
|
|
377
448
|
log(` Another rosetta process already installed the same database.`);
|
|
378
449
|
log(` Reused existing database: ${formatProbeSummary(existingProbe)}`);
|
|
379
|
-
return;
|
|
450
|
+
return existingProbe;
|
|
380
451
|
}
|
|
381
452
|
|
|
382
|
-
|
|
453
|
+
cleanupDbArtifacts(tmpPath);
|
|
383
454
|
throw e;
|
|
384
455
|
}
|
|
385
456
|
|
|
@@ -387,7 +458,7 @@ export async function downloadDb(
|
|
|
387
458
|
const tagInfo = probe.releaseTag ? ` (release ${probe.releaseTag})` : "";
|
|
388
459
|
log(` Wrote ${sizeMB} MB to ${dbPath}${tagInfo}`);
|
|
389
460
|
log(` Validated: schema v${probe.schemaVersion}, ${probe.pages} pages, ${probe.commands} commands.`);
|
|
390
|
-
return;
|
|
461
|
+
return probe;
|
|
391
462
|
}
|
|
392
463
|
|
|
393
464
|
throw lastError ?? new Error("Database download failed for unknown reasons");
|
|
@@ -412,17 +483,13 @@ function tryUnlink(p: string): void {
|
|
|
412
483
|
*/
|
|
413
484
|
export async function refreshDb(log: (msg: string) => void = console.log): Promise<boolean> {
|
|
414
485
|
const dbPath = resolveDbPath(import.meta.dirname);
|
|
486
|
+
let probe: DbProbe;
|
|
415
487
|
try {
|
|
416
|
-
await downloadDb(dbPath, log);
|
|
488
|
+
probe = await downloadDb(dbPath, log);
|
|
417
489
|
} catch (e) {
|
|
418
490
|
log(`✗ Refresh failed: ${e instanceof Error ? e.message : e}`);
|
|
419
491
|
return false;
|
|
420
492
|
}
|
|
421
|
-
const probe = probeDb(dbPath);
|
|
422
|
-
if (!probe) {
|
|
423
|
-
log(`✗ Post-download probe failed`);
|
|
424
|
-
return false;
|
|
425
|
-
}
|
|
426
493
|
const tagInfo = probe.releaseTag ? ` (release ${probe.releaseTag})` : "";
|
|
427
494
|
log(`✓ Database ready${tagInfo}: ${probe.pages} pages, ${probe.commands} commands, schema v${probe.schemaVersion}`);
|
|
428
495
|
return true;
|
|
@@ -431,6 +498,7 @@ export async function refreshDb(log: (msg: string) => void = console.log): Promi
|
|
|
431
498
|
export async function runSetup(force = false) {
|
|
432
499
|
const mode = detectMode(import.meta.dirname);
|
|
433
500
|
const dbPath = resolveDbPath(import.meta.dirname);
|
|
501
|
+
let downloadedProbe: DbProbe | null = null;
|
|
434
502
|
|
|
435
503
|
console.log(`rosetta ${RELEASE_VERSION}`);
|
|
436
504
|
console.log(` ${link("https://github.com/tikoci/rosetta")}`);
|
|
@@ -443,7 +511,7 @@ export async function runSetup(force = false) {
|
|
|
443
511
|
console.log(` (use --refresh or --setup --force to re-download)`);
|
|
444
512
|
} else {
|
|
445
513
|
try {
|
|
446
|
-
await downloadDb(dbPath);
|
|
514
|
+
downloadedProbe = await downloadDb(dbPath);
|
|
447
515
|
} catch (e) {
|
|
448
516
|
console.error(`✗ Database download failed: ${e instanceof Error ? e.message : e}`);
|
|
449
517
|
process.exit(1);
|
|
@@ -452,7 +520,7 @@ export async function runSetup(force = false) {
|
|
|
452
520
|
|
|
453
521
|
// ── Validate DB ──
|
|
454
522
|
console.log();
|
|
455
|
-
const probe = probeDb(dbPath);
|
|
523
|
+
const probe = downloadedProbe ?? probeDb(dbPath);
|
|
456
524
|
if (!probe) {
|
|
457
525
|
console.error(`✗ Database validation failed: cannot open ${dbPath}`);
|
|
458
526
|
const retryCmd = mode === "compiled" ? "rosetta" : mode === "package" ? "bunx @tikoci/rosetta" : "bun run src/setup.ts";
|
|
@@ -464,7 +532,7 @@ export async function runSetup(force = false) {
|
|
|
464
532
|
`✗ DB schema version is ${probe.schemaVersion}, expected ${SCHEMA_VERSION}.`,
|
|
465
533
|
);
|
|
466
534
|
console.error(
|
|
467
|
-
`
|
|
535
|
+
` Package may be out of date. Run: bunx @tikoci/rosetta@latest --refresh`,
|
|
468
536
|
);
|
|
469
537
|
process.exit(1);
|
|
470
538
|
}
|