@os-eco/overstory-cli 0.6.8 → 0.6.10
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 +19 -5
- package/agents/builder.md +6 -15
- package/agents/lead.md +4 -6
- package/agents/merger.md +5 -13
- package/agents/reviewer.md +2 -9
- package/package.json +1 -1
- package/src/agents/hooks-deployer.test.ts +232 -0
- package/src/agents/hooks-deployer.ts +54 -8
- package/src/agents/overlay.test.ts +156 -1
- package/src/agents/overlay.ts +67 -7
- package/src/commands/agents.ts +9 -6
- package/src/commands/clean.ts +2 -1
- package/src/commands/completions.test.ts +8 -20
- package/src/commands/completions.ts +7 -6
- package/src/commands/coordinator.test.ts +8 -0
- package/src/commands/coordinator.ts +11 -8
- package/src/commands/costs.test.ts +48 -38
- package/src/commands/costs.ts +48 -38
- package/src/commands/dashboard.ts +7 -7
- package/src/commands/doctor.test.ts +8 -0
- package/src/commands/doctor.ts +96 -51
- package/src/commands/ecosystem.ts +291 -0
- package/src/commands/errors.test.ts +47 -40
- package/src/commands/errors.ts +5 -4
- package/src/commands/feed.test.ts +40 -33
- package/src/commands/feed.ts +5 -4
- package/src/commands/group.ts +23 -14
- package/src/commands/hooks.ts +2 -1
- package/src/commands/init.test.ts +104 -0
- package/src/commands/init.ts +11 -7
- package/src/commands/inspect.test.ts +2 -0
- package/src/commands/inspect.ts +9 -8
- package/src/commands/logs.test.ts +5 -6
- package/src/commands/logs.ts +2 -1
- package/src/commands/mail.test.ts +11 -10
- package/src/commands/mail.ts +11 -12
- package/src/commands/merge.ts +11 -12
- package/src/commands/metrics.test.ts +15 -2
- package/src/commands/metrics.ts +3 -2
- package/src/commands/monitor.ts +5 -4
- package/src/commands/nudge.ts +2 -3
- package/src/commands/prime.test.ts +1 -6
- package/src/commands/prime.ts +2 -3
- package/src/commands/replay.test.ts +62 -55
- package/src/commands/replay.ts +3 -2
- package/src/commands/run.ts +17 -20
- package/src/commands/sling.ts +3 -2
- package/src/commands/status.test.ts +2 -1
- package/src/commands/status.ts +7 -6
- package/src/commands/stop.test.ts +2 -0
- package/src/commands/stop.ts +10 -11
- package/src/commands/supervisor.ts +7 -6
- package/src/commands/trace.test.ts +52 -44
- package/src/commands/trace.ts +5 -4
- package/src/commands/upgrade.test.ts +46 -0
- package/src/commands/upgrade.ts +259 -0
- package/src/commands/watch.ts +8 -10
- package/src/commands/worktree.test.ts +21 -15
- package/src/commands/worktree.ts +10 -4
- package/src/doctor/databases.test.ts +38 -0
- package/src/doctor/databases.ts +7 -10
- package/src/doctor/ecosystem.test.ts +307 -0
- package/src/doctor/ecosystem.ts +155 -0
- package/src/doctor/merge-queue.test.ts +98 -0
- package/src/doctor/merge-queue.ts +23 -0
- package/src/doctor/structure.test.ts +130 -1
- package/src/doctor/structure.ts +87 -1
- package/src/doctor/types.ts +5 -2
- package/src/index.ts +25 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: ov upgrade [--check] [--all] [--json]
|
|
3
|
+
*
|
|
4
|
+
* Upgrades overstory (and optionally all os-eco tools) to their latest npm versions.
|
|
5
|
+
* --check: Compare current vs latest without installing.
|
|
6
|
+
* --all: Upgrade all 4 ecosystem tools (overstory, mulch, seeds, canopy).
|
|
7
|
+
* --json: Output result as JSON envelope.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import { jsonError, jsonOutput } from "../json.ts";
|
|
12
|
+
import { muted, printError, printHint, printSuccess, printWarning } from "../logging/color.ts";
|
|
13
|
+
|
|
14
|
+
const OVERSTORY_PACKAGE = "@os-eco/overstory-cli";
|
|
15
|
+
|
|
16
|
+
const ALL_PACKAGES = [
|
|
17
|
+
"@os-eco/overstory-cli",
|
|
18
|
+
"@os-eco/mulch-cli",
|
|
19
|
+
"@os-eco/seeds-cli",
|
|
20
|
+
"@os-eco/canopy-cli",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export interface UpgradeOptions {
|
|
24
|
+
check?: boolean;
|
|
25
|
+
all?: boolean;
|
|
26
|
+
json?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function getCurrentVersion(): Promise<string> {
|
|
30
|
+
const pkgPath = new URL("../../package.json", import.meta.url);
|
|
31
|
+
const pkg = JSON.parse(await Bun.file(pkgPath).text()) as { version: string };
|
|
32
|
+
return pkg.version;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fetchLatestVersion(packageName: string): Promise<string> {
|
|
36
|
+
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Failed to fetch npm registry for ${packageName}: ${res.status} ${res.statusText}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const data = (await res.json()) as { version: string };
|
|
43
|
+
return data.version;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function runInstall(packageName: string): Promise<number> {
|
|
47
|
+
const proc = Bun.spawn(["bun", "install", "-g", `${packageName}@latest`], {
|
|
48
|
+
stdout: "inherit",
|
|
49
|
+
stderr: "inherit",
|
|
50
|
+
});
|
|
51
|
+
return proc.exited;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface PackageResult {
|
|
55
|
+
package: string;
|
|
56
|
+
current: string;
|
|
57
|
+
latest: string;
|
|
58
|
+
upToDate: boolean;
|
|
59
|
+
updated: boolean;
|
|
60
|
+
error?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function upgradePackage(packageName: string): Promise<PackageResult> {
|
|
64
|
+
let latest: string;
|
|
65
|
+
try {
|
|
66
|
+
latest = await fetchLatestVersion(packageName);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
69
|
+
return {
|
|
70
|
+
package: packageName,
|
|
71
|
+
current: "unknown",
|
|
72
|
+
latest: "unknown",
|
|
73
|
+
upToDate: false,
|
|
74
|
+
updated: false,
|
|
75
|
+
error,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const exitCode = await runInstall(packageName);
|
|
80
|
+
if (exitCode !== 0) {
|
|
81
|
+
return {
|
|
82
|
+
package: packageName,
|
|
83
|
+
current: "unknown",
|
|
84
|
+
latest,
|
|
85
|
+
upToDate: false,
|
|
86
|
+
updated: false,
|
|
87
|
+
error: `bun install failed with exit code ${exitCode}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { package: packageName, current: "unknown", latest, upToDate: false, updated: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function executeUpgradeSingle(opts: UpgradeOptions): Promise<void> {
|
|
94
|
+
const json = opts.json ?? false;
|
|
95
|
+
const checkOnly = opts.check ?? false;
|
|
96
|
+
|
|
97
|
+
let current: string;
|
|
98
|
+
let latest: string;
|
|
99
|
+
try {
|
|
100
|
+
[current, latest] = await Promise.all([
|
|
101
|
+
getCurrentVersion(),
|
|
102
|
+
fetchLatestVersion(OVERSTORY_PACKAGE),
|
|
103
|
+
]);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
106
|
+
if (json) {
|
|
107
|
+
jsonError("upgrade", msg);
|
|
108
|
+
} else {
|
|
109
|
+
printError("Failed to check for updates", msg);
|
|
110
|
+
}
|
|
111
|
+
process.exitCode = 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const upToDate = current === latest;
|
|
116
|
+
|
|
117
|
+
if (checkOnly) {
|
|
118
|
+
if (json) {
|
|
119
|
+
jsonOutput("upgrade", { current, latest, upToDate });
|
|
120
|
+
} else if (upToDate) {
|
|
121
|
+
printSuccess("Already up to date", current);
|
|
122
|
+
} else {
|
|
123
|
+
printWarning(`Update available: ${current} → ${latest}`);
|
|
124
|
+
printHint("Run 'ov upgrade' to install the latest version");
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (upToDate) {
|
|
131
|
+
if (json) {
|
|
132
|
+
jsonOutput("upgrade", { current, latest, upToDate: true, updated: false });
|
|
133
|
+
} else {
|
|
134
|
+
printSuccess("Already up to date", current);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!json) {
|
|
140
|
+
process.stdout.write(
|
|
141
|
+
`${muted(`Upgrading ${OVERSTORY_PACKAGE} from ${current} to ${latest}...`)}\n`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const exitCode = await runInstall(OVERSTORY_PACKAGE);
|
|
146
|
+
|
|
147
|
+
if (exitCode !== 0) {
|
|
148
|
+
if (json) {
|
|
149
|
+
jsonError("upgrade", `bun install failed with exit code ${exitCode}`);
|
|
150
|
+
} else {
|
|
151
|
+
printError("Upgrade failed", `bun install exited with code ${exitCode}`);
|
|
152
|
+
}
|
|
153
|
+
process.exitCode = 1;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (json) {
|
|
158
|
+
jsonOutput("upgrade", { current, latest, upToDate: false, updated: true });
|
|
159
|
+
} else {
|
|
160
|
+
printSuccess("Upgraded to", latest);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function executeUpgradeAll(opts: UpgradeOptions): Promise<void> {
|
|
165
|
+
const json = opts.json ?? false;
|
|
166
|
+
const checkOnly = opts.check ?? false;
|
|
167
|
+
|
|
168
|
+
// Fetch all latest versions in parallel
|
|
169
|
+
const latestResults = await Promise.allSettled(
|
|
170
|
+
ALL_PACKAGES.map((pkg) => fetchLatestVersion(pkg)),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (checkOnly) {
|
|
174
|
+
// For --check --all, we just report status without installing
|
|
175
|
+
const results: Array<{ package: string; latest: string; error?: string }> = [];
|
|
176
|
+
let anyOutdated = false;
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < ALL_PACKAGES.length; i++) {
|
|
179
|
+
const pkg = ALL_PACKAGES[i] as string;
|
|
180
|
+
const result = latestResults[i];
|
|
181
|
+
if (result === undefined || result.status === "rejected") {
|
|
182
|
+
const error =
|
|
183
|
+
result?.status === "rejected"
|
|
184
|
+
? result.reason instanceof Error
|
|
185
|
+
? result.reason.message
|
|
186
|
+
: String(result.reason)
|
|
187
|
+
: "unknown error";
|
|
188
|
+
results.push({ package: pkg, latest: "unknown", error });
|
|
189
|
+
anyOutdated = true;
|
|
190
|
+
} else {
|
|
191
|
+
results.push({ package: pkg, latest: result.value });
|
|
192
|
+
// We can't easily get current versions for all tools without spawning them
|
|
193
|
+
// so for --all --check we just report latest versions available
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (json) {
|
|
198
|
+
jsonOutput("upgrade", { packages: results });
|
|
199
|
+
} else {
|
|
200
|
+
for (const r of results) {
|
|
201
|
+
if (r.error) {
|
|
202
|
+
printError(`${r.package}`, r.error);
|
|
203
|
+
} else {
|
|
204
|
+
process.stdout.write(` ${r.package} → ${r.latest}\n`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (anyOutdated) process.exitCode = 1;
|
|
208
|
+
}
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Install all packages
|
|
213
|
+
if (!json) {
|
|
214
|
+
process.stdout.write(`${muted("Upgrading all os-eco tools to latest...")}\n`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const results: PackageResult[] = await Promise.all(
|
|
218
|
+
ALL_PACKAGES.map((pkg) => upgradePackage(pkg)),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const anyError = results.some((r) => r.error !== undefined);
|
|
222
|
+
|
|
223
|
+
if (json) {
|
|
224
|
+
if (anyError) {
|
|
225
|
+
jsonError("upgrade", `One or more packages failed to upgrade`);
|
|
226
|
+
} else {
|
|
227
|
+
jsonOutput("upgrade", { packages: results, updated: true });
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
for (const r of results) {
|
|
231
|
+
if (r.error) {
|
|
232
|
+
printError(`Failed to upgrade ${r.package}`, r.error);
|
|
233
|
+
} else {
|
|
234
|
+
printSuccess(`Upgraded ${r.package} to`, r.latest);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (anyError) process.exitCode = 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function executeUpgrade(opts: UpgradeOptions): Promise<void> {
|
|
243
|
+
if (opts.all) {
|
|
244
|
+
await executeUpgradeAll(opts);
|
|
245
|
+
} else {
|
|
246
|
+
await executeUpgradeSingle(opts);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function createUpgradeCommand(): Command {
|
|
251
|
+
return new Command("upgrade")
|
|
252
|
+
.description("Upgrade overstory to the latest version from npm")
|
|
253
|
+
.option("--check", "Check for updates without installing")
|
|
254
|
+
.option("--all", "Upgrade all os-eco ecosystem tools")
|
|
255
|
+
.option("--json", "Output as JSON")
|
|
256
|
+
.action(async (opts: UpgradeOptions) => {
|
|
257
|
+
await executeUpgrade(opts);
|
|
258
|
+
});
|
|
259
|
+
}
|
package/src/commands/watch.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import { loadConfig } from "../config.ts";
|
|
12
12
|
import { OverstoryError } from "../errors.ts";
|
|
13
|
+
import { printError, printHint, printSuccess } from "../logging/color.ts";
|
|
13
14
|
import type { HealthCheck } from "../types.ts";
|
|
14
15
|
import { startDaemon } from "../watchdog/daemon.ts";
|
|
15
16
|
import { isProcessRunning } from "../watchdog/health.ts";
|
|
@@ -130,9 +131,8 @@ async function runWatch(opts: { interval?: string; background?: boolean }): Prom
|
|
|
130
131
|
// Check if a watchdog is already running
|
|
131
132
|
const existingPid = await readPidFile(pidFilePath);
|
|
132
133
|
if (existingPid !== null && isProcessRunning(existingPid)) {
|
|
133
|
-
|
|
134
|
-
`
|
|
135
|
-
`Kill it first or remove ${pidFilePath}\n`,
|
|
134
|
+
printError(
|
|
135
|
+
`Watchdog already running (PID: ${existingPid}). Kill it first or remove ${pidFilePath}`,
|
|
136
136
|
);
|
|
137
137
|
process.exitCode = 1;
|
|
138
138
|
return;
|
|
@@ -168,16 +168,14 @@ async function runWatch(opts: { interval?: string; background?: boolean }): Prom
|
|
|
168
168
|
// Write PID file for later cleanup
|
|
169
169
|
await writePidFile(pidFilePath, childPid);
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
);
|
|
174
|
-
process.stdout.write(`PID file: ${pidFilePath}\n`);
|
|
171
|
+
printSuccess("Watchdog started in background", `PID: ${childPid}, interval: ${intervalMs}ms`);
|
|
172
|
+
printHint(`PID file: ${pidFilePath}`);
|
|
175
173
|
return;
|
|
176
174
|
}
|
|
177
175
|
|
|
178
176
|
// Foreground mode: show real-time health checks
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
printSuccess("Watchdog running", `interval: ${intervalMs}ms`);
|
|
178
|
+
printHint("Press Ctrl+C to stop.");
|
|
181
179
|
|
|
182
180
|
// Write PID file so `--background` check and external tools can find us
|
|
183
181
|
await writePidFile(pidFilePath, process.pid);
|
|
@@ -200,7 +198,7 @@ async function runWatch(opts: { interval?: string; background?: boolean }): Prom
|
|
|
200
198
|
stop();
|
|
201
199
|
// Clean up PID file on graceful shutdown
|
|
202
200
|
removePidFile(pidFilePath).finally(() => {
|
|
203
|
-
|
|
201
|
+
printSuccess("Watchdog stopped.");
|
|
204
202
|
process.exit(0);
|
|
205
203
|
});
|
|
206
204
|
});
|
|
@@ -220,21 +220,27 @@ describe("worktreeCommand", () => {
|
|
|
220
220
|
await worktreeCommand(["list", "--json"]);
|
|
221
221
|
const out = output();
|
|
222
222
|
|
|
223
|
-
const parsed = JSON.parse(out.trim()) as
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
expect(parsed
|
|
237
|
-
expect(parsed
|
|
223
|
+
const parsed = JSON.parse(out.trim()) as {
|
|
224
|
+
success: boolean;
|
|
225
|
+
command: string;
|
|
226
|
+
worktrees: Array<{
|
|
227
|
+
path: string;
|
|
228
|
+
branch: string;
|
|
229
|
+
head: string;
|
|
230
|
+
agentName: string | null;
|
|
231
|
+
state: string | null;
|
|
232
|
+
taskId: string | null;
|
|
233
|
+
}>;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
expect(parsed.success).toBe(true);
|
|
237
|
+
expect(parsed.command).toBe("worktree list");
|
|
238
|
+
expect(parsed.worktrees).toHaveLength(1);
|
|
239
|
+
expect(parsed.worktrees[0]?.path).toBe(worktreePath);
|
|
240
|
+
expect(parsed.worktrees[0]?.branch).toBe("overstory/test-agent/task-1");
|
|
241
|
+
expect(parsed.worktrees[0]?.agentName).toBe("test-agent");
|
|
242
|
+
expect(parsed.worktrees[0]?.state).toBe("working");
|
|
243
|
+
expect(parsed.worktrees[0]?.taskId).toBe("task-1");
|
|
238
244
|
});
|
|
239
245
|
|
|
240
246
|
test("worktrees without sessions show unknown state", async () => {
|
package/src/commands/worktree.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import { loadConfig } from "../config.ts";
|
|
12
12
|
import { ValidationError } from "../errors.ts";
|
|
13
|
+
import { jsonOutput } from "../json.ts";
|
|
13
14
|
import { printHint, printSuccess, printWarning } from "../logging/color.ts";
|
|
14
15
|
import { createMailStore } from "../mail/store.ts";
|
|
15
16
|
import { openSessionStore } from "../sessions/compat.ts";
|
|
@@ -50,7 +51,7 @@ async function handleList(root: string, json: boolean): Promise<void> {
|
|
|
50
51
|
taskId: session?.taskId ?? null,
|
|
51
52
|
};
|
|
52
53
|
});
|
|
53
|
-
|
|
54
|
+
jsonOutput("worktree list", { worktrees: entries });
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
57
|
|
|
@@ -238,9 +239,14 @@ async function handleClean(
|
|
|
238
239
|
}
|
|
239
240
|
|
|
240
241
|
if (json) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
jsonOutput("worktree clean", {
|
|
243
|
+
cleaned,
|
|
244
|
+
failed,
|
|
245
|
+
skipped,
|
|
246
|
+
pruned: pruneCount,
|
|
247
|
+
mailPurged,
|
|
248
|
+
seedsPreserved,
|
|
249
|
+
});
|
|
244
250
|
} else if (
|
|
245
251
|
cleaned.length === 0 &&
|
|
246
252
|
pruneCount === 0 &&
|
|
@@ -307,4 +307,42 @@ describe("checkDatabases", () => {
|
|
|
307
307
|
expect(integrityCheck?.status).toBe("fail");
|
|
308
308
|
expect(integrityCheck?.message).toContain("Failed to open or validate");
|
|
309
309
|
});
|
|
310
|
+
|
|
311
|
+
test("fix() enables WAL mode on database", () => {
|
|
312
|
+
// Create mail.db without WAL mode
|
|
313
|
+
const mailDb = new Database(join(tempDir, "mail.db"));
|
|
314
|
+
mailDb.exec(`
|
|
315
|
+
CREATE TABLE messages (
|
|
316
|
+
id TEXT PRIMARY KEY,
|
|
317
|
+
from_agent TEXT NOT NULL,
|
|
318
|
+
to_agent TEXT NOT NULL,
|
|
319
|
+
subject TEXT NOT NULL,
|
|
320
|
+
body TEXT NOT NULL,
|
|
321
|
+
type TEXT NOT NULL DEFAULT 'status',
|
|
322
|
+
priority TEXT NOT NULL DEFAULT 'normal',
|
|
323
|
+
thread_id TEXT,
|
|
324
|
+
payload TEXT,
|
|
325
|
+
read INTEGER NOT NULL DEFAULT 0,
|
|
326
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
327
|
+
)
|
|
328
|
+
`);
|
|
329
|
+
mailDb.close();
|
|
330
|
+
|
|
331
|
+
const checks = checkDatabases(mockConfig, tempDir) as DoctorCheck[];
|
|
332
|
+
|
|
333
|
+
const walCheck = checks.find((c) => c?.name === "mail.db WAL mode");
|
|
334
|
+
expect(walCheck?.status).toBe("warn");
|
|
335
|
+
expect(walCheck?.fix).toBeDefined();
|
|
336
|
+
|
|
337
|
+
const actions = walCheck?.fix?.();
|
|
338
|
+
expect(Array.isArray(actions)).toBe(true);
|
|
339
|
+
expect((actions as string[]).some((a) => a.includes("WAL mode"))).toBe(true);
|
|
340
|
+
expect((actions as string[]).some((a) => a.includes("mail.db"))).toBe(true);
|
|
341
|
+
|
|
342
|
+
// Verify WAL mode is now enabled
|
|
343
|
+
const verifyDb = new Database(join(tempDir, "mail.db"));
|
|
344
|
+
const journalMode = verifyDb.prepare<{ journal_mode: string }, []>("PRAGMA journal_mode").get();
|
|
345
|
+
verifyDb.close();
|
|
346
|
+
expect(journalMode?.journal_mode?.toLowerCase()).toBe("wal");
|
|
347
|
+
});
|
|
310
348
|
});
|
package/src/doctor/databases.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import type { DoctorCheck, DoctorCheckFn } from "./types.ts";
|
|
4
5
|
|
|
@@ -187,6 +188,12 @@ export const checkDatabases: DoctorCheckFn = (_config, overstoryDir): DoctorChec
|
|
|
187
188
|
message: `Database ${dbSpec.name} is not using WAL mode`,
|
|
188
189
|
details: ["WAL mode improves concurrent access performance"],
|
|
189
190
|
fixable: true,
|
|
191
|
+
fix: () => {
|
|
192
|
+
const fixDb = new Database(dbPath);
|
|
193
|
+
fixDb.exec("PRAGMA journal_mode=WAL");
|
|
194
|
+
fixDb.close();
|
|
195
|
+
return [`Enabled WAL mode on ${dbSpec.name}`];
|
|
196
|
+
},
|
|
190
197
|
});
|
|
191
198
|
} else {
|
|
192
199
|
checks.push({
|
|
@@ -222,13 +229,3 @@ export const checkDatabases: DoctorCheckFn = (_config, overstoryDir): DoctorChec
|
|
|
222
229
|
|
|
223
230
|
return checks;
|
|
224
231
|
};
|
|
225
|
-
|
|
226
|
-
/** Helper to check if file exists (synchronous). */
|
|
227
|
-
function existsSync(path: string): boolean {
|
|
228
|
-
try {
|
|
229
|
-
const { existsSync } = require("node:fs");
|
|
230
|
-
return existsSync(path);
|
|
231
|
-
} catch {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
}
|