@nurix/nustack 0.24.0 → 0.25.2
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/commands/desktopAgent.js +1 -1
- package/dist/commands/doctor.js +8 -11
- package/dist/commands/obligationList.js +23 -49
- package/dist/commands/records.js +6 -88
- package/dist/commands/store.js +199 -0
- package/dist/commands/sync.js +2 -2
- package/dist/commands/toolkit.js +2 -2
- package/dist/lib/context/register.js +1 -1
- package/dist/lib/desktop-agents/checkout.js +1 -1
- package/dist/lib/desktop-agents/claudeRun.js +4 -4
- package/dist/lib/etna/reconcile.js +4 -4
- package/dist/lib/graph/library.js +1 -1
- package/dist/lib/harness/hookCheck.js +2 -8
- package/dist/lib/harness/storeWaivers.js +1 -4
- package/dist/lib/local_store/database.js +12 -92
- package/dist/lib/local_store/device_state.js +2 -2
- package/dist/lib/local_store/index.js +2 -2
- package/dist/lib/local_store/paths.js +0 -6
- package/dist/lib/local_store/routing.js +8 -44
- package/dist/lib/onboarding/folder_state.js +2 -2
- package/dist/lib/onboarding/init_fresh_folder.js +1 -1
- package/dist/lib/onboarding/manifest_writer.js +6 -6
- package/dist/lib/origin_guard.js +1 -1
- package/dist/lib/project_context.js +5 -5
- package/dist/lib/project_contract.js +2 -2
- package/dist/lib/records/drain.js +3 -153
- package/dist/lib/records/kinds.js +16 -0
- package/dist/lib/records/recordInput.js +1 -1
- package/dist/lib/records/recordsTable.js +119 -161
- package/dist/lib/records/refusalLog.js +7 -7
- package/dist/lib/sessions/brief.js +1 -1
- package/dist/lib/sessions/sessionEvents.js +5 -49
- package/dist/lib/store/open.js +136 -0
- package/dist/lib/store/wipe.js +34 -0
- package/dist/lib/sync/apply.js +113 -0
- package/dist/lib/sync/client.js +327 -0
- package/dist/lib/sync/endpoint.js +26 -0
- package/dist/lib/sync/manifest.js +80 -0
- package/dist/lib/sync/outbox.js +104 -0
- package/dist/machine.sql +934 -0
- package/dist/program.js +8 -6
- package/dist/records.schema.json +4 -0
- package/dist/spec_schema.json +5 -5
- package/dist/vendor/harness/rules/docsRoot.js +2 -2
- package/dist/vendor/harness/rules/index.js +1 -1
- package/package.json +2 -2
- package/dist/lib/local_store/migrations.js +0 -399
- package/dist/lib/records/door.js +0 -79
|
@@ -86,7 +86,7 @@ async function runAccepted(request) {
|
|
|
86
86
|
const outcome = await runClaude(request, claudeArgs(request, inputs, modelEnv), modelEnv, worktreePath, composePrompt(request));
|
|
87
87
|
if (request.sessionId !== null) {
|
|
88
88
|
const synced = await syncRecords(env, worktreePath);
|
|
89
|
-
log(`
|
|
89
|
+
log(`store sync exited ${synced.code ?? "unspawned"}`);
|
|
90
90
|
}
|
|
91
91
|
const shared = { vendorSessionId: outcome.vendorSessionId, costCents: outcome.costCents, engineCheck: outcome.engineCheck, mount: mounted.mount };
|
|
92
92
|
if (outcome.reason) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -14,7 +14,6 @@ import { readDeviceState } from "../lib/local_store/device_state.js";
|
|
|
14
14
|
import { getBindingByRepositoryRoot, listActiveBindings } from "../lib/local_store/bindings.js";
|
|
15
15
|
import { listApplicationsForBinding } from "../lib/local_store/handoff_applications.js";
|
|
16
16
|
import { readFilebeatState, listActivityBindings } from "../lib/local_store/filebeat_state.js";
|
|
17
|
-
import { LATEST_SCHEMA_VERSION } from "../lib/local_store/migrations.js";
|
|
18
17
|
import { resolveRepository, readSymbolicBranch, readHeadCommit } from "../lib/git-sync/core/repository.js";
|
|
19
18
|
import { readGitOperationState } from "../lib/git-sync/core/operation_state.js";
|
|
20
19
|
import { NotARepositoryError } from "../lib/git-sync/core/git_runner.js";
|
|
@@ -72,23 +71,21 @@ export async function runDoctor(options) {
|
|
|
72
71
|
add("identity", row.check, row.status, row.detail);
|
|
73
72
|
add("api", "NuStack reachability", ...(await probeReachability(baseUrl)));
|
|
74
73
|
let store = null;
|
|
74
|
+
let storeError = "the Device state database could not be opened";
|
|
75
75
|
try {
|
|
76
76
|
store = await openLocalStore();
|
|
77
77
|
}
|
|
78
|
-
catch {
|
|
78
|
+
catch (error) {
|
|
79
79
|
store = null;
|
|
80
|
+
if (error instanceof Error)
|
|
81
|
+
storeError = error.message;
|
|
80
82
|
}
|
|
81
83
|
if (!store) {
|
|
82
|
-
add("state", "Local store", "error",
|
|
84
|
+
add("state", "Local store", "error", storeError);
|
|
83
85
|
}
|
|
84
86
|
else {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
add("state", "Local store schema", "info", "no Device state yet — sign in to initialize");
|
|
88
|
-
else if (schema >= LATEST_SCHEMA_VERSION)
|
|
89
|
-
add("state", "Local store schema", "ok", `v${schema}`);
|
|
90
|
-
else
|
|
91
|
-
add("state", "Local store schema", "warn", `v${schema} behind v${LATEST_SCHEMA_VERSION} — the next command migrates it`);
|
|
87
|
+
const stamp = store.db.prepare("PRAGMA user_version").get().user_version;
|
|
88
|
+
add("state", "Local store schema", "ok", `v${stamp}`);
|
|
92
89
|
}
|
|
93
90
|
const repository = resolveRepositoryOrNull(cwd);
|
|
94
91
|
if (!repository) {
|
|
@@ -176,7 +173,7 @@ async function fetchCloudProfile(baseUrl, accessToken, projectId) {
|
|
|
176
173
|
async function addManifestRows(add, root, cloud) {
|
|
177
174
|
const manifests = await readProjectManifests(root);
|
|
178
175
|
if (!manifests.projectManifest && !manifests.serviceToolkit) {
|
|
179
|
-
add("manifests", "Project binding markers", "info", "no `.nustack/
|
|
176
|
+
add("manifests", "Project binding markers", "info", "no `.nustack/project.json` or Service toolkit block — repository not attached");
|
|
180
177
|
return;
|
|
181
178
|
}
|
|
182
179
|
const diagnosis = diagnoseManifests(cloud ? { name: cloud.name, version: cloud.version } : null, manifests.projectManifest?.toolkit ?? null, manifests.serviceToolkit);
|
|
@@ -1,42 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { openLocalStore } from "../lib/local_store/database.js";
|
|
2
2
|
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
3
3
|
import { resolveProjectId } from "../lib/project_context.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
function lineOf(
|
|
8
|
-
|
|
9
|
-
return null;
|
|
10
|
-
const payload = (item.payload ?? {});
|
|
4
|
+
import { listRecords } from "../lib/records/recordsTable.js";
|
|
5
|
+
import { pullAll } from "../lib/sync/client.js";
|
|
6
|
+
const PULL_TIMEOUT_MS = 10_000;
|
|
7
|
+
function lineOf(naturalKey, payload) {
|
|
8
|
+
const fields = (payload ?? {});
|
|
11
9
|
return {
|
|
12
|
-
naturalKey
|
|
13
|
-
state: typeof
|
|
14
|
-
verdict: typeof
|
|
10
|
+
naturalKey,
|
|
11
|
+
state: typeof fields.state === "string" ? fields.state : "unknown",
|
|
12
|
+
verdict: typeof fields.outcome?.verdict === "string" ? fields.outcome.verdict : null,
|
|
15
13
|
};
|
|
16
14
|
}
|
|
17
|
-
async function fetchAll(root, globals, projectId, plan) {
|
|
18
|
-
const lines = [];
|
|
19
|
-
let cursor = null;
|
|
20
|
-
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
21
|
-
const query = `/records?kind=obligation&planAnchor=${encodeURIComponent(plan)}&limit=100${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ""}`;
|
|
22
|
-
const answered = await getFromDoor(root, globals, projectId, query, PAGE_TIMEOUT_MS);
|
|
23
|
-
const items = answered?.data?.items;
|
|
24
|
-
if (!answered || !Array.isArray(items))
|
|
25
|
-
return null;
|
|
26
|
-
for (const item of items) {
|
|
27
|
-
const line = lineOf(item);
|
|
28
|
-
if (line)
|
|
29
|
-
lines.push(line);
|
|
30
|
-
}
|
|
31
|
-
cursor = answered.meta?.nextCursor ?? null;
|
|
32
|
-
if (!cursor)
|
|
33
|
-
return lines;
|
|
34
|
-
}
|
|
35
|
-
return lines;
|
|
36
|
-
}
|
|
37
15
|
export async function runObligationList(options, globals) {
|
|
38
|
-
const
|
|
39
|
-
const project = await resolveProjectId(root, options.project ? { project: options.project } : {});
|
|
16
|
+
const project = await resolveProjectId(process.cwd(), options.project ? { project: options.project } : {});
|
|
40
17
|
if (!project.ok) {
|
|
41
18
|
if (options.json)
|
|
42
19
|
printJsonError("obligation list", { code: "NO_PROJECT", message: project.message });
|
|
@@ -46,28 +23,25 @@ export async function runObligationList(options, globals) {
|
|
|
46
23
|
}
|
|
47
24
|
return;
|
|
48
25
|
}
|
|
49
|
-
const
|
|
50
|
-
const fetched = await fetchAll(root, globals, project.projectId, options.plan);
|
|
26
|
+
const store = await openLocalStore();
|
|
51
27
|
let obligations;
|
|
52
|
-
let
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
28
|
+
let pullError;
|
|
29
|
+
try {
|
|
30
|
+
pullError = (await pullAll(store, globals, { timeoutMs: PULL_TIMEOUT_MS })).lastError;
|
|
31
|
+
obligations = listRecords(store, { projectId: project.projectId, kind: "obligation", planAnchor: options.plan })
|
|
32
|
+
.map((record) => lineOf(record.naturalKey, record.payload))
|
|
33
|
+
.sort((a, b) => a.naturalKey.localeCompare(b.naturalKey));
|
|
58
34
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
obligations = cached?.value ?? [];
|
|
62
|
-
fetchedAt = cached?.fetchedAt ?? null;
|
|
63
|
-
source = "cache";
|
|
35
|
+
finally {
|
|
36
|
+
store.close();
|
|
64
37
|
}
|
|
38
|
+
const source = pullError === null ? "synced" : "store";
|
|
65
39
|
if (options.json) {
|
|
66
|
-
printJsonSuccess("obligation list", { plan: options.plan, projectId: project.projectId, source,
|
|
40
|
+
printJsonSuccess("obligation list", { plan: options.plan, projectId: project.projectId, source, pullError, obligations });
|
|
67
41
|
return;
|
|
68
42
|
}
|
|
69
|
-
if (
|
|
70
|
-
process.stderr.write(`obligation list:
|
|
43
|
+
if (pullError !== null)
|
|
44
|
+
process.stderr.write(`obligation list: could not sync (${pullError}); showing the rows as last synced\n`);
|
|
71
45
|
for (const line of obligations)
|
|
72
46
|
process.stdout.write(`${line.naturalKey} ${line.state}${line.verdict ? ` ${line.verdict}` : ""}\n`);
|
|
73
47
|
if (obligations.length === 0)
|
package/dist/commands/records.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { readDesktopSettings } from "../lib/desktop_settings.js";
|
|
3
2
|
import { openLocalStore } from "../lib/local_store/database.js";
|
|
4
|
-
import { stateDbPath } from "../lib/local_store/paths.js";
|
|
5
3
|
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
6
|
-
import { HARNESS_CODES } from "../lib/harness/codes.js";
|
|
7
4
|
import { resolveProjectId } from "../lib/project_context.js";
|
|
8
|
-
import { isEmitKind } from "../lib/records/
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
5
|
+
import { isEmitKind } from "../lib/records/kinds.js";
|
|
6
|
+
import { startDetachedDrain } from "../lib/records/drain.js";
|
|
7
|
+
import { SESSION_ID_VARIABLE, readVariable, recordCredentialOf } from "../lib/records/environment.js";
|
|
11
8
|
import { UNKNOWN_KIND_MESSAGE, assembleRecord, parseRowsText, translateRow, } from "../lib/records/recordInput.js";
|
|
12
|
-
import { AgentOwnedError, OBLIGATION_AGENT_OWNED, countRecords,
|
|
9
|
+
import { AgentOwnedError, OBLIGATION_AGENT_OWNED, countRecords, listRecords, releaseUnboundRows, writeRecords, } from "../lib/records/recordsTable.js";
|
|
13
10
|
import { logRefusals } from "../lib/records/refusalLog.js";
|
|
14
|
-
import { drainSessionLegs } from "../lib/sessions/sessionEvents.js";
|
|
15
11
|
async function readStdin() {
|
|
16
12
|
if (process.stdin.isTTY)
|
|
17
13
|
return "";
|
|
@@ -175,65 +171,10 @@ export async function runRecordsEmit(options, globals) {
|
|
|
175
171
|
json: options.json === true,
|
|
176
172
|
}, globals);
|
|
177
173
|
}
|
|
178
|
-
async function drainAs(store, globals, isChild) {
|
|
179
|
-
if (!isChild) {
|
|
180
|
-
const drained = await drainRecords(store, globals, { isForeground: true });
|
|
181
|
-
await drainSessionLegs(store, globals);
|
|
182
|
-
return drained;
|
|
183
|
-
}
|
|
184
|
-
if (!acquireDrainLock())
|
|
185
|
-
return null;
|
|
186
|
-
try {
|
|
187
|
-
const drained = await drainRecords(store, globals, { onPass: () => refreshDrainLock() });
|
|
188
|
-
refreshDrainLock();
|
|
189
|
-
await drainSessionLegs(store, globals);
|
|
190
|
-
return drained;
|
|
191
|
-
}
|
|
192
|
-
finally {
|
|
193
|
-
releaseDrainLock();
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
export async function runRecordsSync(options, globals) {
|
|
197
|
-
const store = await openLocalStore();
|
|
198
|
-
try {
|
|
199
|
-
const root = process.cwd();
|
|
200
|
-
const project = await resolveProjectId(root, {});
|
|
201
|
-
if (project.ok)
|
|
202
|
-
releaseUnboundRows(store, root);
|
|
203
|
-
const drained = await drainAs(store, globals, isFlagSet(DRAIN_CHILD_VARIABLE));
|
|
204
|
-
if (drained?.originRefusal) {
|
|
205
|
-
if (options.json)
|
|
206
|
-
printJsonError("records sync", { code: HARNESS_CODES.ORIGIN_MISMATCH, message: drained.originRefusal });
|
|
207
|
-
else {
|
|
208
|
-
process.stderr.write(`${drained.originRefusal}\n`);
|
|
209
|
-
process.exitCode = 1;
|
|
210
|
-
}
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
if (options.quiet)
|
|
214
|
-
return;
|
|
215
|
-
const counts = await countRecords(store);
|
|
216
|
-
const result = {
|
|
217
|
-
sent: drained?.sent ?? 0,
|
|
218
|
-
waiting: counts.waitingCount,
|
|
219
|
-
stored: counts.storeCount,
|
|
220
|
-
refused: counts.refusedCount,
|
|
221
|
-
lastError: drained?.lastError ?? counts.lastError,
|
|
222
|
-
};
|
|
223
|
-
if (options.json)
|
|
224
|
-
printJsonSuccess("records sync", result);
|
|
225
|
-
else {
|
|
226
|
-
process.stdout.write(`records sync: sent ${result.sent}; ${result.waiting} waiting, ${result.stored} kept locally, ${result.refused} refused\n`);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
finally {
|
|
230
|
-
store.close();
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
174
|
export async function runRecordsList(options) {
|
|
234
175
|
const store = await openLocalStore();
|
|
235
176
|
try {
|
|
236
|
-
const counts =
|
|
177
|
+
const counts = countRecords(store, options.project);
|
|
237
178
|
const records = listRecords(store, {
|
|
238
179
|
...(options.project ? { projectId: options.project } : {}),
|
|
239
180
|
...(options.kind ? { kind: options.kind } : {}),
|
|
@@ -260,27 +201,10 @@ export async function runRecordsList(options) {
|
|
|
260
201
|
store.close();
|
|
261
202
|
}
|
|
262
203
|
}
|
|
263
|
-
export async function startDrainIfDue(globals) {
|
|
264
|
-
try {
|
|
265
|
-
if (isFlagSet(DRAIN_CHILD_VARIABLE) || !existsSync(stateDbPath()))
|
|
266
|
-
return;
|
|
267
|
-
const store = await openLocalStore();
|
|
268
|
-
try {
|
|
269
|
-
if (hasDueRecord(store.db, Date.now(), readVariable(DEVICE_TOKEN_VARIABLE) !== null)) {
|
|
270
|
-
startDetachedDrain({ nustackUrl: globals.nustackUrl });
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
finally {
|
|
274
|
-
store.close();
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
catch {
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
204
|
export function registerRecordsCommands(recordsCommand, getGlobalOptions) {
|
|
281
205
|
recordsCommand
|
|
282
206
|
.command("emit")
|
|
283
|
-
.description("Write records locally and return; a background
|
|
207
|
+
.description("Write records locally and return; a background store sync sends them to NuStack")
|
|
284
208
|
.option("--kind <kind>", "obligation, gate_event, changelog, memory, backlog, proposal or triage")
|
|
285
209
|
.option("--row <json>", "one row, an array of rows, or {kind, rows}, as JSON (default: read from stdin)")
|
|
286
210
|
.option("--root <dir>", "the folder the records are about (default: the current directory)")
|
|
@@ -289,12 +213,6 @@ export function registerRecordsCommands(recordsCommand, getGlobalOptions) {
|
|
|
289
213
|
.option("--plan <anchor>", "the plan anchor, when a row names none")
|
|
290
214
|
.option("--json", "emit the versioned machine envelope")
|
|
291
215
|
.action(async (options) => runRecordsEmit(options, getGlobalOptions()));
|
|
292
|
-
recordsCommand
|
|
293
|
-
.command("sync")
|
|
294
|
-
.description("Send every due record this device holds to NuStack")
|
|
295
|
-
.option("--quiet", "print nothing")
|
|
296
|
-
.option("--json", "emit the versioned machine envelope")
|
|
297
|
-
.action(async (options) => runRecordsSync(options, getGlobalOptions()));
|
|
298
216
|
recordsCommand
|
|
299
217
|
.command("list")
|
|
300
218
|
.description("Show this device's records: what waits, what was refused, what stays local")
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { HARNESS_CODES } from "../lib/harness/codes.js";
|
|
3
|
+
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
4
|
+
import { openLocalStore } from "../lib/local_store/database.js";
|
|
5
|
+
import { resolveProjectId } from "../lib/project_context.js";
|
|
6
|
+
import { acquireDrainLock, refreshDrainLock, releaseDrainLock, startDetachedDrain } from "../lib/records/drain.js";
|
|
7
|
+
import { DEVICE_TOKEN_VARIABLE, DRAIN_CHILD_VARIABLE, isFlagSet, readVariable } from "../lib/records/environment.js";
|
|
8
|
+
import { countRecords, releaseUnboundRows } from "../lib/records/recordsTable.js";
|
|
9
|
+
import { drainSessionLegs } from "../lib/sessions/sessionEvents.js";
|
|
10
|
+
import { pullAll, pullCursor, pushOutbox } from "../lib/sync/client.js";
|
|
11
|
+
import { countOutbox, hasDueEntry, newestOutboxError } from "../lib/sync/outbox.js";
|
|
12
|
+
import { cliVersion, machineStorePath, storeStampOf } from "../lib/store/open.js";
|
|
13
|
+
const CLIENT_OUTDATED = "CLIENT_OUTDATED";
|
|
14
|
+
function answeredRefusal(command, options, summaries) {
|
|
15
|
+
for (const summary of summaries) {
|
|
16
|
+
if (!summary)
|
|
17
|
+
continue;
|
|
18
|
+
const refusal = summary.originRefusal
|
|
19
|
+
? { code: HARNESS_CODES.ORIGIN_MISMATCH, message: summary.originRefusal }
|
|
20
|
+
: summary.outdated
|
|
21
|
+
? { code: CLIENT_OUTDATED, message: summary.outdated }
|
|
22
|
+
: null;
|
|
23
|
+
if (!refusal)
|
|
24
|
+
continue;
|
|
25
|
+
if (options.json)
|
|
26
|
+
printJsonError(command, refusal);
|
|
27
|
+
else {
|
|
28
|
+
process.stderr.write(`${refusal.message}\n`);
|
|
29
|
+
process.exitCode = 1;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
async function releaseHere(store) {
|
|
36
|
+
const root = process.cwd();
|
|
37
|
+
if ((await resolveProjectId(root, {})).ok)
|
|
38
|
+
releaseUnboundRows(store, root);
|
|
39
|
+
}
|
|
40
|
+
async function syncAs(store, globals, isChild) {
|
|
41
|
+
if (!isChild) {
|
|
42
|
+
const push = await pushOutbox(store, globals, { isForeground: true });
|
|
43
|
+
const pull = push.originRefusal || push.outdated ? null : await pullAll(store, globals);
|
|
44
|
+
await drainSessionLegs(globals);
|
|
45
|
+
return { push, pull };
|
|
46
|
+
}
|
|
47
|
+
if (!acquireDrainLock())
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
const push = await pushOutbox(store, globals, { onPass: () => refreshDrainLock() });
|
|
51
|
+
refreshDrainLock();
|
|
52
|
+
const pull = push.originRefusal || push.outdated ? null : await pullAll(store, globals);
|
|
53
|
+
refreshDrainLock();
|
|
54
|
+
await drainSessionLegs(globals);
|
|
55
|
+
return { push, pull };
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
releaseDrainLock();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function runStoreSync(options, globals) {
|
|
62
|
+
const store = await openLocalStore();
|
|
63
|
+
try {
|
|
64
|
+
await releaseHere(store);
|
|
65
|
+
const synced = await syncAs(store, globals, isFlagSet(DRAIN_CHILD_VARIABLE));
|
|
66
|
+
if (answeredRefusal("store sync", options, [synced?.push ?? null, synced?.pull ?? null]))
|
|
67
|
+
return;
|
|
68
|
+
if (options.quiet)
|
|
69
|
+
return;
|
|
70
|
+
const counts = countRecords(store);
|
|
71
|
+
const result = {
|
|
72
|
+
pushed: synced?.push.sent ?? 0,
|
|
73
|
+
refused: synced?.push.refused ?? 0,
|
|
74
|
+
pulled: synced?.pull?.rows ?? 0,
|
|
75
|
+
tombstones: synced?.pull?.tombstones ?? 0,
|
|
76
|
+
cursor: pullCursor(store),
|
|
77
|
+
waiting: counts.waitingCount,
|
|
78
|
+
stored: counts.storeCount,
|
|
79
|
+
lastError: synced?.push.lastError ?? synced?.pull?.lastError ?? newestOutboxError(store.db),
|
|
80
|
+
};
|
|
81
|
+
if (options.json)
|
|
82
|
+
printJsonSuccess("store sync", result);
|
|
83
|
+
else {
|
|
84
|
+
process.stdout.write(`store sync: pushed ${result.pushed}, pulled ${result.pulled}; ${result.waiting} waiting, ${result.stored} kept locally, ${result.refused} refused\n`);
|
|
85
|
+
if (result.lastError)
|
|
86
|
+
process.stderr.write(` last error: ${result.lastError}\n`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
store.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export async function runStorePush(options, globals) {
|
|
94
|
+
const store = await openLocalStore();
|
|
95
|
+
try {
|
|
96
|
+
await releaseHere(store);
|
|
97
|
+
const push = await pushOutbox(store, globals, { isForeground: true });
|
|
98
|
+
if (answeredRefusal("store push", options, [push]))
|
|
99
|
+
return;
|
|
100
|
+
const result = { pushed: push.sent, refused: push.refused, lastError: push.lastError };
|
|
101
|
+
if (options.json)
|
|
102
|
+
printJsonSuccess("store push", result);
|
|
103
|
+
else
|
|
104
|
+
process.stdout.write(`store push: pushed ${result.pushed}, ${result.refused} refused\n`);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
store.close();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function runStorePull(options, globals) {
|
|
111
|
+
const store = await openLocalStore();
|
|
112
|
+
try {
|
|
113
|
+
const pull = await pullAll(store, globals);
|
|
114
|
+
if (answeredRefusal("store pull", options, [pull]))
|
|
115
|
+
return;
|
|
116
|
+
const result = { pulled: pull.rows, tombstones: pull.tombstones, cursor: pull.cursor, isReset: pull.isReset, lastError: pull.lastError };
|
|
117
|
+
if (options.json)
|
|
118
|
+
printJsonSuccess("store pull", result);
|
|
119
|
+
else {
|
|
120
|
+
process.stdout.write(`store pull: ${result.pulled} row(s), ${result.tombstones} tombstone(s); cursor ${result.cursor}\n`);
|
|
121
|
+
if (result.lastError)
|
|
122
|
+
process.stderr.write(` last error: ${result.lastError}\n`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
store.close();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export async function runStoreStatus(options) {
|
|
130
|
+
const store = await openLocalStore();
|
|
131
|
+
try {
|
|
132
|
+
const tableCount = (name) => Number(store.db.prepare(`SELECT count(*) AS n FROM "${name}"`).get().n);
|
|
133
|
+
const result = {
|
|
134
|
+
path: machineStorePath(),
|
|
135
|
+
version: cliVersion(),
|
|
136
|
+
stamp: storeStampOf(cliVersion()),
|
|
137
|
+
cursor: pullCursor(store),
|
|
138
|
+
outbox: countOutbox(store.db, Date.now()),
|
|
139
|
+
records: tableCount("harness_records"),
|
|
140
|
+
labelRows: tableCount("session_label_rows"),
|
|
141
|
+
lastError: newestOutboxError(store.db),
|
|
142
|
+
};
|
|
143
|
+
if (options.json)
|
|
144
|
+
printJsonSuccess("store status", result);
|
|
145
|
+
else {
|
|
146
|
+
process.stdout.write(`store status: ${result.path} (NuStack ${result.version}, stamp ${result.stamp}); cursor ${result.cursor}\n`);
|
|
147
|
+
process.stdout.write(` ${result.records} record(s), ${result.labelRows} label row(s)\n`);
|
|
148
|
+
if (result.outbox.length === 0)
|
|
149
|
+
process.stdout.write(" outbox: empty\n");
|
|
150
|
+
for (const entry of result.outbox) {
|
|
151
|
+
process.stdout.write(` outbox ${entry.table}: ${entry.pending} pending, ${entry.due} due, ${entry.failing} failing\n`);
|
|
152
|
+
}
|
|
153
|
+
if (result.lastError)
|
|
154
|
+
process.stdout.write(` last error: ${result.lastError}\n`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
store.close();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
export async function startDrainIfDue(globals) {
|
|
162
|
+
try {
|
|
163
|
+
if (isFlagSet(DRAIN_CHILD_VARIABLE) || !existsSync(machineStorePath()))
|
|
164
|
+
return;
|
|
165
|
+
const store = await openLocalStore();
|
|
166
|
+
try {
|
|
167
|
+
if (hasDueEntry(store.db, Date.now(), readVariable(DEVICE_TOKEN_VARIABLE) !== null))
|
|
168
|
+
startDetachedDrain({ nustackUrl: globals.nustackUrl });
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
store.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export function registerStoreCommands(storeCommand, getGlobalOptions) {
|
|
178
|
+
storeCommand
|
|
179
|
+
.command("sync")
|
|
180
|
+
.description("Push this machine's changes to NuStack, then pull what changed there")
|
|
181
|
+
.option("--quiet", "print nothing")
|
|
182
|
+
.option("--json", "emit the versioned machine envelope")
|
|
183
|
+
.action(async (options) => runStoreSync(options, getGlobalOptions()));
|
|
184
|
+
storeCommand
|
|
185
|
+
.command("push")
|
|
186
|
+
.description("Push this machine's pending changes to NuStack")
|
|
187
|
+
.option("--json", "emit the versioned machine envelope")
|
|
188
|
+
.action(async (options) => runStorePush(options, getGlobalOptions()));
|
|
189
|
+
storeCommand
|
|
190
|
+
.command("pull")
|
|
191
|
+
.description("Pull every row that changed on NuStack since the last pull")
|
|
192
|
+
.option("--json", "emit the versioned machine envelope")
|
|
193
|
+
.action(async (options) => runStorePull(options, getGlobalOptions()));
|
|
194
|
+
storeCommand
|
|
195
|
+
.command("status")
|
|
196
|
+
.description("Show the machine database: its release, the pull cursor and the outbox")
|
|
197
|
+
.option("--json", "emit the versioned machine envelope")
|
|
198
|
+
.action(async (options) => runStoreStatus(options));
|
|
199
|
+
}
|
package/dist/commands/sync.js
CHANGED
|
@@ -45,8 +45,8 @@ export async function runSync(options) {
|
|
|
45
45
|
spinner?.stop(`${projections.length} service credential(s) projected`);
|
|
46
46
|
const managedValues = await readManagedValues(path.join(cwd, ".env"));
|
|
47
47
|
const nextValues = {};
|
|
48
|
-
if (managedValues.
|
|
49
|
-
nextValues.
|
|
48
|
+
if (managedValues.NUSTACK_PROJECT_ID)
|
|
49
|
+
nextValues.NUSTACK_PROJECT_ID = managedValues.NUSTACK_PROJECT_ID;
|
|
50
50
|
for (const credential of [...projections].sort((a, b) => a.service.localeCompare(b.service))) {
|
|
51
51
|
const prefix = toEnvPrefix(credential.service);
|
|
52
52
|
nextValues[`${prefix}_ENDPOINT`] = credential.endpoint;
|
package/dist/commands/toolkit.js
CHANGED
|
@@ -101,10 +101,10 @@ export async function runToolkitReconcile(options, globals) {
|
|
|
101
101
|
const binding = await withStore((store) => getBindingByRepositoryRoot(store, ctx.root));
|
|
102
102
|
const manifests = await readProjectManifests(ctx.root);
|
|
103
103
|
const identity = manifests.projectManifest
|
|
104
|
-
? { projectId: manifests.projectManifest.
|
|
104
|
+
? { projectId: manifests.projectManifest.projectId, organizationId: manifests.projectManifest.organizationId }
|
|
105
105
|
: manifests.serviceProjectBinding;
|
|
106
106
|
if (!identity) {
|
|
107
|
-
emitFail("toolkit reconcile", json, "PROJECT_CONTEXT_MISSING", "No `.nustack/
|
|
107
|
+
emitFail("toolkit reconcile", json, "PROJECT_CONTEXT_MISSING", "No `.nustack/project.json` binding to reconcile — attach the repository first.");
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
const plan = await buildReconcilePlan(ctx.root, binding ?? { etnaProfileJson: null }, cloud);
|
|
@@ -8,7 +8,7 @@ const SCALAR_FIELDS = new Set(["kind", "plane", "projects", "one-line"]);
|
|
|
8
8
|
const SEQUENCE_FIELDS = new Set(["aliases", "tables"]);
|
|
9
9
|
export function docsRootOf(root) {
|
|
10
10
|
try {
|
|
11
|
-
const parsed = JSON.parse(readFileSync(path.join(root, ".nustack", "
|
|
11
|
+
const parsed = JSON.parse(readFileSync(path.join(root, ".nustack", "project.json"), "utf8"));
|
|
12
12
|
if (typeof parsed.docsRoot === "string" && parsed.docsRoot.trim() !== "")
|
|
13
13
|
return parsed.docsRoot.trim();
|
|
14
14
|
}
|
|
@@ -12,7 +12,7 @@ export const DESKTOP_AGENT_BIN_DIR = ".bin";
|
|
|
12
12
|
export const DESKTOP_AGENT_PROPOSAL_BRANCH_PREFIX = "nustack/proposal/";
|
|
13
13
|
export const DESKTOP_AGENT_EXCLUDE_START = "# nustack:desktop-agent:start";
|
|
14
14
|
export const DESKTOP_AGENT_EXCLUDE_END = "# nustack:desktop-agent:end";
|
|
15
|
-
const MARKER_RELATIVE = ".nustack/
|
|
15
|
+
const MARKER_RELATIVE = ".nustack/project.json";
|
|
16
16
|
const FETCH_TIMEOUT_MS = 10 * 60_000;
|
|
17
17
|
export function agentCheckoutsRoot(env = process.env) {
|
|
18
18
|
return path.join(sessionsHome(env), DESKTOP_AGENT_CHECKOUTS_DIR);
|
|
@@ -101,9 +101,9 @@ export function sandboxSettings(inputs) {
|
|
|
101
101
|
path.join(inputs.gitCommonDir, "refs"),
|
|
102
102
|
path.join(inputs.gitCommonDir, "logs"),
|
|
103
103
|
inputs.worktreeGitDir,
|
|
104
|
-
path.join(nustackHome, "
|
|
105
|
-
path.join(nustackHome, "
|
|
106
|
-
path.join(nustackHome, "
|
|
104
|
+
path.join(nustackHome, "nustack.db"),
|
|
105
|
+
path.join(nustackHome, "nustack.db-wal"),
|
|
106
|
+
path.join(nustackHome, "nustack.db-shm"),
|
|
107
107
|
path.join(nustackHome, "locks"),
|
|
108
108
|
path.join(nustackHome, "session-events"),
|
|
109
109
|
],
|
|
@@ -253,7 +253,7 @@ export function gitDirsOf(gitCommonDirOutput, gitDirOutput, worktree) {
|
|
|
253
253
|
}
|
|
254
254
|
export function syncRecords(env, cwd) {
|
|
255
255
|
return new Promise((resolve) => {
|
|
256
|
-
const child = spawn(process.execPath, [cliEntry(), "
|
|
256
|
+
const child = spawn(process.execPath, [cliEntry(), "store", "sync", "--json"], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
257
257
|
let output = "";
|
|
258
258
|
child.stdout.on("data", (chunk) => (output += chunk.toString("utf8")));
|
|
259
259
|
child.on("error", () => resolve({ code: null, output }));
|
|
@@ -41,10 +41,10 @@ export async function buildReconcilePlan(root, binding, cloud) {
|
|
|
41
41
|
const changes = [];
|
|
42
42
|
const project = manifests.projectManifest?.toolkit;
|
|
43
43
|
if (project) {
|
|
44
|
-
pushIf(changes, ".nustack/
|
|
45
|
-
pushIf(changes, ".nustack/
|
|
46
|
-
pushIf(changes, ".nustack/
|
|
47
|
-
pushIf(changes, ".nustack/
|
|
44
|
+
pushIf(changes, ".nustack/project.json", "toolkit.name", project.name, cloud.name);
|
|
45
|
+
pushIf(changes, ".nustack/project.json", "toolkit.version", project.version, cloud.version);
|
|
46
|
+
pushIf(changes, ".nustack/project.json", "toolkit.rendererVersion", project.rendererVersion, cloud.rendererVersion);
|
|
47
|
+
pushIf(changes, ".nustack/project.json", "toolkit.placementMapVersion", project.placementMapVersion, cloud.placementMapVersion);
|
|
48
48
|
}
|
|
49
49
|
const svc = manifests.serviceToolkit;
|
|
50
50
|
if (svc) {
|
|
@@ -2,9 +2,9 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { openLocalStore
|
|
5
|
+
import { openLocalStore } from "../local_store/database.js";
|
|
6
6
|
import { writeRecords } from "../records/recordsTable.js";
|
|
7
|
-
import { RECORD_SCHEMA_VERSION } from "../records/
|
|
7
|
+
import { RECORD_SCHEMA_VERSION } from "../records/kinds.js";
|
|
8
8
|
import { idempotencyKeyOf } from "../records/naturalKey.js";
|
|
9
9
|
import { recordCredentialOf } from "../records/environment.js";
|
|
10
10
|
import { readProjectContract, CONTRACT_PATH } from "../project_contract.js";
|
|
@@ -17,7 +17,6 @@ export const HOOK_EVENTS = ["pre-tool-use", "stop"];
|
|
|
17
17
|
export const RULE_UNAVAILABLE_GATE = "hook.rule_unavailable";
|
|
18
18
|
export const DEADLINE_GATE = "hook.deadline";
|
|
19
19
|
const EVENT_NAMESPACE = "3f0c9a52-6d1e-4b7a-9c2f-8e5d4a1b7c60";
|
|
20
|
-
const RECORDS_STORE_VERSION = 9;
|
|
21
20
|
const EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
22
21
|
export const COMMIT_RULES = new Set([
|
|
23
22
|
"changelog",
|
|
@@ -163,11 +162,6 @@ async function writeVerdicts(root, projectId, payload, target, verdicts) {
|
|
|
163
162
|
if (verdicts.length === 0)
|
|
164
163
|
return null;
|
|
165
164
|
try {
|
|
166
|
-
const held = await openLocalStoreReadOnly();
|
|
167
|
-
const version = held?.version ?? RECORDS_STORE_VERSION;
|
|
168
|
-
held?.close();
|
|
169
|
-
if (version < RECORDS_STORE_VERSION)
|
|
170
|
-
return null;
|
|
171
165
|
const store = await openLocalStore();
|
|
172
166
|
try {
|
|
173
167
|
const now = Date.now();
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { openLocalStoreReadOnly } from "../local_store/database.js";
|
|
2
|
-
const RECORDS_STORE_VERSION = 9;
|
|
3
2
|
const NEVER_EXPIRES = "9999-12-31";
|
|
4
3
|
const DISCHARGED_VERDICTS = new Set(["updated", "owes_nothing"]);
|
|
5
4
|
export async function readStoreDocumentationWaivers(projectId, dbPath) {
|
|
@@ -9,10 +8,8 @@ export async function readStoreDocumentationWaivers(projectId, dbPath) {
|
|
|
9
8
|
if (!store)
|
|
10
9
|
return [];
|
|
11
10
|
try {
|
|
12
|
-
if (store.version < RECORDS_STORE_VERSION)
|
|
13
|
-
return [];
|
|
14
11
|
const rows = store.db
|
|
15
|
-
.prepare("SELECT payload_json FROM
|
|
12
|
+
.prepare("SELECT payload_json FROM harness_records WHERE kind = 'obligation' AND project_id = ?")
|
|
16
13
|
.all(projectId);
|
|
17
14
|
const subjects = new Set();
|
|
18
15
|
for (const row of rows) {
|