negotium 0.2.2 → 0.2.4
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/agent-helpers.js +3117 -2915
- package/dist/agent-helpers.js.map +38 -37
- package/dist/background-bash.js.map +1 -1
- package/dist/browser-runtime.js +137 -21
- package/dist/browser-runtime.js.map +10 -9
- package/dist/{chunk-kc5kmr95.js → chunk-1s9ryz8g.js} +1195 -21
- package/dist/chunk-1s9ryz8g.js.map +35 -0
- package/dist/hosted-agent.js +60 -48
- package/dist/hosted-agent.js.map +10 -10
- package/dist/main.js +3038 -3357
- package/dist/main.js.map +35 -35
- package/dist/mcp-factories.js +6227 -6021
- package/dist/mcp-factories.js.map +40 -39
- package/dist/prompts.js.map +1 -1
- package/dist/query-runtime.js +22 -2
- package/dist/query-runtime.js.map +4 -4
- package/dist/registry.js +169 -213
- package/dist/registry.js.map +8 -9
- package/dist/rollout.js +1 -1
- package/dist/runtime/src/agents/codex-app-server.ts +136 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +1 -1
- package/dist/runtime/src/agents/codex-provider.ts +21 -11
- package/dist/runtime/src/agents/codex-registry.ts +9 -17
- package/dist/runtime/src/agents/contracts.ts +2 -0
- package/dist/runtime/src/agents/execution-host.ts +6 -0
- package/dist/runtime/src/agents/fork.ts +24 -18
- package/dist/runtime/src/agents/idle-archiver.ts +7 -2
- package/dist/runtime/src/agents/index.ts +2 -1
- package/dist/runtime/src/agents/maestro-registry.ts +26 -7
- package/dist/runtime/src/agents/memory-archive-policy.ts +20 -0
- package/dist/runtime/src/agents/rollout/codex.ts +6 -5
- package/dist/runtime/src/agents/rollout/shared.ts +3 -5
- package/dist/runtime/src/mcp/factories/session-comm.ts +12 -0
- package/dist/runtime/src/mcp/session-comm/default-host.ts +16 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +25 -0
- package/dist/runtime/src/platform/config.ts +4 -1
- package/dist/runtime/src/platform/playwright/manager.ts +57 -1
- package/dist/runtime/src/platform/playwright/profile-management.ts +33 -0
- package/dist/runtime/src/platform/playwright/public-runtime.ts +5 -0
- package/dist/runtime/src/platform/playwright/vault-broker.ts +9 -10
- package/dist/runtime/src/storage/browser-profiles.ts +35 -10
- package/dist/runtime/src/storage/storage-host.ts +31 -5
- package/dist/runtime/src/storage/vault.ts +30 -2
- package/dist/runtime/src/topics/derive.ts +38 -2
- package/dist/runtime/src/topics/lifecycle.ts +12 -2
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +22 -2
- package/dist/runtime-helpers.js.map +4 -4
- package/dist/storage.js +22 -2
- package/dist/storage.js.map +4 -4
- package/dist/types/packages/core/src/agents/codex-app-server.d.ts +13 -0
- package/dist/types/packages/core/src/agents/codex-native-multi-agent.d.ts +1 -0
- package/dist/types/packages/core/src/agents/contracts.d.ts +2 -0
- package/dist/types/packages/core/src/agents/execution-host.d.ts +2 -0
- package/dist/types/packages/core/src/agents/fork.d.ts +8 -7
- package/dist/types/packages/core/src/agents/memory-archive-policy.d.ts +10 -0
- package/dist/types/packages/core/src/agents/rollout/codex.d.ts +4 -2
- package/dist/types/packages/core/src/mcp/factories/session-comm.d.ts +1 -0
- package/dist/types/packages/core/src/platform/playwright/manager.d.ts +15 -0
- package/dist/types/packages/core/src/platform/playwright/profile-management.d.ts +8 -0
- package/dist/types/packages/core/src/platform/playwright/public-runtime.d.ts +2 -1
- package/dist/types/packages/core/src/storage/browser-profiles.d.ts +3 -1
- package/dist/types/packages/core/src/storage/storage-host.d.ts +1 -0
- package/dist/types/packages/core/src/storage/vault.d.ts +8 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js +19 -3
- package/dist/vault.js.map +4 -4
- package/package.json +2 -2
- package/dist/chunk-kc5kmr95.js.map +0 -19
- package/dist/runtime/src/migration/single-user.ts +0 -805
|
@@ -1,805 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
chmodSync,
|
|
4
|
-
copyFileSync,
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
readdirSync,
|
|
8
|
-
readFileSync,
|
|
9
|
-
renameSync,
|
|
10
|
-
rmSync,
|
|
11
|
-
statSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
} from "node:fs";
|
|
14
|
-
import { homedir } from "node:os";
|
|
15
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
16
|
-
import { sanitizeTopicName } from "#security/sanitize";
|
|
17
|
-
import { Database } from "#storage/sqlite";
|
|
18
|
-
import { decryptVaultValueWithKey, encryptVaultValueWithKey } from "#storage/vault-crypto-core";
|
|
19
|
-
|
|
20
|
-
export const CANONICAL_LOCAL_USER_ID = "local";
|
|
21
|
-
export const SINGLE_USER_MIGRATION_ID = "negotium-0.2.0-single-user";
|
|
22
|
-
|
|
23
|
-
export interface SingleUserMigrationOptions {
|
|
24
|
-
stateDir?: string;
|
|
25
|
-
sourcePrincipal?: string;
|
|
26
|
-
deleteOtherUsers: boolean;
|
|
27
|
-
confirmed: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface SingleUserMigrationResult {
|
|
31
|
-
status: "completed" | "already-completed";
|
|
32
|
-
markerPath: string;
|
|
33
|
-
sourcePrincipal: string;
|
|
34
|
-
movedPaths: number;
|
|
35
|
-
deletedPaths: number;
|
|
36
|
-
migratedTables: string[];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface MoveOperation {
|
|
40
|
-
source: string;
|
|
41
|
-
staged: string;
|
|
42
|
-
destination?: string;
|
|
43
|
-
destinationStaged?: string;
|
|
44
|
-
collision?: "keep-destination" | "replace-destination" | "merge-jsonl" | "merge-tasks";
|
|
45
|
-
state: "pending" | "staged" | "source-staged" | "destination-staged" | "placed";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface MigrationJournal {
|
|
49
|
-
id: string;
|
|
50
|
-
sourcePrincipal: string;
|
|
51
|
-
createdAt: string;
|
|
52
|
-
phase: "filesystem" | "database" | "complete";
|
|
53
|
-
operations: MoveOperation[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function entries(path: string): string[] {
|
|
57
|
-
return existsSync(path) && statSync(path).isDirectory() ? readdirSync(path) : [];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function processIsAlive(pid: number): boolean {
|
|
61
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
62
|
-
try {
|
|
63
|
-
process.kill(pid, 0);
|
|
64
|
-
return true;
|
|
65
|
-
} catch (error) {
|
|
66
|
-
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function assertNoActiveNode(stateDir: string): void {
|
|
71
|
-
for (const runtimeName of ["run", "runtime"]) {
|
|
72
|
-
const infoPath = join(stateDir, runtimeName, "node-daemon.json");
|
|
73
|
-
if (!existsSync(infoPath)) continue;
|
|
74
|
-
try {
|
|
75
|
-
const value = JSON.parse(readFileSync(infoPath, "utf8")) as {
|
|
76
|
-
pid?: number;
|
|
77
|
-
};
|
|
78
|
-
if (typeof value.pid === "number" && processIsAlive(value.pid)) {
|
|
79
|
-
throw new Error(`Refusing migration while Negotium pid ${value.pid} is active.`);
|
|
80
|
-
}
|
|
81
|
-
} catch (error) {
|
|
82
|
-
if (error instanceof SyntaxError) {
|
|
83
|
-
throw new Error(`Refusing migration: unreadable process metadata at ${infoPath}.`);
|
|
84
|
-
}
|
|
85
|
-
throw error;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const dbPath = join(stateDir, "data", "sessions.db");
|
|
89
|
-
if (!existsSync(dbPath)) return;
|
|
90
|
-
const database = new Database(dbPath, { readonly: true });
|
|
91
|
-
try {
|
|
92
|
-
const hasLeases = database
|
|
93
|
-
.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='runtime_process_leases'")
|
|
94
|
-
.get();
|
|
95
|
-
if (!hasLeases) return;
|
|
96
|
-
const leases = database
|
|
97
|
-
.query<{ role: string; pid: number }, []>("SELECT role, pid FROM runtime_process_leases")
|
|
98
|
-
.all();
|
|
99
|
-
const active = leases.find((lease) => processIsAlive(lease.pid));
|
|
100
|
-
if (active)
|
|
101
|
-
throw new Error(`Refusing migration while ${active.role} pid ${active.pid} is active.`);
|
|
102
|
-
} finally {
|
|
103
|
-
database.close();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function oldOwnerDirectory(ownerId: string): string {
|
|
108
|
-
const digest = createHash("sha256").update(ownerId).digest("hex").slice(0, 16);
|
|
109
|
-
return `${sanitizeTopicName(ownerId).slice(0, 24)}_${digest}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function addDirectoryContents(
|
|
113
|
-
operations: MoveOperation[],
|
|
114
|
-
sourceDir: string,
|
|
115
|
-
destinationDir: string | undefined,
|
|
116
|
-
stagingDir: string,
|
|
117
|
-
): void {
|
|
118
|
-
for (const name of entries(sourceDir)) {
|
|
119
|
-
operations.push({
|
|
120
|
-
source: join(sourceDir, name),
|
|
121
|
-
staged: join(stagingDir, randomUUID()),
|
|
122
|
-
...(destinationDir ? { destination: join(destinationDir, name) } : {}),
|
|
123
|
-
state: "pending",
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function pathWithin(path: string, root: string): boolean {
|
|
129
|
-
return path === root || path.startsWith(`${root}/`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function collisionPolicy(
|
|
133
|
-
operation: MoveOperation,
|
|
134
|
-
stateDir: string,
|
|
135
|
-
): NonNullable<MoveOperation["collision"]> | undefined {
|
|
136
|
-
const destination = operation.destination;
|
|
137
|
-
if (!destination || !existsSync(destination)) return undefined;
|
|
138
|
-
if (pathWithin(destination, join(stateDir, "runtime"))) return "keep-destination";
|
|
139
|
-
if (pathWithin(destination, join(stateDir, "binaries"))) return "keep-destination";
|
|
140
|
-
if (pathWithin(destination, join(stateDir, "secrets"))) return "replace-destination";
|
|
141
|
-
if (destination === join(stateDir, "data", "vault", "vault.db")) {
|
|
142
|
-
return "replace-destination";
|
|
143
|
-
}
|
|
144
|
-
if (pathWithin(destination, join(stateDir, "browser", "profiles"))) {
|
|
145
|
-
return "replace-destination";
|
|
146
|
-
}
|
|
147
|
-
if (
|
|
148
|
-
pathWithin(destination, join(stateDir, "data", "conversations")) &&
|
|
149
|
-
destination.endsWith(".jsonl")
|
|
150
|
-
) {
|
|
151
|
-
return "merge-jsonl";
|
|
152
|
-
}
|
|
153
|
-
if (pathWithin(destination, join(stateDir, "data", "tasks")) && destination.endsWith(".json")) {
|
|
154
|
-
return "merge-tasks";
|
|
155
|
-
}
|
|
156
|
-
if (
|
|
157
|
-
statSync(operation.source).isFile() &&
|
|
158
|
-
statSync(destination).isFile() &&
|
|
159
|
-
readFileSync(operation.source).equals(readFileSync(destination))
|
|
160
|
-
) {
|
|
161
|
-
return "keep-destination";
|
|
162
|
-
}
|
|
163
|
-
throw new Error(`Migration collision at ${destination}.`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function configureCollisions(
|
|
167
|
-
operations: MoveOperation[],
|
|
168
|
-
stateDir: string,
|
|
169
|
-
stagingRoot: string,
|
|
170
|
-
): void {
|
|
171
|
-
const destinations = new Set<string>();
|
|
172
|
-
for (const operation of operations) {
|
|
173
|
-
if (!operation.destination) continue;
|
|
174
|
-
if (destinations.has(operation.destination)) {
|
|
175
|
-
throw new Error(`Multiple legacy paths target ${operation.destination}.`);
|
|
176
|
-
}
|
|
177
|
-
destinations.add(operation.destination);
|
|
178
|
-
operation.collision = collisionPolicy(operation, stateDir);
|
|
179
|
-
if (operation.collision && operation.collision !== "keep-destination") {
|
|
180
|
-
operation.destinationStaged = join(stagingRoot, randomUUID());
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function writeJournal(path: string, journal: MigrationJournal): void {
|
|
186
|
-
writeFileSync(path, `${JSON.stringify(journal, null, 2)}\n`, { mode: 0o600 });
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function rollbackFilesystem(journal: MigrationJournal, journalPath: string): void {
|
|
190
|
-
for (const operation of [...journal.operations].reverse()) {
|
|
191
|
-
if (operation.state === "placed") {
|
|
192
|
-
if (operation.collision === "keep-destination" || !operation.destination) {
|
|
193
|
-
if (existsSync(operation.staged) && !existsSync(operation.source)) {
|
|
194
|
-
mkdirSync(dirname(operation.source), { recursive: true });
|
|
195
|
-
renameSync(operation.staged, operation.source);
|
|
196
|
-
}
|
|
197
|
-
} else if (operation.destination && existsSync(operation.destination)) {
|
|
198
|
-
if (operation.collision === "merge-jsonl" || operation.collision === "merge-tasks") {
|
|
199
|
-
rmSync(operation.destination, { recursive: true, force: true });
|
|
200
|
-
if (existsSync(operation.staged) && !existsSync(operation.source)) {
|
|
201
|
-
mkdirSync(dirname(operation.source), { recursive: true });
|
|
202
|
-
renameSync(operation.staged, operation.source);
|
|
203
|
-
}
|
|
204
|
-
} else if (!existsSync(operation.source)) {
|
|
205
|
-
mkdirSync(dirname(operation.source), { recursive: true });
|
|
206
|
-
renameSync(operation.destination, operation.source);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
} else if (existsSync(operation.staged) && !existsSync(operation.source)) {
|
|
210
|
-
mkdirSync(dirname(operation.source), { recursive: true });
|
|
211
|
-
renameSync(operation.staged, operation.source);
|
|
212
|
-
}
|
|
213
|
-
if (
|
|
214
|
-
operation.destination &&
|
|
215
|
-
operation.destinationStaged &&
|
|
216
|
-
existsSync(operation.destinationStaged) &&
|
|
217
|
-
!existsSync(operation.destination)
|
|
218
|
-
) {
|
|
219
|
-
mkdirSync(dirname(operation.destination), { recursive: true });
|
|
220
|
-
renameSync(operation.destinationStaged, operation.destination);
|
|
221
|
-
}
|
|
222
|
-
operation.state = "pending";
|
|
223
|
-
}
|
|
224
|
-
journal.phase = "filesystem";
|
|
225
|
-
writeJournal(journalPath, journal);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function mergedJsonl(source: string, destination: string): string {
|
|
229
|
-
const lines = new Set<string>();
|
|
230
|
-
for (const path of [source, destination]) {
|
|
231
|
-
for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
232
|
-
if (line) lines.add(line);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
return lines.size > 0 ? `${[...lines].join("\n")}\n` : "";
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
interface TaskFileShape {
|
|
239
|
-
version: 1;
|
|
240
|
-
tasks: Array<{ id: string; blockedBy?: string[]; [key: string]: unknown }>;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function readTaskFile(path: string): TaskFileShape {
|
|
244
|
-
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<TaskFileShape>;
|
|
245
|
-
if (parsed.version !== 1 || !Array.isArray(parsed.tasks)) {
|
|
246
|
-
throw new Error(`Invalid task file during migration: ${path}`);
|
|
247
|
-
}
|
|
248
|
-
return parsed as TaskFileShape;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function mergedTasks(source: string, destination: string): string {
|
|
252
|
-
const legacy = readTaskFile(source).tasks;
|
|
253
|
-
const current = readTaskFile(destination).tasks;
|
|
254
|
-
const merged = legacy.map((task) => ({ ...task }));
|
|
255
|
-
const usedIds = new Set(merged.map((task) => String(task.id)));
|
|
256
|
-
let nextId = Math.max(0, ...[...usedIds].map(Number).filter(Number.isInteger)) + 1;
|
|
257
|
-
const remapped = new Map<string, string>();
|
|
258
|
-
|
|
259
|
-
for (const task of current) {
|
|
260
|
-
const originalId = String(task.id);
|
|
261
|
-
const exact = merged.find(
|
|
262
|
-
(candidate) =>
|
|
263
|
-
candidate.id === originalId && JSON.stringify(candidate) === JSON.stringify(task),
|
|
264
|
-
);
|
|
265
|
-
if (exact) {
|
|
266
|
-
remapped.set(originalId, originalId);
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
const id = usedIds.has(originalId) ? String(nextId++) : originalId;
|
|
270
|
-
usedIds.add(id);
|
|
271
|
-
remapped.set(originalId, id);
|
|
272
|
-
merged.push({ ...task, id });
|
|
273
|
-
}
|
|
274
|
-
for (const task of merged.slice(legacy.length)) {
|
|
275
|
-
if (task.blockedBy) task.blockedBy = task.blockedBy.map((id) => remapped.get(id) ?? id);
|
|
276
|
-
}
|
|
277
|
-
return `${JSON.stringify({ version: 1, tasks: merged }, null, 2)}\n`;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function placeOperation(operation: MoveOperation): void {
|
|
281
|
-
if (!operation.destination || operation.collision === "keep-destination") return;
|
|
282
|
-
mkdirSync(dirname(operation.destination), { recursive: true });
|
|
283
|
-
if (operation.collision === "merge-jsonl") {
|
|
284
|
-
writeFileSync(
|
|
285
|
-
operation.destination,
|
|
286
|
-
mergedJsonl(operation.staged, operation.destinationStaged!),
|
|
287
|
-
);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
if (operation.collision === "merge-tasks") {
|
|
291
|
-
writeFileSync(
|
|
292
|
-
operation.destination,
|
|
293
|
-
mergedTasks(operation.staged, operation.destinationStaged!),
|
|
294
|
-
);
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
renameSync(operation.staged, operation.destination);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function tablesWithColumn(database: InstanceType<typeof Database>, columnName: string): string[] {
|
|
301
|
-
const tables = database
|
|
302
|
-
.query<{ name: string }, []>(
|
|
303
|
-
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
|
|
304
|
-
)
|
|
305
|
-
.all();
|
|
306
|
-
return tables
|
|
307
|
-
.map((row) => row.name)
|
|
308
|
-
.filter((table) =>
|
|
309
|
-
database
|
|
310
|
-
.query<{ name: string }, []>(`PRAGMA table_info("${table.replaceAll('"', '""')}")`)
|
|
311
|
-
.all()
|
|
312
|
-
.some((column) => column.name === columnName),
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function selectedTopicIds(dbPath: string, source: string): Set<string> | null {
|
|
317
|
-
if (!existsSync(dbPath)) return null;
|
|
318
|
-
const database = new Database(dbPath, { readonly: true });
|
|
319
|
-
try {
|
|
320
|
-
const hasMemberships = database
|
|
321
|
-
.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='topic_members'")
|
|
322
|
-
.get();
|
|
323
|
-
if (!hasMemberships) return null;
|
|
324
|
-
return new Set(
|
|
325
|
-
database
|
|
326
|
-
.query<{ topic_id: string }, string>("SELECT topic_id FROM topic_members WHERE user_id = ?")
|
|
327
|
-
.all(source)
|
|
328
|
-
.map((row) => row.topic_id),
|
|
329
|
-
);
|
|
330
|
-
} finally {
|
|
331
|
-
database.close();
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function migrateDatabase(
|
|
336
|
-
dbPath: string,
|
|
337
|
-
source: string,
|
|
338
|
-
masterKey?: string,
|
|
339
|
-
legacyTopicProfiles: Array<{ topicId: string; profile: string }> = [],
|
|
340
|
-
): string[] {
|
|
341
|
-
if (!existsSync(dbPath)) return [];
|
|
342
|
-
const database = new Database(dbPath);
|
|
343
|
-
const userTables = tablesWithColumn(database, "user_id");
|
|
344
|
-
const ownerUserTables = tablesWithColumn(database, "owner_user_id");
|
|
345
|
-
const migratedTables = [...new Set([...userTables, ...ownerUserTables])];
|
|
346
|
-
const retainedTopicIds = selectedTopicIds(dbPath, source);
|
|
347
|
-
const quote = (name: string) => `"${name.replaceAll('"', '""')}"`;
|
|
348
|
-
try {
|
|
349
|
-
if (source !== CANONICAL_LOCAL_USER_ID) {
|
|
350
|
-
for (const [column, tables] of [
|
|
351
|
-
["user_id", userTables],
|
|
352
|
-
["owner_user_id", ownerUserTables],
|
|
353
|
-
] as const) {
|
|
354
|
-
for (const table of tables) {
|
|
355
|
-
const collision = database
|
|
356
|
-
.query<{ count: number }, string>(
|
|
357
|
-
`SELECT COUNT(*) AS count FROM ${quote(table)} WHERE ${column} = ?`,
|
|
358
|
-
)
|
|
359
|
-
.get(CANONICAL_LOCAL_USER_ID)?.count;
|
|
360
|
-
if (collision)
|
|
361
|
-
throw new Error(`Database collision: ${table}.${column} already contains local rows.`);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
const hasUsers = database
|
|
365
|
-
.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='users'")
|
|
366
|
-
.get();
|
|
367
|
-
if (hasUsers) {
|
|
368
|
-
const localUser = database
|
|
369
|
-
.query("SELECT 1 FROM users WHERE id = ?")
|
|
370
|
-
.get(CANONICAL_LOCAL_USER_ID);
|
|
371
|
-
if (localUser)
|
|
372
|
-
throw new Error("Database collision: users already contains canonical local.");
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
database.exec("PRAGMA foreign_keys = OFF");
|
|
376
|
-
database.exec("BEGIN IMMEDIATE");
|
|
377
|
-
for (const table of userTables) {
|
|
378
|
-
database.query(`DELETE FROM ${quote(table)} WHERE user_id <> ?`).run(source);
|
|
379
|
-
if (table === "vault" && source !== CANONICAL_LOCAL_USER_ID) {
|
|
380
|
-
if (!masterKey)
|
|
381
|
-
throw new Error("Cannot migrate encrypted vault rows without vault-master-key.");
|
|
382
|
-
const rows = database
|
|
383
|
-
.query<{ key: string; value: string }, string>(
|
|
384
|
-
"SELECT key, value FROM vault WHERE user_id = ?",
|
|
385
|
-
)
|
|
386
|
-
.all(source);
|
|
387
|
-
for (const row of rows) {
|
|
388
|
-
const plaintext = decryptVaultValueWithKey(source, row.key, row.value, masterKey).value;
|
|
389
|
-
database
|
|
390
|
-
.query("UPDATE vault SET value = ? WHERE user_id = ? AND key = ?")
|
|
391
|
-
.run(
|
|
392
|
-
encryptVaultValueWithKey(CANONICAL_LOCAL_USER_ID, row.key, plaintext, masterKey),
|
|
393
|
-
source,
|
|
394
|
-
row.key,
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
database
|
|
399
|
-
.query(`UPDATE ${quote(table)} SET user_id = ? WHERE user_id = ?`)
|
|
400
|
-
.run(CANONICAL_LOCAL_USER_ID, source);
|
|
401
|
-
}
|
|
402
|
-
for (const table of ownerUserTables) {
|
|
403
|
-
database.query(`DELETE FROM ${quote(table)} WHERE owner_user_id <> ?`).run(source);
|
|
404
|
-
database
|
|
405
|
-
.query(`UPDATE ${quote(table)} SET owner_user_id = ? WHERE owner_user_id = ?`)
|
|
406
|
-
.run(CANONICAL_LOCAL_USER_ID, source);
|
|
407
|
-
}
|
|
408
|
-
if (retainedTopicIds) {
|
|
409
|
-
database.exec("CREATE TEMP TABLE migration_selected_topics (id TEXT PRIMARY KEY)");
|
|
410
|
-
for (const topicId of retainedTopicIds) {
|
|
411
|
-
database.query("INSERT INTO migration_selected_topics (id) VALUES (?)").run(topicId);
|
|
412
|
-
}
|
|
413
|
-
const topicTables = database
|
|
414
|
-
.query<{ name: string }, []>(
|
|
415
|
-
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
|
|
416
|
-
)
|
|
417
|
-
.all()
|
|
418
|
-
.map((row) => row.name)
|
|
419
|
-
.filter((table) =>
|
|
420
|
-
database
|
|
421
|
-
.query<{ name: string }, []>(`PRAGMA table_info(${quote(table)})`)
|
|
422
|
-
.all()
|
|
423
|
-
.some((column) => column.name === "topic_id"),
|
|
424
|
-
);
|
|
425
|
-
for (const table of topicTables) {
|
|
426
|
-
database
|
|
427
|
-
.query(
|
|
428
|
-
`DELETE FROM ${quote(table)} WHERE topic_id NOT IN (SELECT id FROM migration_selected_topics)`,
|
|
429
|
-
)
|
|
430
|
-
.run();
|
|
431
|
-
}
|
|
432
|
-
if (
|
|
433
|
-
database.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='api_topics'").get()
|
|
434
|
-
) {
|
|
435
|
-
database
|
|
436
|
-
.query(
|
|
437
|
-
"DELETE FROM api_topics WHERE id NOT IN (SELECT id FROM migration_selected_topics)",
|
|
438
|
-
)
|
|
439
|
-
.run();
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
// browser owner_id is end-user identity; runtime lease/query owner_id columns are not.
|
|
443
|
-
if (
|
|
444
|
-
database
|
|
445
|
-
.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='browser_profiles'")
|
|
446
|
-
.get()
|
|
447
|
-
) {
|
|
448
|
-
database.query("DELETE FROM browser_profiles WHERE owner_id <> ?").run(source);
|
|
449
|
-
database
|
|
450
|
-
.query("UPDATE browser_profiles SET owner_id = ? WHERE owner_id = ?")
|
|
451
|
-
.run(CANONICAL_LOCAL_USER_ID, source);
|
|
452
|
-
}
|
|
453
|
-
if (
|
|
454
|
-
database
|
|
455
|
-
.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='api_topics'")
|
|
456
|
-
.get() &&
|
|
457
|
-
database
|
|
458
|
-
.query<{ name: string }, []>("PRAGMA table_info(api_topics)")
|
|
459
|
-
.all()
|
|
460
|
-
.some((column) => column.name === "browser_profile_owner")
|
|
461
|
-
) {
|
|
462
|
-
database
|
|
463
|
-
.query("UPDATE api_topics SET browser_profile_owner = ? WHERE browser_profile_owner = ?")
|
|
464
|
-
.run(CANONICAL_LOCAL_USER_ID, source);
|
|
465
|
-
if (retainedTopicIds) {
|
|
466
|
-
database
|
|
467
|
-
.query(
|
|
468
|
-
"UPDATE api_topics SET browser_profile_owner = ? WHERE browser_profile_owner IS NOT NULL",
|
|
469
|
-
)
|
|
470
|
-
.run(CANONICAL_LOCAL_USER_ID);
|
|
471
|
-
} else {
|
|
472
|
-
database
|
|
473
|
-
.query(
|
|
474
|
-
"DELETE FROM api_topics WHERE browser_profile_owner IS NOT NULL AND browser_profile_owner <> ?",
|
|
475
|
-
)
|
|
476
|
-
.run(CANONICAL_LOCAL_USER_ID);
|
|
477
|
-
}
|
|
478
|
-
for (const binding of legacyTopicProfiles) {
|
|
479
|
-
database
|
|
480
|
-
.query(
|
|
481
|
-
"UPDATE api_topics SET browser_profile = ?, browser_profile_owner = ? WHERE id = ?",
|
|
482
|
-
)
|
|
483
|
-
.run(binding.profile, CANONICAL_LOCAL_USER_ID, binding.topicId);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
if (database.query("SELECT 1 FROM sqlite_master WHERE type='table' AND name='users'").get()) {
|
|
487
|
-
database.query("DELETE FROM users WHERE id <> ?").run(source);
|
|
488
|
-
database.query("UPDATE users SET id = ? WHERE id = ?").run(CANONICAL_LOCAL_USER_ID, source);
|
|
489
|
-
}
|
|
490
|
-
database.exec("COMMIT");
|
|
491
|
-
database.exec("PRAGMA foreign_keys = ON");
|
|
492
|
-
const fkErrors = database.query("PRAGMA foreign_key_check").all();
|
|
493
|
-
if (fkErrors.length > 0) throw new Error("Database migration left foreign-key violations.");
|
|
494
|
-
return migratedTables;
|
|
495
|
-
} catch (error) {
|
|
496
|
-
try {
|
|
497
|
-
database.exec("ROLLBACK");
|
|
498
|
-
} catch {}
|
|
499
|
-
throw error;
|
|
500
|
-
} finally {
|
|
501
|
-
database.close();
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
export function migrateSingleUserState(
|
|
506
|
-
options: SingleUserMigrationOptions,
|
|
507
|
-
): SingleUserMigrationResult {
|
|
508
|
-
const stateDir = resolve(
|
|
509
|
-
options.stateDir ?? process.env.NEGOTIUM_STATE_DIR ?? join(homedir(), ".negotium"),
|
|
510
|
-
);
|
|
511
|
-
const source = options.sourcePrincipal?.trim() || CANONICAL_LOCAL_USER_ID;
|
|
512
|
-
if (!source || /[/\\]|\.\./.test(source)) throw new Error("Invalid source principal.");
|
|
513
|
-
if (!options.deleteOtherUsers || !options.confirmed) {
|
|
514
|
-
throw new Error(
|
|
515
|
-
"Migration requires --delete-other-users and --yes; other principals are permanently deleted.",
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
const markerPath = join(stateDir, ".migration-0.2.0-single-user.json");
|
|
519
|
-
if (existsSync(markerPath)) {
|
|
520
|
-
const marker = JSON.parse(readFileSync(markerPath, "utf8")) as {
|
|
521
|
-
id?: string;
|
|
522
|
-
sourcePrincipal?: string;
|
|
523
|
-
movedPaths?: number;
|
|
524
|
-
deletedPaths?: number;
|
|
525
|
-
migratedTables?: string[];
|
|
526
|
-
};
|
|
527
|
-
if (marker.id !== SINGLE_USER_MIGRATION_ID)
|
|
528
|
-
throw new Error(`Unexpected migration marker at ${markerPath}.`);
|
|
529
|
-
return {
|
|
530
|
-
status: "already-completed",
|
|
531
|
-
markerPath,
|
|
532
|
-
sourcePrincipal: marker.sourcePrincipal ?? source,
|
|
533
|
-
movedPaths: marker.movedPaths ?? 0,
|
|
534
|
-
deletedPaths: marker.deletedPaths ?? 0,
|
|
535
|
-
migratedTables: marker.migratedTables ?? [],
|
|
536
|
-
};
|
|
537
|
-
}
|
|
538
|
-
assertNoActiveNode(stateDir);
|
|
539
|
-
mkdirSync(stateDir, { recursive: true });
|
|
540
|
-
const stagingRoot = join(stateDir, ".migration-0.2.0-staging");
|
|
541
|
-
const journalPath = join(stagingRoot, "journal.json");
|
|
542
|
-
mkdirSync(stagingRoot, { recursive: true, mode: 0o700 });
|
|
543
|
-
let journal: MigrationJournal;
|
|
544
|
-
if (existsSync(journalPath)) {
|
|
545
|
-
journal = JSON.parse(readFileSync(journalPath, "utf8")) as MigrationJournal;
|
|
546
|
-
if (journal.sourcePrincipal !== source)
|
|
547
|
-
throw new Error("Interrupted migration used a different source principal.");
|
|
548
|
-
} else {
|
|
549
|
-
const operations: MoveOperation[] = [];
|
|
550
|
-
const retainedTopicIds = selectedTopicIds(join(stateDir, "data", "sessions.db"), source);
|
|
551
|
-
addDirectoryContents(operations, join(stateDir, "run"), join(stateDir, "runtime"), stagingRoot);
|
|
552
|
-
addDirectoryContents(
|
|
553
|
-
operations,
|
|
554
|
-
join(stateDir, "bin"),
|
|
555
|
-
join(stateDir, "binaries"),
|
|
556
|
-
stagingRoot,
|
|
557
|
-
);
|
|
558
|
-
for (const secret of ["node-control-token", "runtime-mcp-secret", "vault-master-key"]) {
|
|
559
|
-
const sourcePath = join(stateDir, secret);
|
|
560
|
-
if (existsSync(sourcePath))
|
|
561
|
-
operations.push({
|
|
562
|
-
source: sourcePath,
|
|
563
|
-
staged: join(stagingRoot, randomUUID()),
|
|
564
|
-
destination: join(stateDir, "secrets", secret),
|
|
565
|
-
state: "pending",
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
const legacyVaultDb = join(stateDir, "data", "vault.db");
|
|
569
|
-
if (existsSync(legacyVaultDb)) {
|
|
570
|
-
operations.push({
|
|
571
|
-
source: legacyVaultDb,
|
|
572
|
-
staged: join(stagingRoot, randomUUID()),
|
|
573
|
-
destination: join(stateDir, "data", "vault", "vault.db"),
|
|
574
|
-
state: "pending",
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
for (const store of ["conversations", "tasks", "users"]) {
|
|
578
|
-
const root = join(stateDir, "data", store);
|
|
579
|
-
addDirectoryContents(operations, join(root, source), root, stagingRoot);
|
|
580
|
-
for (const owner of entries(root)) {
|
|
581
|
-
const ownerPath = join(root, owner);
|
|
582
|
-
if (owner !== source && statSync(ownerPath).isDirectory()) {
|
|
583
|
-
operations.push({
|
|
584
|
-
source: ownerPath,
|
|
585
|
-
staged: join(stagingRoot, randomUUID()),
|
|
586
|
-
state: "pending",
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
for (const legacyWorkspaceDir of ["contexts", "dm", "sessions"]) {
|
|
592
|
-
addDirectoryContents(
|
|
593
|
-
operations,
|
|
594
|
-
join(stateDir, "workspace", legacyWorkspaceDir),
|
|
595
|
-
join(stateDir, "data", legacyWorkspaceDir),
|
|
596
|
-
stagingRoot,
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
for (const topicId of entries(join(stateDir, "workspace", "topics"))) {
|
|
600
|
-
const topicPath = join(stateDir, "workspace", "topics", topicId);
|
|
601
|
-
if (retainedTopicIds && !retainedTopicIds.has(topicId)) {
|
|
602
|
-
operations.push({
|
|
603
|
-
source: topicPath,
|
|
604
|
-
staged: join(stagingRoot, randomUUID()),
|
|
605
|
-
state: "pending",
|
|
606
|
-
});
|
|
607
|
-
} else {
|
|
608
|
-
addDirectoryContents(
|
|
609
|
-
operations,
|
|
610
|
-
join(topicPath, "uploads"),
|
|
611
|
-
join(stateDir, "data", "uploads", topicId),
|
|
612
|
-
stagingRoot,
|
|
613
|
-
);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
if (retainedTopicIds) {
|
|
617
|
-
for (const topicId of entries(join(stateDir, "workspace", "wiki"))) {
|
|
618
|
-
if (retainedTopicIds.has(topicId)) continue;
|
|
619
|
-
operations.push({
|
|
620
|
-
source: join(stateDir, "workspace", "wiki", topicId),
|
|
621
|
-
staged: join(stagingRoot, randomUUID()),
|
|
622
|
-
state: "pending",
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
const legacyBrowserRoot = join(stateDir, "workspace", "browser-profiles");
|
|
627
|
-
const selectedProfiles = join(legacyBrowserRoot, "profiles", oldOwnerDirectory(source));
|
|
628
|
-
addDirectoryContents(
|
|
629
|
-
operations,
|
|
630
|
-
selectedProfiles,
|
|
631
|
-
join(stateDir, "browser", "profiles"),
|
|
632
|
-
stagingRoot,
|
|
633
|
-
);
|
|
634
|
-
for (const owner of entries(join(legacyBrowserRoot, "profiles"))) {
|
|
635
|
-
const ownerPath = join(legacyBrowserRoot, "profiles", owner);
|
|
636
|
-
if (ownerPath !== selectedProfiles) {
|
|
637
|
-
operations.push({
|
|
638
|
-
source: ownerPath,
|
|
639
|
-
staged: join(stagingRoot, randomUUID()),
|
|
640
|
-
state: "pending",
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
for (const topicId of entries(legacyBrowserRoot).filter((name) => name !== "profiles")) {
|
|
645
|
-
if (retainedTopicIds && !retainedTopicIds.has(topicId)) {
|
|
646
|
-
operations.push({
|
|
647
|
-
source: join(legacyBrowserRoot, topicId),
|
|
648
|
-
staged: join(stagingRoot, randomUUID()),
|
|
649
|
-
state: "pending",
|
|
650
|
-
});
|
|
651
|
-
continue;
|
|
652
|
-
}
|
|
653
|
-
const profile = `legacy_${createHash("sha256").update(topicId).digest("hex").slice(0, 12)}`;
|
|
654
|
-
operations.push({
|
|
655
|
-
source: join(legacyBrowserRoot, topicId),
|
|
656
|
-
staged: join(stagingRoot, randomUUID()),
|
|
657
|
-
destination: join(stateDir, "browser", "profiles", profile),
|
|
658
|
-
state: "pending",
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
configureCollisions(operations, stateDir, stagingRoot);
|
|
662
|
-
journal = {
|
|
663
|
-
id: SINGLE_USER_MIGRATION_ID,
|
|
664
|
-
sourcePrincipal: source,
|
|
665
|
-
createdAt: new Date().toISOString(),
|
|
666
|
-
phase: "filesystem",
|
|
667
|
-
operations,
|
|
668
|
-
};
|
|
669
|
-
writeJournal(journalPath, journal);
|
|
670
|
-
}
|
|
671
|
-
const dbPath = join(stateDir, "data", "sessions.db");
|
|
672
|
-
const dbBackup = join(stagingRoot, "sessions.db.rollback");
|
|
673
|
-
const vaultDbPath = join(stateDir, "data", "vault", "vault.db");
|
|
674
|
-
const vaultDbBackup = join(stagingRoot, "vault.db.rollback");
|
|
675
|
-
try {
|
|
676
|
-
for (const operation of journal.operations) {
|
|
677
|
-
// Reconcile a crash between rename(2) and the following journal write.
|
|
678
|
-
if (operation.state === "staged") operation.state = "source-staged";
|
|
679
|
-
if (operation.state === "pending" && !existsSync(operation.source)) {
|
|
680
|
-
if (existsSync(operation.staged)) operation.state = "source-staged";
|
|
681
|
-
else if (operation.destination && existsSync(operation.destination)) {
|
|
682
|
-
operation.state = "placed";
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
if (operation.state === "pending") {
|
|
686
|
-
mkdirSync(dirname(operation.staged), { recursive: true });
|
|
687
|
-
renameSync(operation.source, operation.staged);
|
|
688
|
-
operation.state = "source-staged";
|
|
689
|
-
writeJournal(journalPath, journal);
|
|
690
|
-
}
|
|
691
|
-
if (
|
|
692
|
-
operation.state === "source-staged" &&
|
|
693
|
-
operation.collision === "replace-destination" &&
|
|
694
|
-
operation.destination &&
|
|
695
|
-
operation.destinationStaged &&
|
|
696
|
-
!existsSync(operation.staged) &&
|
|
697
|
-
existsSync(operation.destination) &&
|
|
698
|
-
existsSync(operation.destinationStaged)
|
|
699
|
-
) {
|
|
700
|
-
operation.state = "placed";
|
|
701
|
-
writeJournal(journalPath, journal);
|
|
702
|
-
}
|
|
703
|
-
if (
|
|
704
|
-
operation.state === "destination-staged" &&
|
|
705
|
-
operation.collision !== "merge-jsonl" &&
|
|
706
|
-
operation.collision !== "merge-tasks" &&
|
|
707
|
-
operation.collision !== "keep-destination" &&
|
|
708
|
-
operation.destination &&
|
|
709
|
-
!existsSync(operation.staged) &&
|
|
710
|
-
existsSync(operation.destination)
|
|
711
|
-
) {
|
|
712
|
-
operation.state = "placed";
|
|
713
|
-
writeJournal(journalPath, journal);
|
|
714
|
-
}
|
|
715
|
-
if (operation.state === "source-staged") {
|
|
716
|
-
if (operation.destination && operation.destinationStaged) {
|
|
717
|
-
if (existsSync(operation.destination) && !existsSync(operation.destinationStaged)) {
|
|
718
|
-
renameSync(operation.destination, operation.destinationStaged);
|
|
719
|
-
}
|
|
720
|
-
if (!existsSync(operation.destinationStaged)) {
|
|
721
|
-
throw new Error(`Missing collision backup for ${operation.destination}.`);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
operation.state = "destination-staged";
|
|
725
|
-
writeJournal(journalPath, journal);
|
|
726
|
-
}
|
|
727
|
-
if (operation.state === "destination-staged") {
|
|
728
|
-
placeOperation(operation);
|
|
729
|
-
operation.state = "placed";
|
|
730
|
-
writeJournal(journalPath, journal);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
journal.phase = "database";
|
|
734
|
-
writeJournal(journalPath, journal);
|
|
735
|
-
if (existsSync(dbPath) && !existsSync(dbBackup)) copyFileSync(dbPath, dbBackup);
|
|
736
|
-
if (existsSync(vaultDbPath) && !existsSync(vaultDbBackup))
|
|
737
|
-
copyFileSync(vaultDbPath, vaultDbBackup);
|
|
738
|
-
const masterKeyPath = join(stateDir, "secrets", "vault-master-key");
|
|
739
|
-
const masterKey = existsSync(masterKeyPath)
|
|
740
|
-
? readFileSync(masterKeyPath, "utf8").trim()
|
|
741
|
-
: undefined;
|
|
742
|
-
const legacyBrowserRoot = join(stateDir, "workspace", "browser-profiles");
|
|
743
|
-
const legacyTopicProfiles = journal.operations
|
|
744
|
-
.filter(
|
|
745
|
-
(operation) => dirname(operation.source) === legacyBrowserRoot && operation.destination,
|
|
746
|
-
)
|
|
747
|
-
.map((operation) => ({
|
|
748
|
-
topicId: basename(operation.source),
|
|
749
|
-
profile: basename(operation.destination!),
|
|
750
|
-
}));
|
|
751
|
-
const migratedTables = migrateDatabase(dbPath, source, masterKey, legacyTopicProfiles);
|
|
752
|
-
const migratedVaultTables = migrateDatabase(vaultDbPath, source, masterKey);
|
|
753
|
-
for (const table of migratedVaultTables)
|
|
754
|
-
if (!migratedTables.includes(table)) migratedTables.push(table);
|
|
755
|
-
mkdirSync(join(stateDir, "secrets"), { recursive: true, mode: 0o700 });
|
|
756
|
-
chmodSync(join(stateDir, "secrets"), 0o700);
|
|
757
|
-
for (const secret of ["node-control-token", "runtime-mcp-secret", "vault-master-key"]) {
|
|
758
|
-
const path = join(stateDir, "secrets", secret);
|
|
759
|
-
if (existsSync(path)) chmodSync(path, 0o600);
|
|
760
|
-
}
|
|
761
|
-
const deletedPaths = journal.operations.filter((operation) => !operation.destination).length;
|
|
762
|
-
const movedPaths = journal.operations.length - deletedPaths;
|
|
763
|
-
const marker = {
|
|
764
|
-
id: SINGLE_USER_MIGRATION_ID,
|
|
765
|
-
version: "0.2.0",
|
|
766
|
-
completedAt: new Date().toISOString(),
|
|
767
|
-
sourcePrincipal: source,
|
|
768
|
-
canonicalPrincipal: CANONICAL_LOCAL_USER_ID,
|
|
769
|
-
movedPaths,
|
|
770
|
-
deletedPaths,
|
|
771
|
-
migratedTables,
|
|
772
|
-
};
|
|
773
|
-
for (const store of ["conversations", "tasks", "users"]) {
|
|
774
|
-
rmSync(join(stateDir, "data", store, source), {
|
|
775
|
-
recursive: true,
|
|
776
|
-
force: true,
|
|
777
|
-
});
|
|
778
|
-
}
|
|
779
|
-
rmSync(join(stateDir, "run"), { recursive: true, force: true });
|
|
780
|
-
rmSync(join(stateDir, "bin"), { recursive: true, force: true });
|
|
781
|
-
rmSync(join(stateDir, "workspace", "browser-profiles"), {
|
|
782
|
-
recursive: true,
|
|
783
|
-
force: true,
|
|
784
|
-
});
|
|
785
|
-
writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, {
|
|
786
|
-
mode: 0o600,
|
|
787
|
-
flag: "wx",
|
|
788
|
-
});
|
|
789
|
-
journal.phase = "complete";
|
|
790
|
-
rmSync(stagingRoot, { recursive: true, force: true });
|
|
791
|
-
return {
|
|
792
|
-
status: "completed",
|
|
793
|
-
markerPath,
|
|
794
|
-
sourcePrincipal: source,
|
|
795
|
-
movedPaths,
|
|
796
|
-
deletedPaths,
|
|
797
|
-
migratedTables,
|
|
798
|
-
};
|
|
799
|
-
} catch (error) {
|
|
800
|
-
if (existsSync(dbBackup)) copyFileSync(dbBackup, dbPath);
|
|
801
|
-
if (existsSync(vaultDbBackup)) copyFileSync(vaultDbBackup, vaultDbPath);
|
|
802
|
-
rollbackFilesystem(journal, journalPath);
|
|
803
|
-
throw error;
|
|
804
|
-
}
|
|
805
|
-
}
|