@nurix/nustack 0.10.0-dev.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 +77 -0
- package/dist/commands/add.js +130 -0
- package/dist/commands/agent.js +100 -0
- package/dist/commands/bootstrap.js +54 -0
- package/dist/commands/describe.js +34 -0
- package/dist/commands/device.js +159 -0
- package/dist/commands/docs.js +119 -0
- package/dist/commands/doctor.js +284 -0
- package/dist/commands/graph.js +34 -0
- package/dist/commands/handoff.js +108 -0
- package/dist/commands/init.js +280 -0
- package/dist/commands/lane.js +12 -0
- package/dist/commands/list.js +20 -0
- package/dist/commands/login.js +134 -0
- package/dist/commands/logout.js +44 -0
- package/dist/commands/open.js +54 -0
- package/dist/commands/organization.js +114 -0
- package/dist/commands/review.js +152 -0
- package/dist/commands/search.js +57 -0
- package/dist/commands/sessions.js +62 -0
- package/dist/commands/sync.js +85 -0
- package/dist/commands/telemetry.js +112 -0
- package/dist/commands/toolkit.js +167 -0
- package/dist/commands/validate.js +67 -0
- package/dist/commands/workspace-sync.js +645 -0
- package/dist/commands/workspace.js +479 -0
- package/dist/index.js +357 -0
- package/dist/lib/agent/claudeHooks.js +77 -0
- package/dist/lib/auth_grant.js +398 -0
- package/dist/lib/auth_recovery.js +49 -0
- package/dist/lib/browser_opener.js +24 -0
- package/dist/lib/device_identity.js +28 -0
- package/dist/lib/device_keychain.js +101 -0
- package/dist/lib/env_file.js +75 -0
- package/dist/lib/etna/manager.js +43 -0
- package/dist/lib/etna/profile.js +41 -0
- package/dist/lib/etna/reconcile.js +77 -0
- package/dist/lib/filebeat_installer.js +246 -0
- package/dist/lib/git-sync/core/commit_range.js +37 -0
- package/dist/lib/git-sync/core/engine.js +263 -0
- package/dist/lib/git-sync/core/git_runner.js +137 -0
- package/dist/lib/git-sync/core/operation_state.js +22 -0
- package/dist/lib/git-sync/core/path_normalizer.js +98 -0
- package/dist/lib/git-sync/core/reconciliation_lock.js +103 -0
- package/dist/lib/git-sync/core/repository.js +49 -0
- package/dist/lib/git-sync/core/watch_stream.js +86 -0
- package/dist/lib/git-sync/core/watcher.js +61 -0
- package/dist/lib/git-sync/inbound/applier.js +131 -0
- package/dist/lib/git-sync/inbound/apply_commit.js +225 -0
- package/dist/lib/git-sync/inbound/approval.js +117 -0
- package/dist/lib/git-sync/inbound/content_decision.js +148 -0
- package/dist/lib/git-sync/inbound/download.js +223 -0
- package/dist/lib/git-sync/inbound/event_sync.js +133 -0
- package/dist/lib/git-sync/inbound/index.js +10 -0
- package/dist/lib/git-sync/inbound/notification.js +39 -0
- package/dist/lib/git-sync/inbound/state_fingerprint.js +32 -0
- package/dist/lib/git-sync/inbound/technical_wait.js +39 -0
- package/dist/lib/git-sync/inbound/undo.js +198 -0
- package/dist/lib/git-sync/outbound/extraction.js +107 -0
- package/dist/lib/git-sync/outbound/history_decision.js +112 -0
- package/dist/lib/git-sync/outbound/index.js +26 -0
- package/dist/lib/git-sync/outbound/manifest_builder.js +58 -0
- package/dist/lib/git-sync/outbound/publisher.js +51 -0
- package/dist/lib/git-sync/outbound/queue.js +123 -0
- package/dist/lib/git-sync/outbound/state.js +12 -0
- package/dist/lib/git-sync/outbound/upload_client.js +141 -0
- package/dist/lib/git-sync/recovery/bundle.js +56 -0
- package/dist/lib/git-sync/recovery/crypto.js +114 -0
- package/dist/lib/git-sync/recovery/index.js +3 -0
- package/dist/lib/git-sync/recovery/store.js +55 -0
- package/dist/lib/git-sync/whitelist/index.js +4 -0
- package/dist/lib/git-sync/whitelist/matcher.js +62 -0
- package/dist/lib/git-sync/whitelist/store.js +99 -0
- package/dist/lib/git-sync/whitelist/types.js +46 -0
- package/dist/lib/git-sync/whitelist/validation.js +60 -0
- package/dist/lib/graph/answer.js +171 -0
- package/dist/lib/graph/freshness.js +29 -0
- package/dist/lib/graph/library.js +19 -0
- package/dist/lib/guards.js +106 -0
- package/dist/lib/interactive.js +12 -0
- package/dist/lib/json_output.js +8 -0
- package/dist/lib/lanes/claudeAdapter.js +135 -0
- package/dist/lib/lanes/frames.js +73 -0
- package/dist/lib/lanes/supervisor.js +247 -0
- package/dist/lib/lanes/worktree.js +33 -0
- package/dist/lib/local_store/accepted_uploads.js +49 -0
- package/dist/lib/local_store/bindings.js +163 -0
- package/dist/lib/local_store/commit_origins.js +25 -0
- package/dist/lib/local_store/database.js +71 -0
- package/dist/lib/local_store/device_state.js +72 -0
- package/dist/lib/local_store/filebeat_state.js +133 -0
- package/dist/lib/local_store/handoff_applications.js +166 -0
- package/dist/lib/local_store/history_decisions.js +48 -0
- package/dist/lib/local_store/index.js +14 -0
- package/dist/lib/local_store/migrations.js +240 -0
- package/dist/lib/local_store/paths.js +24 -0
- package/dist/lib/local_store/pending_uploads.js +76 -0
- package/dist/lib/local_store/recovery_bundles.js +66 -0
- package/dist/lib/local_store/sync_vocabulary.js +33 -0
- package/dist/lib/local_store/workspace_events_cache.js +54 -0
- package/dist/lib/loopback.js +78 -0
- package/dist/lib/manifest.js +83 -0
- package/dist/lib/nustack_client.js +547 -0
- package/dist/lib/nustack_store.js +17 -0
- package/dist/lib/onboarding/attach_existing_git.js +65 -0
- package/dist/lib/onboarding/binding.js +91 -0
- package/dist/lib/onboarding/folder_state.js +99 -0
- package/dist/lib/onboarding/git_init.js +10 -0
- package/dist/lib/onboarding/handoff_apply.js +68 -0
- package/dist/lib/onboarding/init_fresh_folder.js +100 -0
- package/dist/lib/onboarding/manifest_writer.js +161 -0
- package/dist/lib/onboarding/web_first_attach.js +133 -0
- package/dist/lib/package_manager.js +23 -0
- package/dist/lib/review/client.js +66 -0
- package/dist/lib/schema_validator.js +24 -0
- package/dist/lib/secure_file.js +10 -0
- package/dist/lib/session_telemetry.js +263 -0
- package/dist/lib/sessions/client.js +48 -0
- package/dist/lib/sessions/outsideSessions.js +54 -0
- package/dist/lib/sse_framing.js +48 -0
- package/dist/lib/telemetry/config_renderer.js +175 -0
- package/dist/lib/telemetry/health_report.js +173 -0
- package/dist/lib/telemetry/provider_paths.js +38 -0
- package/dist/lib/telemetry/redaction_policy.js +90 -0
- package/dist/lib/telemetry/reload.js +84 -0
- package/dist/lib/workspace_context.js +121 -0
- package/dist/spec_schema.json +422 -0
- package/dist/wire/auth.js +1 -0
- package/dist/wire/sync-paths.js +25 -0
- package/dist/wire/telemetry.js +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createInterface } from "node:readline";
|
|
5
|
+
import { ClaudeLane } from "./claudeAdapter.js";
|
|
6
|
+
import { FRAME_VERSION, parseInboundFrame, serializeFrame, } from "./frames.js";
|
|
7
|
+
import { createLaneWorktree, lanesHome } from "./worktree.js";
|
|
8
|
+
function defaultProbeClaude(env) {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
execFile("claude", ["--version"], { env, timeout: 5_000 }, (error, stdout) => {
|
|
11
|
+
if (error)
|
|
12
|
+
resolve({ found: false, version: null });
|
|
13
|
+
else
|
|
14
|
+
resolve({ found: true, version: stdout.trim() });
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export class LaneSupervisor {
|
|
19
|
+
options;
|
|
20
|
+
env;
|
|
21
|
+
lanes = new Map();
|
|
22
|
+
closingLanes = new Set();
|
|
23
|
+
shuttingDown = false;
|
|
24
|
+
resolveDone = null;
|
|
25
|
+
lines = null;
|
|
26
|
+
registryWrites = Promise.resolve();
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.options = options;
|
|
29
|
+
this.env = options.env ?? process.env;
|
|
30
|
+
}
|
|
31
|
+
emit(frame) {
|
|
32
|
+
this.options.output.write(serializeFrame(frame));
|
|
33
|
+
}
|
|
34
|
+
status(laneId, status, message = null) {
|
|
35
|
+
this.emit({ v: FRAME_VERSION, type: "lane.status", laneId, status, message });
|
|
36
|
+
}
|
|
37
|
+
error(laneId, code, message) {
|
|
38
|
+
this.emit({ v: FRAME_VERSION, type: "lane.error", laneId, code, message });
|
|
39
|
+
}
|
|
40
|
+
registryPath() {
|
|
41
|
+
return path.join(lanesHome(this.env), "registry.json");
|
|
42
|
+
}
|
|
43
|
+
async boundWorkspaceOf(projectPath) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(await readFile(path.join(projectPath, ".nustack", "workspace.json"), "utf8"));
|
|
46
|
+
const id = parsed?.workspaceId;
|
|
47
|
+
return typeof id === "string" && id.trim() ? id.trim() : null;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async moveLaneSession(record, to) {
|
|
54
|
+
const client = this.options.sessions;
|
|
55
|
+
if (!client || !record.workspaceId)
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
await client.transitionSession(record.workspaceId, record.laneId, to);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async updateRegistry(mutate) {
|
|
64
|
+
const run = async () => {
|
|
65
|
+
const file = this.registryPath();
|
|
66
|
+
let lanes = [];
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
69
|
+
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.lanes)) {
|
|
70
|
+
lanes = parsed.lanes;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
}
|
|
75
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
76
|
+
await writeFile(file, JSON.stringify({ v: 1, lanes: mutate(lanes) }, null, 2) + "\n", "utf8");
|
|
77
|
+
};
|
|
78
|
+
this.registryWrites = this.registryWrites.then(run, run);
|
|
79
|
+
return this.registryWrites;
|
|
80
|
+
}
|
|
81
|
+
async run() {
|
|
82
|
+
const probe = this.options.probeClaude ?? (() => defaultProbeClaude(this.env));
|
|
83
|
+
const claude = await probe();
|
|
84
|
+
this.emit({ v: FRAME_VERSION, type: "engine.hello", engineVersion: this.options.engineVersion, claude });
|
|
85
|
+
const lines = createInterface({ input: this.options.input });
|
|
86
|
+
this.lines = lines;
|
|
87
|
+
lines.on("line", (line) => {
|
|
88
|
+
if (line.trim() === "")
|
|
89
|
+
return;
|
|
90
|
+
const parsed = parseInboundFrame(line);
|
|
91
|
+
if (!parsed.ok) {
|
|
92
|
+
this.error(null, parsed.error.code, parsed.error.message);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
void this.handle(parsed.frame);
|
|
96
|
+
});
|
|
97
|
+
await new Promise((resolve) => {
|
|
98
|
+
this.resolveDone = resolve;
|
|
99
|
+
lines.on("close", () => void this.shutdown());
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async handle(frame) {
|
|
103
|
+
switch (frame.type) {
|
|
104
|
+
case "engine.shutdown":
|
|
105
|
+
await this.shutdown();
|
|
106
|
+
return;
|
|
107
|
+
case "lane.open":
|
|
108
|
+
await this.openLane(frame.laneId, frame.projectPath, frame.permissionMode, frame.isolated, frame.resumeSessionId);
|
|
109
|
+
return;
|
|
110
|
+
case "lane.turn": {
|
|
111
|
+
const lane = this.lanes.get(frame.laneId);
|
|
112
|
+
if (!lane)
|
|
113
|
+
return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
|
|
114
|
+
lane.adapter.sendTurn(frame.text);
|
|
115
|
+
this.status(frame.laneId, "running");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
case "lane.approval": {
|
|
119
|
+
const lane = this.lanes.get(frame.laneId);
|
|
120
|
+
if (!lane)
|
|
121
|
+
return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
|
|
122
|
+
lane.adapter.respondApproval(frame.requestId, frame.verdict);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
case "lane.interrupt": {
|
|
126
|
+
const lane = this.lanes.get(frame.laneId);
|
|
127
|
+
if (!lane)
|
|
128
|
+
return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
|
|
129
|
+
lane.adapter.interrupt();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
case "lane.close": {
|
|
133
|
+
const lane = this.lanes.get(frame.laneId);
|
|
134
|
+
if (!lane)
|
|
135
|
+
return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
|
|
136
|
+
this.closingLanes.add(frame.laneId);
|
|
137
|
+
lane.adapter.stop();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async openLane(laneId, projectPath, permissionMode, isolated, resumeSessionId) {
|
|
143
|
+
if (this.lanes.has(laneId))
|
|
144
|
+
return this.error(laneId, "LANE_ALREADY_OPEN", `lane ${laneId} is already open`);
|
|
145
|
+
this.status(laneId, "starting");
|
|
146
|
+
const worktree = await createLaneWorktree(projectPath, laneId, this.env, isolated === true);
|
|
147
|
+
const record = {
|
|
148
|
+
laneId,
|
|
149
|
+
projectPath,
|
|
150
|
+
worktreePath: worktree.worktreePath,
|
|
151
|
+
branch: worktree.branch,
|
|
152
|
+
provider: "claude-code",
|
|
153
|
+
providerSessionId: null,
|
|
154
|
+
createdAt: new Date().toISOString(),
|
|
155
|
+
status: "open",
|
|
156
|
+
workspaceId: await this.boundWorkspaceOf(projectPath),
|
|
157
|
+
};
|
|
158
|
+
const adapterOptions = {
|
|
159
|
+
cwd: worktree.worktreePath,
|
|
160
|
+
permissionMode,
|
|
161
|
+
resumeSessionId,
|
|
162
|
+
env: this.env,
|
|
163
|
+
onEvent: (event) => {
|
|
164
|
+
this.emit({ v: FRAME_VERSION, type: "lane.event", laneId, event });
|
|
165
|
+
if (typeof event === "object" && event !== null && event.type === "result") {
|
|
166
|
+
this.status(laneId, "ready");
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
onApprovalRequest: (request) => {
|
|
170
|
+
this.emit({
|
|
171
|
+
v: FRAME_VERSION,
|
|
172
|
+
type: "lane.approvalRequest",
|
|
173
|
+
laneId,
|
|
174
|
+
requestId: request.requestId,
|
|
175
|
+
toolName: request.toolName,
|
|
176
|
+
displayName: request.displayName,
|
|
177
|
+
toolInput: request.toolInput,
|
|
178
|
+
description: request.description,
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
onProtocolError: (code, message) => this.error(laneId, code, message),
|
|
182
|
+
onSessionId: (sessionId) => {
|
|
183
|
+
record.providerSessionId = sessionId;
|
|
184
|
+
void this.updateRegistry((lanes) => lanes.map((row) => (row.laneId === laneId ? { ...row, providerSessionId: sessionId } : row)));
|
|
185
|
+
const client = this.options.sessions;
|
|
186
|
+
if (client && record.workspaceId) {
|
|
187
|
+
void client.setVendorSessionId(record.workspaceId, laneId, sessionId).catch(() => undefined);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
onExit: (code) => {
|
|
191
|
+
const wasExpected = this.closingLanes.delete(laneId) || this.shuttingDown;
|
|
192
|
+
this.lanes.delete(laneId);
|
|
193
|
+
if (!wasExpected)
|
|
194
|
+
this.error(laneId, "ADAPTER_FAILED", `claude exited unexpectedly (code ${String(code)})`);
|
|
195
|
+
this.emit({ v: FRAME_VERSION, type: "lane.closed", laneId });
|
|
196
|
+
void this.updateRegistry((lanes) => lanes.map((row) => (row.laneId === laneId ? { ...row, status: "closed" } : row)));
|
|
197
|
+
void this.moveLaneSession(record, wasExpected ? "done" : "failed");
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
const create = this.options.createAdapter ?? ((opts) => new ClaudeLane(opts));
|
|
201
|
+
let adapter;
|
|
202
|
+
try {
|
|
203
|
+
adapter = create(adapterOptions);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
return this.error(laneId, "ADAPTER_FAILED", error instanceof Error ? error.message : String(error));
|
|
207
|
+
}
|
|
208
|
+
this.lanes.set(laneId, { adapter, record });
|
|
209
|
+
await this.updateRegistry((lanes) => [...lanes.filter((row) => row.laneId !== laneId), record]);
|
|
210
|
+
if (this.options.sessions && record.workspaceId) {
|
|
211
|
+
try {
|
|
212
|
+
await this.options.sessions.createSession(record.workspaceId, {
|
|
213
|
+
id: laneId,
|
|
214
|
+
startedBy: "person",
|
|
215
|
+
substrate: "device",
|
|
216
|
+
mode: "synchronous",
|
|
217
|
+
origin: "inside",
|
|
218
|
+
vendor: "claude_code",
|
|
219
|
+
renderer: "claude_transcript",
|
|
220
|
+
status: "created",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
this.emit({
|
|
227
|
+
v: FRAME_VERSION,
|
|
228
|
+
type: "lane.opened",
|
|
229
|
+
laneId,
|
|
230
|
+
projectPath,
|
|
231
|
+
worktreePath: worktree.worktreePath,
|
|
232
|
+
branch: worktree.branch,
|
|
233
|
+
isolated: worktree.isolated,
|
|
234
|
+
});
|
|
235
|
+
this.status(laneId, "ready");
|
|
236
|
+
}
|
|
237
|
+
async shutdown() {
|
|
238
|
+
if (this.shuttingDown)
|
|
239
|
+
return;
|
|
240
|
+
this.shuttingDown = true;
|
|
241
|
+
for (const [, lane] of this.lanes)
|
|
242
|
+
lane.adapter.stop();
|
|
243
|
+
this.lanes.clear();
|
|
244
|
+
this.lines?.close();
|
|
245
|
+
this.resolveDone?.();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
export const LANE_BRANCH_PREFIX = "nustack/lane-";
|
|
6
|
+
export function lanesHome(env = process.env) {
|
|
7
|
+
const override = env.NUSTACK_LANES_HOME;
|
|
8
|
+
if (override && override !== "")
|
|
9
|
+
return override;
|
|
10
|
+
return path.join(os.homedir(), ".nustack", "lanes");
|
|
11
|
+
}
|
|
12
|
+
function git(args, cwd) {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
execFile("git", args, { cwd, timeout: 15_000 }, (error, stdout) => {
|
|
15
|
+
resolve(error ? null : stdout.trim());
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export async function createLaneWorktree(projectPath, laneId, env = process.env, isolate = false) {
|
|
20
|
+
const inPlace = { worktreePath: projectPath, branch: null, isolated: false };
|
|
21
|
+
if (!isolate)
|
|
22
|
+
return inPlace;
|
|
23
|
+
const isRepo = await git(["rev-parse", "--is-inside-work-tree"], projectPath);
|
|
24
|
+
if (isRepo !== "true")
|
|
25
|
+
return inPlace;
|
|
26
|
+
const branch = `${LANE_BRANCH_PREFIX}${laneId}`;
|
|
27
|
+
const worktreePath = path.join(lanesHome(env), "worktrees", laneId);
|
|
28
|
+
await mkdir(path.dirname(worktreePath), { recursive: true });
|
|
29
|
+
const added = await git(["worktree", "add", worktreePath, "-b", branch], projectPath);
|
|
30
|
+
if (added === null)
|
|
31
|
+
return inPlace;
|
|
32
|
+
return { worktreePath, branch, isolated: true };
|
|
33
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function mapRow(row) {
|
|
2
|
+
return {
|
|
3
|
+
bindingId: row.binding_id,
|
|
4
|
+
commitId: row.commit_id,
|
|
5
|
+
handoffId: row.handoff_id,
|
|
6
|
+
workspaceSequence: row.workspace_sequence,
|
|
7
|
+
manifestSha256: row.manifest_sha256,
|
|
8
|
+
createdAtMs: row.created_at_ms,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function recordAcceptedUpload(store, input) {
|
|
12
|
+
store.db
|
|
13
|
+
.prepare(`INSERT INTO local_accepted_uploads (
|
|
14
|
+
binding_id, commit_id, handoff_id, workspace_sequence, manifest_sha256, created_at_ms
|
|
15
|
+
) VALUES (
|
|
16
|
+
:binding_id, :commit_id, :handoff_id, :workspace_sequence, :manifest_sha256, :created_at_ms
|
|
17
|
+
)
|
|
18
|
+
ON CONFLICT(binding_id, commit_id) DO NOTHING`)
|
|
19
|
+
.run({
|
|
20
|
+
binding_id: input.bindingId,
|
|
21
|
+
commit_id: input.commitId,
|
|
22
|
+
handoff_id: input.handoffId,
|
|
23
|
+
workspace_sequence: input.workspaceSequence,
|
|
24
|
+
manifest_sha256: input.manifestSha256,
|
|
25
|
+
created_at_ms: Date.now(),
|
|
26
|
+
});
|
|
27
|
+
return getAcceptedUpload(store, input.bindingId, input.commitId);
|
|
28
|
+
}
|
|
29
|
+
export function getAcceptedUpload(store, bindingId, commitId) {
|
|
30
|
+
const row = store.db
|
|
31
|
+
.prepare("SELECT * FROM local_accepted_uploads WHERE binding_id = :binding_id AND commit_id = :commit_id")
|
|
32
|
+
.get({ binding_id: bindingId, commit_id: commitId });
|
|
33
|
+
return row ? mapRow(row) : null;
|
|
34
|
+
}
|
|
35
|
+
export function getAcceptedUploadByHandoff(store, handoffId) {
|
|
36
|
+
const row = store.db
|
|
37
|
+
.prepare("SELECT * FROM local_accepted_uploads WHERE handoff_id = :handoff_id LIMIT 1")
|
|
38
|
+
.get({ handoff_id: handoffId });
|
|
39
|
+
return row ? mapRow(row) : null;
|
|
40
|
+
}
|
|
41
|
+
export function latestAcceptedUpload(store, bindingId) {
|
|
42
|
+
const row = store.db
|
|
43
|
+
.prepare(`SELECT * FROM local_accepted_uploads
|
|
44
|
+
WHERE binding_id = :binding_id
|
|
45
|
+
ORDER BY workspace_sequence DESC, created_at_ms DESC
|
|
46
|
+
LIMIT 1`)
|
|
47
|
+
.get({ binding_id: bindingId });
|
|
48
|
+
return row ? mapRow(row) : null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { withTransaction } from "./database.js";
|
|
2
|
+
import { LIVE_BINDING_STATUSES } from "./sync_vocabulary.js";
|
|
3
|
+
function mapRow(row) {
|
|
4
|
+
return {
|
|
5
|
+
id: row.id,
|
|
6
|
+
workspaceId: row.workspace_id,
|
|
7
|
+
organizationId: row.organization_id,
|
|
8
|
+
repositoryRoot: row.repository_root,
|
|
9
|
+
gitDir: row.git_dir,
|
|
10
|
+
gitCommonDir: row.git_common_dir,
|
|
11
|
+
repositoryFingerprint: row.repository_fingerprint,
|
|
12
|
+
boundBranch: row.bound_branch,
|
|
13
|
+
lastProcessedCommitId: row.last_processed_commit_id ?? null,
|
|
14
|
+
lastAppliedWorkspaceSequence: row.last_applied_workspace_sequence,
|
|
15
|
+
whitelistManifestId: row.whitelist_manifest_id ?? null,
|
|
16
|
+
whitelistManifestSha256: row.whitelist_manifest_sha256 ?? null,
|
|
17
|
+
whitelistCachePath: row.whitelist_cache_path ?? null,
|
|
18
|
+
etnaProfileJson: row.etna_profile_json ?? null,
|
|
19
|
+
status: row.status,
|
|
20
|
+
lastErrorCode: row.last_error_code ?? null,
|
|
21
|
+
lastErrorDetail: row.last_error_detail ?? null,
|
|
22
|
+
telemetryEnabled: row.telemetry_enabled !== 0,
|
|
23
|
+
createdAtMs: row.created_at_ms,
|
|
24
|
+
updatedAtMs: row.updated_at_ms,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function insertBinding(store, input) {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
store.db
|
|
30
|
+
.prepare(`INSERT INTO local_bindings (
|
|
31
|
+
id, workspace_id, organization_id, repository_root, git_dir, git_common_dir,
|
|
32
|
+
repository_fingerprint, bound_branch, last_processed_commit_id,
|
|
33
|
+
last_applied_workspace_sequence, whitelist_manifest_id, whitelist_manifest_sha256,
|
|
34
|
+
whitelist_cache_path, etna_profile_json, status, last_error_code,
|
|
35
|
+
last_error_detail, created_at_ms, updated_at_ms
|
|
36
|
+
) VALUES (
|
|
37
|
+
:id, :workspace_id, :organization_id, :repository_root, :git_dir, :git_common_dir,
|
|
38
|
+
:repository_fingerprint, :bound_branch, :last_processed_commit_id,
|
|
39
|
+
:last_applied_workspace_sequence, :whitelist_manifest_id, :whitelist_manifest_sha256,
|
|
40
|
+
:whitelist_cache_path, :etna_profile_json, :status, NULL,
|
|
41
|
+
NULL, :created_at_ms, :updated_at_ms
|
|
42
|
+
)`)
|
|
43
|
+
.run({
|
|
44
|
+
id: input.id,
|
|
45
|
+
workspace_id: input.workspaceId,
|
|
46
|
+
organization_id: input.organizationId,
|
|
47
|
+
repository_root: input.repositoryRoot,
|
|
48
|
+
git_dir: input.gitDir,
|
|
49
|
+
git_common_dir: input.gitCommonDir,
|
|
50
|
+
repository_fingerprint: input.repositoryFingerprint,
|
|
51
|
+
bound_branch: input.boundBranch,
|
|
52
|
+
last_processed_commit_id: input.lastProcessedCommitId ?? null,
|
|
53
|
+
last_applied_workspace_sequence: input.lastAppliedWorkspaceSequence ?? 0,
|
|
54
|
+
whitelist_manifest_id: input.whitelistManifestId ?? null,
|
|
55
|
+
whitelist_manifest_sha256: input.whitelistManifestSha256 ?? null,
|
|
56
|
+
whitelist_cache_path: input.whitelistCachePath ?? null,
|
|
57
|
+
etna_profile_json: input.etnaProfileJson ?? null,
|
|
58
|
+
status: input.status ?? "creating",
|
|
59
|
+
created_at_ms: now,
|
|
60
|
+
updated_at_ms: now,
|
|
61
|
+
});
|
|
62
|
+
return getBindingById(store, input.id);
|
|
63
|
+
}
|
|
64
|
+
export function getBindingById(store, bindingId) {
|
|
65
|
+
const row = store.db.prepare("SELECT * FROM local_bindings WHERE id = :id").get({
|
|
66
|
+
id: bindingId,
|
|
67
|
+
});
|
|
68
|
+
return row ? mapRow(row) : null;
|
|
69
|
+
}
|
|
70
|
+
export function getBindingByRepositoryRoot(store, repositoryRoot) {
|
|
71
|
+
const row = store.db
|
|
72
|
+
.prepare(`SELECT * FROM local_bindings
|
|
73
|
+
WHERE repository_root = :root
|
|
74
|
+
AND status IN ('creating','active','paused')`)
|
|
75
|
+
.get({ root: repositoryRoot });
|
|
76
|
+
return row ? mapRow(row) : null;
|
|
77
|
+
}
|
|
78
|
+
export function listBindings(store, statusFilter) {
|
|
79
|
+
if (statusFilter === undefined) {
|
|
80
|
+
const rows = store.db
|
|
81
|
+
.prepare("SELECT * FROM local_bindings ORDER BY created_at_ms ASC")
|
|
82
|
+
.all();
|
|
83
|
+
return rows.map(mapRow);
|
|
84
|
+
}
|
|
85
|
+
const statuses = Array.isArray(statusFilter) ? statusFilter : [statusFilter];
|
|
86
|
+
const placeholders = statuses.map((_, i) => `:s${i}`).join(", ");
|
|
87
|
+
const params = Object.fromEntries(statuses.map((s, i) => [`s${i}`, s]));
|
|
88
|
+
const rows = store.db
|
|
89
|
+
.prepare(`SELECT * FROM local_bindings WHERE status IN (${placeholders}) ORDER BY created_at_ms ASC`)
|
|
90
|
+
.all(params);
|
|
91
|
+
return rows.map(mapRow);
|
|
92
|
+
}
|
|
93
|
+
export function listActiveBindings(store) {
|
|
94
|
+
return listBindings(store, LIVE_BINDING_STATUSES);
|
|
95
|
+
}
|
|
96
|
+
export function updateBindingCursor(store, bindingId, patch) {
|
|
97
|
+
return withTransaction(store, () => {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
if (Object.prototype.hasOwnProperty.call(patch, "lastProcessedCommitId")) {
|
|
100
|
+
store.db
|
|
101
|
+
.prepare(`UPDATE local_bindings
|
|
102
|
+
SET last_processed_commit_id = :commit, updated_at_ms = :now
|
|
103
|
+
WHERE id = :id`)
|
|
104
|
+
.run({ commit: patch.lastProcessedCommitId ?? null, now, id: bindingId });
|
|
105
|
+
}
|
|
106
|
+
if (patch.lastAppliedWorkspaceSequence !== undefined) {
|
|
107
|
+
store.db
|
|
108
|
+
.prepare(`UPDATE local_bindings
|
|
109
|
+
SET last_applied_workspace_sequence = :seq, updated_at_ms = :now
|
|
110
|
+
WHERE id = :id`)
|
|
111
|
+
.run({ seq: patch.lastAppliedWorkspaceSequence, now, id: bindingId });
|
|
112
|
+
}
|
|
113
|
+
return getBindingById(store, bindingId);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
export function updateBindingStatus(store, bindingId, status, error) {
|
|
117
|
+
store.db
|
|
118
|
+
.prepare(`UPDATE local_bindings
|
|
119
|
+
SET status = :status, last_error_code = :code, last_error_detail = :detail,
|
|
120
|
+
updated_at_ms = :now
|
|
121
|
+
WHERE id = :id`)
|
|
122
|
+
.run({
|
|
123
|
+
status,
|
|
124
|
+
code: error?.code ?? null,
|
|
125
|
+
detail: error?.detail ?? null,
|
|
126
|
+
now: Date.now(),
|
|
127
|
+
id: bindingId,
|
|
128
|
+
});
|
|
129
|
+
return getBindingById(store, bindingId);
|
|
130
|
+
}
|
|
131
|
+
export function setBindingTelemetry(store, bindingId, enabled) {
|
|
132
|
+
store.db
|
|
133
|
+
.prepare(`UPDATE local_bindings
|
|
134
|
+
SET telemetry_enabled = :enabled, updated_at_ms = :now
|
|
135
|
+
WHERE id = :id`)
|
|
136
|
+
.run({ enabled: enabled ? 1 : 0, now: Date.now(), id: bindingId });
|
|
137
|
+
return getBindingById(store, bindingId);
|
|
138
|
+
}
|
|
139
|
+
export function updateBindingEtnaProfile(store, bindingId, etnaProfileJson) {
|
|
140
|
+
store.db
|
|
141
|
+
.prepare(`UPDATE local_bindings
|
|
142
|
+
SET etna_profile_json = :profile, updated_at_ms = :now
|
|
143
|
+
WHERE id = :id`)
|
|
144
|
+
.run({ profile: etnaProfileJson, now: Date.now(), id: bindingId });
|
|
145
|
+
return getBindingById(store, bindingId);
|
|
146
|
+
}
|
|
147
|
+
export function updateBindingWhitelistCache(store, bindingId, patch) {
|
|
148
|
+
store.db
|
|
149
|
+
.prepare(`UPDATE local_bindings
|
|
150
|
+
SET whitelist_manifest_id = :manifestId,
|
|
151
|
+
whitelist_manifest_sha256 = :manifestSha,
|
|
152
|
+
whitelist_cache_path = :cachePath,
|
|
153
|
+
updated_at_ms = :now
|
|
154
|
+
WHERE id = :id`)
|
|
155
|
+
.run({
|
|
156
|
+
manifestId: patch.whitelistManifestId ?? null,
|
|
157
|
+
manifestSha: patch.whitelistManifestSha256 ?? null,
|
|
158
|
+
cachePath: patch.whitelistCachePath ?? null,
|
|
159
|
+
now: Date.now(),
|
|
160
|
+
id: bindingId,
|
|
161
|
+
});
|
|
162
|
+
return getBindingById(store, bindingId);
|
|
163
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function recordInboundCommit(store, input) {
|
|
2
|
+
store.db
|
|
3
|
+
.prepare(`INSERT INTO local_commit_origins (binding_id, commit_id, handoff_id, origin, created_at_ms)
|
|
4
|
+
VALUES (:binding_id, :commit_id, :handoff_id, 'cloud_handoff', :created_at_ms)
|
|
5
|
+
ON CONFLICT(binding_id, commit_id) DO NOTHING`)
|
|
6
|
+
.run({
|
|
7
|
+
binding_id: input.bindingId,
|
|
8
|
+
commit_id: input.commitId,
|
|
9
|
+
handoff_id: input.handoffId,
|
|
10
|
+
created_at_ms: Date.now(),
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function isCloudOriginCommit(store, bindingId, commitId) {
|
|
14
|
+
const row = store.db
|
|
15
|
+
.prepare(`SELECT 1 AS present FROM local_commit_origins
|
|
16
|
+
WHERE binding_id = :binding_id AND commit_id = :commit_id`)
|
|
17
|
+
.get({ binding_id: bindingId, commit_id: commitId });
|
|
18
|
+
return row !== undefined;
|
|
19
|
+
}
|
|
20
|
+
export function hasCloudOriginForHandoff(store, handoffId) {
|
|
21
|
+
const row = store.db
|
|
22
|
+
.prepare(`SELECT 1 AS present FROM local_commit_origins WHERE handoff_id = :handoff_id`)
|
|
23
|
+
.get({ handoff_id: handoffId });
|
|
24
|
+
return row !== undefined;
|
|
25
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { chmod } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { mkdirSecure } from "../secure_file.js";
|
|
4
|
+
import { stateDbPath } from "./paths.js";
|
|
5
|
+
import { migrateToLatest } from "./migrations.js";
|
|
6
|
+
let sqliteModulePromise;
|
|
7
|
+
async function loadSqlite() {
|
|
8
|
+
if (!sqliteModulePromise) {
|
|
9
|
+
sqliteModulePromise = importSqliteQuietly();
|
|
10
|
+
}
|
|
11
|
+
return sqliteModulePromise;
|
|
12
|
+
}
|
|
13
|
+
async function importSqliteQuietly() {
|
|
14
|
+
return withSuppressedSqliteWarning(() => import("node:sqlite"));
|
|
15
|
+
}
|
|
16
|
+
async function withSuppressedSqliteWarning(fn) {
|
|
17
|
+
const original = process.emitWarning;
|
|
18
|
+
const filtered = (warning, ...args) => {
|
|
19
|
+
const name = warning instanceof Error
|
|
20
|
+
? warning.name
|
|
21
|
+
: typeof args[0] === "string"
|
|
22
|
+
? args[0]
|
|
23
|
+
: args[0]?.type;
|
|
24
|
+
const message = warning instanceof Error ? warning.message : String(warning);
|
|
25
|
+
if (name === "ExperimentalWarning" && /sqlite/i.test(message))
|
|
26
|
+
return;
|
|
27
|
+
original(warning, ...args);
|
|
28
|
+
};
|
|
29
|
+
process.emitWarning = filtered;
|
|
30
|
+
try {
|
|
31
|
+
return await fn();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
process.emitWarning = original;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function openLocalStore(options = {}) {
|
|
38
|
+
const dbFile = options.dbPath ?? stateDbPath();
|
|
39
|
+
await mkdirSecure(path.dirname(dbFile));
|
|
40
|
+
const db = await withSuppressedSqliteWarning(async () => {
|
|
41
|
+
const { DatabaseSync } = await loadSqlite();
|
|
42
|
+
return new DatabaseSync(dbFile);
|
|
43
|
+
});
|
|
44
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
45
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
46
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
47
|
+
migrateToLatest(db);
|
|
48
|
+
await chmod(dbFile, 0o600).catch(() => { });
|
|
49
|
+
return {
|
|
50
|
+
db,
|
|
51
|
+
close() {
|
|
52
|
+
db.close();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function withTransaction(store, fn) {
|
|
57
|
+
const { db } = store;
|
|
58
|
+
db.exec("BEGIN IMMEDIATE");
|
|
59
|
+
try {
|
|
60
|
+
const result = fn(db);
|
|
61
|
+
db.exec("COMMIT");
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
db.exec("ROLLBACK");
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export function bindNullable(value) {
|
|
70
|
+
return value ?? null;
|
|
71
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { LATEST_SCHEMA_VERSION } from "./migrations.js";
|
|
2
|
+
import { withTransaction } from "./database.js";
|
|
3
|
+
function mapRow(row) {
|
|
4
|
+
return {
|
|
5
|
+
deviceRef: row.device_ref ?? null,
|
|
6
|
+
apiBaseUrl: row.api_base_url ?? null,
|
|
7
|
+
activeUserId: row.active_user_id ?? null,
|
|
8
|
+
activeUserEmail: row.active_user_email ?? null,
|
|
9
|
+
activeOrganizationId: row.active_organization_id ?? null,
|
|
10
|
+
keychainService: row.keychain_service ?? null,
|
|
11
|
+
keychainAccount: row.keychain_account ?? null,
|
|
12
|
+
schemaVersion: row.schema_version,
|
|
13
|
+
lastAuthRefreshAtMs: row.last_auth_refresh_at_ms ?? null,
|
|
14
|
+
createdAtMs: row.created_at_ms,
|
|
15
|
+
updatedAtMs: row.updated_at_ms,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function readDeviceState(store) {
|
|
19
|
+
const row = store.db.prepare("SELECT * FROM local_device_state WHERE id = 1").get();
|
|
20
|
+
return row ? mapRow(row) : null;
|
|
21
|
+
}
|
|
22
|
+
export function upsertDeviceState(store, patch) {
|
|
23
|
+
return withTransaction(store, (db) => {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const current = readDeviceState(store);
|
|
26
|
+
const pick = (key, fallback) => Object.prototype.hasOwnProperty.call(patch, key) ? (patch[key] ?? null) : fallback;
|
|
27
|
+
const merged = {
|
|
28
|
+
deviceRef: pick("deviceRef", current?.deviceRef ?? null),
|
|
29
|
+
apiBaseUrl: pick("apiBaseUrl", current?.apiBaseUrl ?? null),
|
|
30
|
+
activeUserId: pick("activeUserId", current?.activeUserId ?? null),
|
|
31
|
+
activeUserEmail: pick("activeUserEmail", current?.activeUserEmail ?? null),
|
|
32
|
+
activeOrganizationId: pick("activeOrganizationId", current?.activeOrganizationId ?? null),
|
|
33
|
+
keychainService: pick("keychainService", current?.keychainService ?? null),
|
|
34
|
+
keychainAccount: pick("keychainAccount", current?.keychainAccount ?? null),
|
|
35
|
+
lastAuthRefreshAtMs: pick("lastAuthRefreshAtMs", current?.lastAuthRefreshAtMs ?? null),
|
|
36
|
+
createdAtMs: current?.createdAtMs ?? now,
|
|
37
|
+
};
|
|
38
|
+
db.prepare(`INSERT INTO local_device_state (
|
|
39
|
+
id, device_ref, api_base_url, active_user_id, active_user_email,
|
|
40
|
+
active_organization_id, keychain_service, keychain_account, schema_version,
|
|
41
|
+
last_auth_refresh_at_ms, created_at_ms, updated_at_ms
|
|
42
|
+
) VALUES (
|
|
43
|
+
1, :device_ref, :api_base_url, :active_user_id, :active_user_email,
|
|
44
|
+
:active_organization_id, :keychain_service, :keychain_account, :schema_version,
|
|
45
|
+
:last_auth_refresh_at_ms, :created_at_ms, :updated_at_ms
|
|
46
|
+
)
|
|
47
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
48
|
+
device_ref = excluded.device_ref,
|
|
49
|
+
api_base_url = excluded.api_base_url,
|
|
50
|
+
active_user_id = excluded.active_user_id,
|
|
51
|
+
active_user_email = excluded.active_user_email,
|
|
52
|
+
active_organization_id = excluded.active_organization_id,
|
|
53
|
+
keychain_service = excluded.keychain_service,
|
|
54
|
+
keychain_account = excluded.keychain_account,
|
|
55
|
+
schema_version = excluded.schema_version,
|
|
56
|
+
last_auth_refresh_at_ms = excluded.last_auth_refresh_at_ms,
|
|
57
|
+
updated_at_ms = excluded.updated_at_ms`).run({
|
|
58
|
+
device_ref: merged.deviceRef,
|
|
59
|
+
api_base_url: merged.apiBaseUrl,
|
|
60
|
+
active_user_id: merged.activeUserId,
|
|
61
|
+
active_user_email: merged.activeUserEmail,
|
|
62
|
+
active_organization_id: merged.activeOrganizationId,
|
|
63
|
+
keychain_service: merged.keychainService,
|
|
64
|
+
keychain_account: merged.keychainAccount,
|
|
65
|
+
schema_version: LATEST_SCHEMA_VERSION,
|
|
66
|
+
last_auth_refresh_at_ms: merged.lastAuthRefreshAtMs,
|
|
67
|
+
created_at_ms: merged.createdAtMs,
|
|
68
|
+
updated_at_ms: now,
|
|
69
|
+
});
|
|
70
|
+
return readDeviceState(store);
|
|
71
|
+
});
|
|
72
|
+
}
|