@co0ontty/wand 1.7.0 → 1.10.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/dist/git-worktree.d.ts +17 -1
- package/dist/git-worktree.js +244 -0
- package/dist/resume-policy.d.ts +0 -77
- package/dist/resume-policy.js +0 -162
- package/dist/server-session-routes.js +153 -0
- package/dist/server.js +27 -1
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +205 -141
- package/dist/types.d.ts +34 -0
- package/dist/web-ui/content/scripts.js +850 -174
- package/dist/web-ui/content/styles.css +854 -144
- package/dist/web-ui/scripts.js +3 -6
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { parseMessages } from "./message-parser.js";
|
|
3
3
|
import { SessionInputError } from "./process-manager.js";
|
|
4
|
+
import { checkSessionWorktreeMergeability, cleanupSessionWorktree, getWorktreeMergeErrorCode, mergeSessionWorktree, WorktreeMergeError } from "./git-worktree.js";
|
|
4
5
|
export function getErrorMessage(error, fallback) {
|
|
5
6
|
return error instanceof Error ? error.message : fallback;
|
|
6
7
|
}
|
|
@@ -70,6 +71,66 @@ function listAllSessions(processes, structured) {
|
|
|
70
71
|
return [...structured.list(), ...processes.list()]
|
|
71
72
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
72
73
|
}
|
|
74
|
+
function requireWorktreeSession(snapshot) {
|
|
75
|
+
if (!snapshot) {
|
|
76
|
+
throw new Error("未找到该会话。");
|
|
77
|
+
}
|
|
78
|
+
if (!snapshot.worktreeEnabled || !snapshot.worktree?.branch || !snapshot.worktree?.path) {
|
|
79
|
+
throw new Error("该会话未启用 worktree 模式。 ");
|
|
80
|
+
}
|
|
81
|
+
return snapshot;
|
|
82
|
+
}
|
|
83
|
+
function buildWorktreeMergeInfo(current, status, info) {
|
|
84
|
+
return {
|
|
85
|
+
...(current.worktreeMergeInfo ?? null),
|
|
86
|
+
...(info ?? null),
|
|
87
|
+
lastError: info?.lastError,
|
|
88
|
+
conflict: info?.conflict,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function saveWorktreeMergeState(storage, current, status, info) {
|
|
92
|
+
const mergedInfo = buildWorktreeMergeInfo(current, status, info);
|
|
93
|
+
const updated = {
|
|
94
|
+
...current,
|
|
95
|
+
worktreeMergeStatus: status,
|
|
96
|
+
worktreeMergeInfo: mergedInfo,
|
|
97
|
+
};
|
|
98
|
+
storage.saveSessionMetadata(updated);
|
|
99
|
+
return updated;
|
|
100
|
+
}
|
|
101
|
+
function getWorktreeMergeResponseStatus(error) {
|
|
102
|
+
const code = getWorktreeMergeErrorCode(error);
|
|
103
|
+
if (!code) {
|
|
104
|
+
return 400;
|
|
105
|
+
}
|
|
106
|
+
if (code === "WORKTREE_MERGE_CONFLICT") {
|
|
107
|
+
return 409;
|
|
108
|
+
}
|
|
109
|
+
return 400;
|
|
110
|
+
}
|
|
111
|
+
function getWorktreeMergePayload(error, fallback) {
|
|
112
|
+
if (error instanceof WorktreeMergeError) {
|
|
113
|
+
return {
|
|
114
|
+
error: error.message,
|
|
115
|
+
errorCode: error.code,
|
|
116
|
+
result: error.result ?? null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
error: getErrorMessage(error, fallback),
|
|
121
|
+
errorCode: getWorktreeMergeErrorCode(error) ?? null,
|
|
122
|
+
result: null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function getLatestSessionSnapshot(processes, structured, storage, id) {
|
|
126
|
+
return getSessionById(processes, structured, id) ?? storage.getSession(id);
|
|
127
|
+
}
|
|
128
|
+
function canMergeSession(snapshot) {
|
|
129
|
+
return Boolean(snapshot.worktreeEnabled && snapshot.worktree?.branch && snapshot.worktree?.path);
|
|
130
|
+
}
|
|
131
|
+
function isMergeActionAllowed(snapshot) {
|
|
132
|
+
return snapshot.status !== "running";
|
|
133
|
+
}
|
|
73
134
|
export function registerSessionRoutes(app, processes, structured, storage, defaultMode) {
|
|
74
135
|
app.get("/api/sessions", (_req, res) => {
|
|
75
136
|
const all = listAllSessions(processes, structured);
|
|
@@ -117,6 +178,98 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
117
178
|
res.status(400).json({ error: getErrorMessage(error, "无法发送结构化消息。") });
|
|
118
179
|
}
|
|
119
180
|
});
|
|
181
|
+
app.post("/api/sessions/:id/worktree/merge/check", (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const current = requireWorktreeSession(getLatestSessionSnapshot(processes, structured, storage, req.params.id));
|
|
184
|
+
if (!isMergeActionAllowed(current)) {
|
|
185
|
+
res.status(409).json({ error: "会话仍在运行,请结束后再合并。", errorCode: "SESSION_STILL_RUNNING" });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const checking = saveWorktreeMergeState(storage, current, "checking", {
|
|
189
|
+
...(current.worktreeMergeInfo ?? null),
|
|
190
|
+
targetBranch: current.worktreeMergeInfo?.targetBranch,
|
|
191
|
+
lastError: undefined,
|
|
192
|
+
conflict: false,
|
|
193
|
+
});
|
|
194
|
+
const result = checkSessionWorktreeMergeability({
|
|
195
|
+
worktree: checking.worktree,
|
|
196
|
+
targetBranch: current.worktreeMergeInfo?.targetBranch,
|
|
197
|
+
});
|
|
198
|
+
const nextStatus = result.ok ? "ready" : "failed";
|
|
199
|
+
const updated = saveWorktreeMergeState(storage, checking, nextStatus, {
|
|
200
|
+
targetBranch: result.targetBranch,
|
|
201
|
+
conflict: result.hasConflicts,
|
|
202
|
+
lastError: result.ok ? undefined : result.reason,
|
|
203
|
+
});
|
|
204
|
+
res.json({ session: updated, result });
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法检查 worktree 合并状态。"));
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
app.post("/api/sessions/:id/worktree/merge", express.json(), (req, res) => {
|
|
211
|
+
try {
|
|
212
|
+
const current = requireWorktreeSession(getLatestSessionSnapshot(processes, structured, storage, req.params.id));
|
|
213
|
+
if (!isMergeActionAllowed(current)) {
|
|
214
|
+
res.status(409).json({ error: "会话仍在运行,请结束后再合并。", errorCode: "SESSION_STILL_RUNNING" });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const merging = saveWorktreeMergeState(storage, current, "merging", {
|
|
218
|
+
...(current.worktreeMergeInfo ?? null),
|
|
219
|
+
lastError: undefined,
|
|
220
|
+
conflict: false,
|
|
221
|
+
});
|
|
222
|
+
const result = mergeSessionWorktree({
|
|
223
|
+
worktree: merging.worktree,
|
|
224
|
+
targetBranch: current.worktreeMergeInfo?.targetBranch,
|
|
225
|
+
});
|
|
226
|
+
const updated = saveWorktreeMergeState(storage, merging, "merged", {
|
|
227
|
+
targetBranch: result.targetBranch,
|
|
228
|
+
mergedAt: result.mergedAt,
|
|
229
|
+
mergeCommit: result.mergeCommit,
|
|
230
|
+
cleanupDone: result.cleanupDone,
|
|
231
|
+
lastError: undefined,
|
|
232
|
+
conflict: false,
|
|
233
|
+
});
|
|
234
|
+
res.json({ session: updated, result });
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const current = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
|
|
238
|
+
if (current && canMergeSession(current)) {
|
|
239
|
+
const payload = getWorktreeMergePayload(error, "无法合并 worktree。");
|
|
240
|
+
saveWorktreeMergeState(storage, current, "failed", {
|
|
241
|
+
...(current.worktreeMergeInfo ?? null),
|
|
242
|
+
targetBranch: payload.result?.targetBranch ?? current.worktreeMergeInfo?.targetBranch,
|
|
243
|
+
mergedAt: payload.result?.mergedAt,
|
|
244
|
+
mergeCommit: payload.result?.mergeCommit,
|
|
245
|
+
cleanupDone: payload.result?.cleanupDone,
|
|
246
|
+
lastError: payload.error,
|
|
247
|
+
conflict: payload.result?.conflict === true,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法合并 worktree。"));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
app.post("/api/sessions/:id/worktree/cleanup", (req, res) => {
|
|
254
|
+
try {
|
|
255
|
+
const current = requireWorktreeSession(getLatestSessionSnapshot(processes, structured, storage, req.params.id));
|
|
256
|
+
if (current.worktreeMergeStatus !== "merged" || current.worktreeMergeInfo?.cleanupDone !== false) {
|
|
257
|
+
res.status(400).json({ error: "当前 worktree 无需补偿清理。", errorCode: "WORKTREE_CLEANUP_NOT_NEEDED" });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
cleanupSessionWorktree({ worktree: current.worktree });
|
|
261
|
+
const updated = saveWorktreeMergeState(storage, current, "merged", {
|
|
262
|
+
...(current.worktreeMergeInfo ?? null),
|
|
263
|
+
cleanupDone: true,
|
|
264
|
+
lastError: undefined,
|
|
265
|
+
conflict: false,
|
|
266
|
+
});
|
|
267
|
+
res.json({ session: updated, ok: true });
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法清理 worktree。"));
|
|
271
|
+
}
|
|
272
|
+
});
|
|
120
273
|
app.post("/api/sessions/batch-delete", express.json(), (req, res) => {
|
|
121
274
|
const sessionIds = Array.isArray(req.body?.sessionIds)
|
|
122
275
|
? req.body.sessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readdir, readFile, stat } from "node:fs/promises";
|
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createServer as createHttpServer } from "node:http";
|
|
5
5
|
import { createServer as createHttpsServer } from "node:https";
|
|
6
|
-
import { exec } from "node:child_process";
|
|
6
|
+
import { exec, spawn } from "node:child_process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import process from "node:process";
|
|
@@ -478,6 +478,8 @@ export async function startServer(config, configPath) {
|
|
|
478
478
|
repoUrl: PKG_REPO_URL,
|
|
479
479
|
config: safeConfig,
|
|
480
480
|
hasCert: existsSync(certPaths.keyPath) && existsSync(certPaths.certPath),
|
|
481
|
+
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
482
|
+
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
481
483
|
});
|
|
482
484
|
});
|
|
483
485
|
app.post("/api/settings/config", async (req, res) => {
|
|
@@ -885,6 +887,30 @@ export async function startServer(config, configPath) {
|
|
|
885
887
|
structuredSessions.setEventEmitter((event) => {
|
|
886
888
|
wsManager.emitEvent(event);
|
|
887
889
|
});
|
|
890
|
+
// ── Restart endpoint (needs server + wss in scope) ──
|
|
891
|
+
app.post("/api/restart", async (_req, res) => {
|
|
892
|
+
res.json({ ok: true, message: "服务正在重启..." });
|
|
893
|
+
wsManager.emitEvent({
|
|
894
|
+
type: "notification",
|
|
895
|
+
sessionId: "__system__",
|
|
896
|
+
data: { kind: "restart" },
|
|
897
|
+
});
|
|
898
|
+
setTimeout(() => {
|
|
899
|
+
// Close all WebSocket connections first
|
|
900
|
+
wss.clients.forEach((client) => client.close());
|
|
901
|
+
server.close(() => {
|
|
902
|
+
spawn(process.execPath, process.argv.slice(1), {
|
|
903
|
+
detached: true,
|
|
904
|
+
stdio: "inherit",
|
|
905
|
+
cwd: process.cwd(),
|
|
906
|
+
env: process.env,
|
|
907
|
+
}).unref();
|
|
908
|
+
process.exit(0);
|
|
909
|
+
});
|
|
910
|
+
// Force exit after 5s if graceful shutdown stalls
|
|
911
|
+
setTimeout(() => process.exit(0), 5000);
|
|
912
|
+
}, 600);
|
|
913
|
+
});
|
|
888
914
|
await new Promise((resolve, reject) => {
|
|
889
915
|
server.listen(config.port, config.host, () => {
|
|
890
916
|
const listenAddr = config.host === "0.0.0.0" ? "0.0.0.0 (所有接口)" : config.host;
|
package/dist/storage.d.ts
CHANGED
|
@@ -37,5 +37,6 @@ export declare class WandStorage {
|
|
|
37
37
|
getLatestSessionByClaudeSessionId(claudeSessionId: string): SessionSnapshot | null;
|
|
38
38
|
loadSessions(): SessionSnapshot[];
|
|
39
39
|
private mapSessionRow;
|
|
40
|
+
updateSessionWorktreeMergeState(id: string, status: SessionSnapshot["worktreeMergeStatus"], info: SessionSnapshot["worktreeMergeInfo"]): SessionSnapshot | null;
|
|
40
41
|
deleteSession(id: string): void;
|
|
41
42
|
}
|
package/dist/storage.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
|
-
function
|
|
5
|
-
if (!raw)
|
|
4
|
+
function safeJsonParse(raw) {
|
|
5
|
+
if (!raw)
|
|
6
6
|
return undefined;
|
|
7
|
-
}
|
|
8
7
|
try {
|
|
9
8
|
return JSON.parse(raw);
|
|
10
9
|
}
|
|
@@ -13,27 +12,8 @@ function parseStoredMessages(raw) {
|
|
|
13
12
|
}
|
|
14
13
|
}
|
|
15
14
|
function parseQueuedMessages(raw) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
try {
|
|
20
|
-
const parsed = JSON.parse(raw);
|
|
21
|
-
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : undefined;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function parseStructuredState(raw) {
|
|
28
|
-
if (!raw) {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(raw);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
return undefined;
|
|
36
|
-
}
|
|
15
|
+
const parsed = safeJsonParse(raw);
|
|
16
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : undefined;
|
|
37
17
|
}
|
|
38
18
|
function inferSessionProvider(row) {
|
|
39
19
|
if (row.provider === "claude" || row.provider === "codex") {
|
|
@@ -45,23 +25,164 @@ function inferSessionProvider(row) {
|
|
|
45
25
|
return /^claude\b/.test(row.command.trim()) ? "claude" : undefined;
|
|
46
26
|
}
|
|
47
27
|
function parseWorktreeInfo(raw) {
|
|
48
|
-
|
|
49
|
-
|
|
28
|
+
const parsed = safeJsonParse(raw);
|
|
29
|
+
if (parsed && typeof parsed.branch === "string" && typeof parsed.path === "string") {
|
|
30
|
+
return { branch: parsed.branch, path: parsed.path };
|
|
50
31
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function parseWorktreeMergeInfo(raw) {
|
|
35
|
+
return safeJsonParse(raw);
|
|
36
|
+
}
|
|
37
|
+
function serializeWorktreeMergeInfo(info) {
|
|
38
|
+
return info ? JSON.stringify(info) : null;
|
|
39
|
+
}
|
|
40
|
+
function serializeWorktreeInfo(info) {
|
|
41
|
+
return info ? JSON.stringify(info) : null;
|
|
42
|
+
}
|
|
43
|
+
function normalizeWorktreeMergeStatus(raw) {
|
|
44
|
+
if (raw === "ready" || raw === "checking" || raw === "merging" || raw === "merged" || raw === "failed") {
|
|
45
|
+
return raw;
|
|
62
46
|
}
|
|
63
47
|
return undefined;
|
|
64
48
|
}
|
|
49
|
+
function mapWorktreeMergeFields(row) {
|
|
50
|
+
return {
|
|
51
|
+
worktreeMergeStatus: normalizeWorktreeMergeStatus(row.worktree_merge_status),
|
|
52
|
+
worktreeMergeInfo: parseWorktreeMergeInfo(row.worktree_merge_info) ?? null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function sessionSelectFields() {
|
|
56
|
+
return `id, provider, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, archived, archived_at, claude_session_id, messages, queued_messages, structured_state
|
|
57
|
+
, resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info`;
|
|
58
|
+
}
|
|
59
|
+
function sessionPersistFields() {
|
|
60
|
+
return `id, command, cwd, mode, status, exit_code, started_at, ended_at, output
|
|
61
|
+
, archived, archived_at, claude_session_id, provider, session_kind, runner, messages, queued_messages, structured_state
|
|
62
|
+
, resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info`;
|
|
63
|
+
}
|
|
64
|
+
function sessionPersistAssignments() {
|
|
65
|
+
return `command = excluded.command,
|
|
66
|
+
cwd = excluded.cwd,
|
|
67
|
+
mode = excluded.mode,
|
|
68
|
+
status = excluded.status,
|
|
69
|
+
exit_code = excluded.exit_code,
|
|
70
|
+
started_at = excluded.started_at,
|
|
71
|
+
ended_at = excluded.ended_at,
|
|
72
|
+
output = excluded.output,
|
|
73
|
+
archived = excluded.archived,
|
|
74
|
+
archived_at = excluded.archived_at,
|
|
75
|
+
claude_session_id = excluded.claude_session_id,
|
|
76
|
+
provider = excluded.provider,
|
|
77
|
+
session_kind = excluded.session_kind,
|
|
78
|
+
runner = excluded.runner,
|
|
79
|
+
messages = excluded.messages,
|
|
80
|
+
queued_messages = excluded.queued_messages,
|
|
81
|
+
structured_state = excluded.structured_state,
|
|
82
|
+
resumed_from_session_id = excluded.resumed_from_session_id,
|
|
83
|
+
resumed_to_session_id = excluded.resumed_to_session_id,
|
|
84
|
+
auto_recovered = excluded.auto_recovered,
|
|
85
|
+
worktree_enabled = excluded.worktree_enabled,
|
|
86
|
+
worktree_info = excluded.worktree_info,
|
|
87
|
+
worktree_merge_status = excluded.worktree_merge_status,
|
|
88
|
+
worktree_merge_info = excluded.worktree_merge_info`;
|
|
89
|
+
}
|
|
90
|
+
function sessionMetadataAssignments() {
|
|
91
|
+
return `command = ?, cwd = ?, mode = ?, status = ?, exit_code = ?,
|
|
92
|
+
started_at = ?, ended_at = ?, output = ?,
|
|
93
|
+
archived = ?, archived_at = ?, claude_session_id = ?,
|
|
94
|
+
provider = ?, session_kind = ?, runner = ?, structured_state = ?,
|
|
95
|
+
resumed_from_session_id = ?, resumed_to_session_id = ?, auto_recovered = ?,
|
|
96
|
+
worktree_enabled = ?, worktree_info = ?, worktree_merge_status = ?, worktree_merge_info = ?`;
|
|
97
|
+
}
|
|
98
|
+
function sessionPersistValues(snapshot) {
|
|
99
|
+
return [
|
|
100
|
+
snapshot.id,
|
|
101
|
+
snapshot.command,
|
|
102
|
+
snapshot.cwd,
|
|
103
|
+
snapshot.mode,
|
|
104
|
+
snapshot.status,
|
|
105
|
+
snapshot.exitCode,
|
|
106
|
+
snapshot.startedAt,
|
|
107
|
+
snapshot.endedAt,
|
|
108
|
+
snapshot.output,
|
|
109
|
+
snapshot.archived ? 1 : 0,
|
|
110
|
+
snapshot.archivedAt,
|
|
111
|
+
snapshot.claudeSessionId,
|
|
112
|
+
snapshot.provider ?? null,
|
|
113
|
+
snapshot.sessionKind ?? "pty",
|
|
114
|
+
snapshot.runner ?? null,
|
|
115
|
+
snapshot.messages ? JSON.stringify(snapshot.messages) : null,
|
|
116
|
+
snapshot.queuedMessages ? JSON.stringify(snapshot.queuedMessages) : null,
|
|
117
|
+
snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null,
|
|
118
|
+
snapshot.resumedFromSessionId ?? null,
|
|
119
|
+
snapshot.resumedToSessionId ?? null,
|
|
120
|
+
snapshot.autoRecovered ? 1 : 0,
|
|
121
|
+
snapshot.worktreeEnabled ? 1 : 0,
|
|
122
|
+
serializeWorktreeInfo(snapshot.worktree),
|
|
123
|
+
snapshot.worktreeMergeStatus ?? null,
|
|
124
|
+
serializeWorktreeMergeInfo(snapshot.worktreeMergeInfo),
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
function sessionMetadataValues(snapshot) {
|
|
128
|
+
return [
|
|
129
|
+
snapshot.command,
|
|
130
|
+
snapshot.cwd,
|
|
131
|
+
snapshot.mode,
|
|
132
|
+
snapshot.status,
|
|
133
|
+
snapshot.exitCode,
|
|
134
|
+
snapshot.startedAt,
|
|
135
|
+
snapshot.endedAt,
|
|
136
|
+
snapshot.output,
|
|
137
|
+
snapshot.archived ? 1 : 0,
|
|
138
|
+
snapshot.archivedAt,
|
|
139
|
+
snapshot.claudeSessionId,
|
|
140
|
+
snapshot.provider ?? null,
|
|
141
|
+
snapshot.sessionKind ?? "pty",
|
|
142
|
+
snapshot.runner ?? null,
|
|
143
|
+
snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null,
|
|
144
|
+
snapshot.resumedFromSessionId ?? null,
|
|
145
|
+
snapshot.resumedToSessionId ?? null,
|
|
146
|
+
snapshot.autoRecovered ? 1 : 0,
|
|
147
|
+
snapshot.worktreeEnabled ? 1 : 0,
|
|
148
|
+
serializeWorktreeInfo(snapshot.worktree),
|
|
149
|
+
snapshot.worktreeMergeStatus ?? null,
|
|
150
|
+
serializeWorktreeMergeInfo(snapshot.worktreeMergeInfo),
|
|
151
|
+
snapshot.id,
|
|
152
|
+
];
|
|
153
|
+
}
|
|
154
|
+
function mapSessionCore(row) {
|
|
155
|
+
const provider = inferSessionProvider(row);
|
|
156
|
+
return {
|
|
157
|
+
id: row.id,
|
|
158
|
+
sessionKind: row.session_kind ?? "pty",
|
|
159
|
+
provider,
|
|
160
|
+
runner: row.runner ?? undefined,
|
|
161
|
+
command: row.command,
|
|
162
|
+
cwd: row.cwd,
|
|
163
|
+
mode: row.mode,
|
|
164
|
+
status: row.status,
|
|
165
|
+
exitCode: row.exit_code,
|
|
166
|
+
startedAt: row.started_at,
|
|
167
|
+
endedAt: row.ended_at,
|
|
168
|
+
output: row.output,
|
|
169
|
+
archived: Boolean(row.archived),
|
|
170
|
+
archivedAt: row.archived_at,
|
|
171
|
+
claudeSessionId: row.claude_session_id,
|
|
172
|
+
messages: safeJsonParse(row.messages),
|
|
173
|
+
queuedMessages: parseQueuedMessages(row.queued_messages),
|
|
174
|
+
structuredState: safeJsonParse(row.structured_state),
|
|
175
|
+
resumedFromSessionId: row.resumed_from_session_id ?? undefined,
|
|
176
|
+
resumedToSessionId: row.resumed_to_session_id ?? undefined,
|
|
177
|
+
autoRecovered: Boolean(row.auto_recovered),
|
|
178
|
+
worktreeEnabled: Boolean(row.worktree_enabled),
|
|
179
|
+
worktree: parseWorktreeInfo(row.worktree_info) ?? null,
|
|
180
|
+
...mapWorktreeMergeFields(row),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function sessionRowQuery(base) {
|
|
184
|
+
return `${base} ${sessionSelectFields()}`;
|
|
185
|
+
}
|
|
65
186
|
export const DEFAULT_DB_FILE = "wand.db";
|
|
66
187
|
export function resolveDatabasePath(configPath) {
|
|
67
188
|
return path.resolve(path.dirname(configPath), DEFAULT_DB_FILE);
|
|
@@ -95,7 +216,9 @@ const INIT_SQL = `
|
|
|
95
216
|
resumed_to_session_id TEXT,
|
|
96
217
|
auto_recovered INTEGER NOT NULL DEFAULT 0,
|
|
97
218
|
worktree_enabled INTEGER NOT NULL DEFAULT 0,
|
|
98
|
-
worktree_info TEXT
|
|
219
|
+
worktree_info TEXT,
|
|
220
|
+
worktree_merge_status TEXT,
|
|
221
|
+
worktree_merge_info TEXT
|
|
99
222
|
);
|
|
100
223
|
|
|
101
224
|
CREATE TABLE IF NOT EXISTS app_config (
|
|
@@ -185,34 +308,11 @@ export class WandStorage {
|
|
|
185
308
|
try {
|
|
186
309
|
this.db
|
|
187
310
|
.prepare(`INSERT INTO command_sessions (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
, resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
|
|
191
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
311
|
+
${sessionPersistFields()}
|
|
312
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
192
313
|
ON CONFLICT(id) DO UPDATE SET
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
mode = excluded.mode,
|
|
196
|
-
status = excluded.status,
|
|
197
|
-
exit_code = excluded.exit_code,
|
|
198
|
-
started_at = excluded.started_at,
|
|
199
|
-
ended_at = excluded.ended_at,
|
|
200
|
-
output = excluded.output,
|
|
201
|
-
archived = excluded.archived,
|
|
202
|
-
archived_at = excluded.archived_at,
|
|
203
|
-
claude_session_id = excluded.claude_session_id,
|
|
204
|
-
provider = excluded.provider,
|
|
205
|
-
session_kind = excluded.session_kind,
|
|
206
|
-
runner = excluded.runner,
|
|
207
|
-
messages = excluded.messages,
|
|
208
|
-
queued_messages = excluded.queued_messages,
|
|
209
|
-
structured_state = excluded.structured_state,
|
|
210
|
-
resumed_from_session_id = excluded.resumed_from_session_id,
|
|
211
|
-
resumed_to_session_id = excluded.resumed_to_session_id,
|
|
212
|
-
auto_recovered = excluded.auto_recovered,
|
|
213
|
-
worktree_enabled = excluded.worktree_enabled,
|
|
214
|
-
worktree_info = excluded.worktree_info`)
|
|
215
|
-
.run(snapshot.id, snapshot.command, snapshot.cwd, snapshot.mode, snapshot.status, snapshot.exitCode, snapshot.startedAt, snapshot.endedAt, snapshot.output, snapshot.archived ? 1 : 0, snapshot.archivedAt, snapshot.claudeSessionId, snapshot.provider ?? null, snapshot.sessionKind ?? "pty", snapshot.runner ?? null, snapshot.messages ? JSON.stringify(snapshot.messages) : null, snapshot.queuedMessages ? JSON.stringify(snapshot.queuedMessages) : null, snapshot.structuredState ? JSON.stringify(snapshot.structuredState) : null, snapshot.resumedFromSessionId ?? null, snapshot.resumedToSessionId ?? null, snapshot.autoRecovered ? 1 : 0, snapshot.worktreeEnabled ? 1 : 0, snapshot.worktree ? JSON.stringify(snapshot.worktree) : null);
|
|
314
|
+
${sessionPersistAssignments()}`)
|
|
315
|
+
.run(...sessionPersistValues(snapshot));
|
|
216
316
|
this.db.exec("COMMIT");
|
|
217
317
|
}
|
|
218
318
|
catch (error) {
|
|
@@ -228,19 +328,13 @@ export class WandStorage {
|
|
|
228
328
|
saveSessionMetadata(snapshot) {
|
|
229
329
|
this.db
|
|
230
330
|
.prepare(`UPDATE command_sessions SET
|
|
231
|
-
|
|
232
|
-
started_at = ?, ended_at = ?, output = ?,
|
|
233
|
-
archived = ?, archived_at = ?, claude_session_id = ?,
|
|
234
|
-
provider = ?, session_kind = ?, runner = ?, structured_state = ?,
|
|
235
|
-
resumed_from_session_id = ?, resumed_to_session_id = ?, auto_recovered = ?,
|
|
236
|
-
worktree_enabled = ?, worktree_info = ?
|
|
331
|
+
${sessionMetadataAssignments()}
|
|
237
332
|
WHERE id = ?`)
|
|
238
|
-
.run(
|
|
333
|
+
.run(...sessionMetadataValues(snapshot));
|
|
239
334
|
}
|
|
240
335
|
getSession(id) {
|
|
241
336
|
const row = this.db
|
|
242
|
-
.prepare(
|
|
243
|
-
, resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
|
|
337
|
+
.prepare(`${sessionRowQuery("SELECT")}
|
|
244
338
|
FROM command_sessions
|
|
245
339
|
WHERE id = ?`)
|
|
246
340
|
.get(id);
|
|
@@ -248,8 +342,7 @@ export class WandStorage {
|
|
|
248
342
|
}
|
|
249
343
|
getLatestSessionByClaudeSessionId(claudeSessionId) {
|
|
250
344
|
const row = this.db
|
|
251
|
-
.prepare(
|
|
252
|
-
, resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
|
|
345
|
+
.prepare(`${sessionRowQuery("SELECT")}
|
|
253
346
|
FROM command_sessions
|
|
254
347
|
WHERE claude_session_id = ?
|
|
255
348
|
ORDER BY started_at DESC
|
|
@@ -259,85 +352,56 @@ export class WandStorage {
|
|
|
259
352
|
}
|
|
260
353
|
loadSessions() {
|
|
261
354
|
const rows = this.db
|
|
262
|
-
.prepare(
|
|
263
|
-
, resumed_from_session_id, resumed_to_session_id, auto_recovered, worktree_enabled, worktree_info
|
|
355
|
+
.prepare(`${sessionRowQuery("SELECT")}
|
|
264
356
|
FROM command_sessions
|
|
265
357
|
ORDER BY started_at DESC`)
|
|
266
358
|
.all();
|
|
267
359
|
return rows.map((row) => this.mapSessionRow(row));
|
|
268
360
|
}
|
|
269
361
|
mapSessionRow(row) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
startedAt: row.started_at,
|
|
282
|
-
endedAt: row.ended_at,
|
|
283
|
-
output: row.output,
|
|
284
|
-
archived: Boolean(row.archived),
|
|
285
|
-
archivedAt: row.archived_at,
|
|
286
|
-
claudeSessionId: row.claude_session_id,
|
|
287
|
-
messages: parseStoredMessages(row.messages),
|
|
288
|
-
queuedMessages: parseQueuedMessages(row.queued_messages),
|
|
289
|
-
structuredState: parseStructuredState(row.structured_state),
|
|
290
|
-
resumedFromSessionId: row.resumed_from_session_id ?? undefined,
|
|
291
|
-
resumedToSessionId: row.resumed_to_session_id ?? undefined,
|
|
292
|
-
autoRecovered: Boolean(row.auto_recovered),
|
|
293
|
-
worktreeEnabled: Boolean(row.worktree_enabled),
|
|
294
|
-
worktree: parseWorktreeInfo(row.worktree_info) ?? null
|
|
362
|
+
return mapSessionCore(row);
|
|
363
|
+
}
|
|
364
|
+
updateSessionWorktreeMergeState(id, status, info) {
|
|
365
|
+
const current = this.getSession(id);
|
|
366
|
+
if (!current) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
const updated = {
|
|
370
|
+
...current,
|
|
371
|
+
worktreeMergeStatus: status,
|
|
372
|
+
worktreeMergeInfo: info,
|
|
295
373
|
};
|
|
374
|
+
this.saveSessionMetadata(updated);
|
|
375
|
+
return updated;
|
|
296
376
|
}
|
|
297
377
|
deleteSession(id) {
|
|
298
378
|
this.db.prepare("DELETE FROM command_sessions WHERE id = ?").run(id);
|
|
299
379
|
}
|
|
300
380
|
}
|
|
381
|
+
const SCHEMA_MIGRATIONS = [
|
|
382
|
+
["archived", "ALTER TABLE command_sessions ADD COLUMN archived INTEGER NOT NULL DEFAULT 0"],
|
|
383
|
+
["archived_at", "ALTER TABLE command_sessions ADD COLUMN archived_at TEXT"],
|
|
384
|
+
["claude_session_id", "ALTER TABLE command_sessions ADD COLUMN claude_session_id TEXT"],
|
|
385
|
+
["provider", "ALTER TABLE command_sessions ADD COLUMN provider TEXT"],
|
|
386
|
+
["session_kind", "ALTER TABLE command_sessions ADD COLUMN session_kind TEXT NOT NULL DEFAULT 'pty'"],
|
|
387
|
+
["runner", "ALTER TABLE command_sessions ADD COLUMN runner TEXT"],
|
|
388
|
+
["messages", "ALTER TABLE command_sessions ADD COLUMN messages TEXT"],
|
|
389
|
+
["queued_messages", "ALTER TABLE command_sessions ADD COLUMN queued_messages TEXT"],
|
|
390
|
+
["structured_state", "ALTER TABLE command_sessions ADD COLUMN structured_state TEXT"],
|
|
391
|
+
["resumed_from_session_id", "ALTER TABLE command_sessions ADD COLUMN resumed_from_session_id TEXT"],
|
|
392
|
+
["resumed_to_session_id", "ALTER TABLE command_sessions ADD COLUMN resumed_to_session_id TEXT"],
|
|
393
|
+
["auto_recovered", "ALTER TABLE command_sessions ADD COLUMN auto_recovered INTEGER NOT NULL DEFAULT 0"],
|
|
394
|
+
["worktree_enabled", "ALTER TABLE command_sessions ADD COLUMN worktree_enabled INTEGER NOT NULL DEFAULT 0"],
|
|
395
|
+
["worktree_info", "ALTER TABLE command_sessions ADD COLUMN worktree_info TEXT"],
|
|
396
|
+
["worktree_merge_status", "ALTER TABLE command_sessions ADD COLUMN worktree_merge_status TEXT"],
|
|
397
|
+
["worktree_merge_info", "ALTER TABLE command_sessions ADD COLUMN worktree_merge_info TEXT"],
|
|
398
|
+
];
|
|
301
399
|
function ensureCommandSessionSchema(db) {
|
|
302
400
|
const columns = db.prepare("PRAGMA table_info(command_sessions)").all();
|
|
303
401
|
const names = new Set(columns.map((column) => column.name));
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN archived_at TEXT");
|
|
309
|
-
}
|
|
310
|
-
if (!names.has("claude_session_id")) {
|
|
311
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN claude_session_id TEXT");
|
|
312
|
-
}
|
|
313
|
-
if (!names.has("provider")) {
|
|
314
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN provider TEXT");
|
|
315
|
-
}
|
|
316
|
-
if (!names.has("session_kind")) {
|
|
317
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN session_kind TEXT NOT NULL DEFAULT 'pty'");
|
|
318
|
-
}
|
|
319
|
-
if (!names.has("runner")) {
|
|
320
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN runner TEXT");
|
|
321
|
-
}
|
|
322
|
-
if (!names.has("messages")) {
|
|
323
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN messages TEXT");
|
|
324
|
-
}
|
|
325
|
-
if (!names.has("queued_messages")) {
|
|
326
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN queued_messages TEXT");
|
|
327
|
-
}
|
|
328
|
-
if (!names.has("structured_state")) {
|
|
329
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN structured_state TEXT");
|
|
330
|
-
}
|
|
331
|
-
if (!names.has("resumed_from_session_id")) {
|
|
332
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN resumed_from_session_id TEXT");
|
|
333
|
-
}
|
|
334
|
-
if (!names.has("resumed_to_session_id")) {
|
|
335
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN resumed_to_session_id TEXT");
|
|
336
|
-
}
|
|
337
|
-
if (!names.has("worktree_enabled")) {
|
|
338
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN worktree_enabled INTEGER NOT NULL DEFAULT 0");
|
|
339
|
-
}
|
|
340
|
-
if (!names.has("worktree_info")) {
|
|
341
|
-
db.exec("ALTER TABLE command_sessions ADD COLUMN worktree_info TEXT");
|
|
402
|
+
for (const [column, sql] of SCHEMA_MIGRATIONS) {
|
|
403
|
+
if (!names.has(column)) {
|
|
404
|
+
db.exec(sql);
|
|
405
|
+
}
|
|
342
406
|
}
|
|
343
407
|
}
|