@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.
Files changed (131) hide show
  1. package/README.md +77 -0
  2. package/dist/commands/add.js +130 -0
  3. package/dist/commands/agent.js +100 -0
  4. package/dist/commands/bootstrap.js +54 -0
  5. package/dist/commands/describe.js +34 -0
  6. package/dist/commands/device.js +159 -0
  7. package/dist/commands/docs.js +119 -0
  8. package/dist/commands/doctor.js +284 -0
  9. package/dist/commands/graph.js +34 -0
  10. package/dist/commands/handoff.js +108 -0
  11. package/dist/commands/init.js +280 -0
  12. package/dist/commands/lane.js +12 -0
  13. package/dist/commands/list.js +20 -0
  14. package/dist/commands/login.js +134 -0
  15. package/dist/commands/logout.js +44 -0
  16. package/dist/commands/open.js +54 -0
  17. package/dist/commands/organization.js +114 -0
  18. package/dist/commands/review.js +152 -0
  19. package/dist/commands/search.js +57 -0
  20. package/dist/commands/sessions.js +62 -0
  21. package/dist/commands/sync.js +85 -0
  22. package/dist/commands/telemetry.js +112 -0
  23. package/dist/commands/toolkit.js +167 -0
  24. package/dist/commands/validate.js +67 -0
  25. package/dist/commands/workspace-sync.js +645 -0
  26. package/dist/commands/workspace.js +479 -0
  27. package/dist/index.js +357 -0
  28. package/dist/lib/agent/claudeHooks.js +77 -0
  29. package/dist/lib/auth_grant.js +398 -0
  30. package/dist/lib/auth_recovery.js +49 -0
  31. package/dist/lib/browser_opener.js +24 -0
  32. package/dist/lib/device_identity.js +28 -0
  33. package/dist/lib/device_keychain.js +101 -0
  34. package/dist/lib/env_file.js +75 -0
  35. package/dist/lib/etna/manager.js +43 -0
  36. package/dist/lib/etna/profile.js +41 -0
  37. package/dist/lib/etna/reconcile.js +77 -0
  38. package/dist/lib/filebeat_installer.js +246 -0
  39. package/dist/lib/git-sync/core/commit_range.js +37 -0
  40. package/dist/lib/git-sync/core/engine.js +263 -0
  41. package/dist/lib/git-sync/core/git_runner.js +137 -0
  42. package/dist/lib/git-sync/core/operation_state.js +22 -0
  43. package/dist/lib/git-sync/core/path_normalizer.js +98 -0
  44. package/dist/lib/git-sync/core/reconciliation_lock.js +103 -0
  45. package/dist/lib/git-sync/core/repository.js +49 -0
  46. package/dist/lib/git-sync/core/watch_stream.js +86 -0
  47. package/dist/lib/git-sync/core/watcher.js +61 -0
  48. package/dist/lib/git-sync/inbound/applier.js +131 -0
  49. package/dist/lib/git-sync/inbound/apply_commit.js +225 -0
  50. package/dist/lib/git-sync/inbound/approval.js +117 -0
  51. package/dist/lib/git-sync/inbound/content_decision.js +148 -0
  52. package/dist/lib/git-sync/inbound/download.js +223 -0
  53. package/dist/lib/git-sync/inbound/event_sync.js +133 -0
  54. package/dist/lib/git-sync/inbound/index.js +10 -0
  55. package/dist/lib/git-sync/inbound/notification.js +39 -0
  56. package/dist/lib/git-sync/inbound/state_fingerprint.js +32 -0
  57. package/dist/lib/git-sync/inbound/technical_wait.js +39 -0
  58. package/dist/lib/git-sync/inbound/undo.js +198 -0
  59. package/dist/lib/git-sync/outbound/extraction.js +107 -0
  60. package/dist/lib/git-sync/outbound/history_decision.js +112 -0
  61. package/dist/lib/git-sync/outbound/index.js +26 -0
  62. package/dist/lib/git-sync/outbound/manifest_builder.js +58 -0
  63. package/dist/lib/git-sync/outbound/publisher.js +51 -0
  64. package/dist/lib/git-sync/outbound/queue.js +123 -0
  65. package/dist/lib/git-sync/outbound/state.js +12 -0
  66. package/dist/lib/git-sync/outbound/upload_client.js +141 -0
  67. package/dist/lib/git-sync/recovery/bundle.js +56 -0
  68. package/dist/lib/git-sync/recovery/crypto.js +114 -0
  69. package/dist/lib/git-sync/recovery/index.js +3 -0
  70. package/dist/lib/git-sync/recovery/store.js +55 -0
  71. package/dist/lib/git-sync/whitelist/index.js +4 -0
  72. package/dist/lib/git-sync/whitelist/matcher.js +62 -0
  73. package/dist/lib/git-sync/whitelist/store.js +99 -0
  74. package/dist/lib/git-sync/whitelist/types.js +46 -0
  75. package/dist/lib/git-sync/whitelist/validation.js +60 -0
  76. package/dist/lib/graph/answer.js +171 -0
  77. package/dist/lib/graph/freshness.js +29 -0
  78. package/dist/lib/graph/library.js +19 -0
  79. package/dist/lib/guards.js +106 -0
  80. package/dist/lib/interactive.js +12 -0
  81. package/dist/lib/json_output.js +8 -0
  82. package/dist/lib/lanes/claudeAdapter.js +135 -0
  83. package/dist/lib/lanes/frames.js +73 -0
  84. package/dist/lib/lanes/supervisor.js +247 -0
  85. package/dist/lib/lanes/worktree.js +33 -0
  86. package/dist/lib/local_store/accepted_uploads.js +49 -0
  87. package/dist/lib/local_store/bindings.js +163 -0
  88. package/dist/lib/local_store/commit_origins.js +25 -0
  89. package/dist/lib/local_store/database.js +71 -0
  90. package/dist/lib/local_store/device_state.js +72 -0
  91. package/dist/lib/local_store/filebeat_state.js +133 -0
  92. package/dist/lib/local_store/handoff_applications.js +166 -0
  93. package/dist/lib/local_store/history_decisions.js +48 -0
  94. package/dist/lib/local_store/index.js +14 -0
  95. package/dist/lib/local_store/migrations.js +240 -0
  96. package/dist/lib/local_store/paths.js +24 -0
  97. package/dist/lib/local_store/pending_uploads.js +76 -0
  98. package/dist/lib/local_store/recovery_bundles.js +66 -0
  99. package/dist/lib/local_store/sync_vocabulary.js +33 -0
  100. package/dist/lib/local_store/workspace_events_cache.js +54 -0
  101. package/dist/lib/loopback.js +78 -0
  102. package/dist/lib/manifest.js +83 -0
  103. package/dist/lib/nustack_client.js +547 -0
  104. package/dist/lib/nustack_store.js +17 -0
  105. package/dist/lib/onboarding/attach_existing_git.js +65 -0
  106. package/dist/lib/onboarding/binding.js +91 -0
  107. package/dist/lib/onboarding/folder_state.js +99 -0
  108. package/dist/lib/onboarding/git_init.js +10 -0
  109. package/dist/lib/onboarding/handoff_apply.js +68 -0
  110. package/dist/lib/onboarding/init_fresh_folder.js +100 -0
  111. package/dist/lib/onboarding/manifest_writer.js +161 -0
  112. package/dist/lib/onboarding/web_first_attach.js +133 -0
  113. package/dist/lib/package_manager.js +23 -0
  114. package/dist/lib/review/client.js +66 -0
  115. package/dist/lib/schema_validator.js +24 -0
  116. package/dist/lib/secure_file.js +10 -0
  117. package/dist/lib/session_telemetry.js +263 -0
  118. package/dist/lib/sessions/client.js +48 -0
  119. package/dist/lib/sessions/outsideSessions.js +54 -0
  120. package/dist/lib/sse_framing.js +48 -0
  121. package/dist/lib/telemetry/config_renderer.js +175 -0
  122. package/dist/lib/telemetry/health_report.js +173 -0
  123. package/dist/lib/telemetry/provider_paths.js +38 -0
  124. package/dist/lib/telemetry/redaction_policy.js +90 -0
  125. package/dist/lib/telemetry/reload.js +84 -0
  126. package/dist/lib/workspace_context.js +121 -0
  127. package/dist/spec_schema.json +422 -0
  128. package/dist/wire/auth.js +1 -0
  129. package/dist/wire/sync-paths.js +25 -0
  130. package/dist/wire/telemetry.js +1 -0
  131. package/package.json +49 -0
@@ -0,0 +1,645 @@
1
+ import os from "node:os";
2
+ import { resolveNuStackUrl } from "../lib/nustack_client.js";
3
+ import { openLocalStore } from "../lib/local_store/database.js";
4
+ import { listActiveBindings, getBindingById, } from "../lib/local_store/bindings.js";
5
+ import { readDeviceState } from "../lib/local_store/device_state.js";
6
+ import { listApplicationsForBinding, getApplication, } from "../lib/local_store/handoff_applications.js";
7
+ import { ensureAccessToken, signedOutCopy } from "../lib/auth_grant.js";
8
+ import { createSyncEngine, observeBinding } from "../lib/git-sync/core/engine.js";
9
+ import { createWatchStreamEmitter } from "../lib/git-sync/core/watch_stream.js";
10
+ import { reconcileInboundBinding, retryApplication, approveHandoff, undoHandoff, listUnacknowledgedNotifications, acknowledgeNotification, renderCommitNotification, } from "../lib/git-sync/inbound/index.js";
11
+ import { listBundles, restoreBundle, deleteBundle, sweepExpiredBundles, } from "../lib/git-sync/recovery/index.js";
12
+ import { resolveRepository } from "../lib/git-sync/core/repository.js";
13
+ import { registerOutbound, createUploadClient, deviceTokenProvider, describeHistoryDecision, publishCurrentState, adoptWithoutPublishing, } from "../lib/git-sync/outbound/index.js";
14
+ import { latestAcceptedUpload } from "../lib/local_store/accepted_uploads.js";
15
+ const ONBOARDING_POINTER = "No NuStack Workspace bindings on this machine. Run `nustack workspace attach` inside a Git repository to bind it.";
16
+ function writeLine(line) {
17
+ process.stdout.write(`${line}\n`);
18
+ }
19
+ export function registerWorkspaceSyncCommands(workspaceCommand, getGlobalOptions) {
20
+ void getGlobalOptions;
21
+ const sync = workspaceCommand
22
+ .command("sync")
23
+ .description("Synchronize NuStack Documents with your local Git repository");
24
+ sync
25
+ .command("status")
26
+ .description("Show local binding status and a live Git observation (no network)")
27
+ .option("--json", "emit the binding observations as JSON")
28
+ .action(async (options) => runStatus(options));
29
+ sync
30
+ .command("now")
31
+ .description("Run one reconciliation pass over each active binding")
32
+ .option("--binding <id>", "reconcile only this binding")
33
+ .option("--json", "emit the reconciliation outcomes as JSON")
34
+ .action(async (options) => runNow(getGlobalOptions(), options));
35
+ sync
36
+ .command("watch")
37
+ .description("Run the reconciliation engine continuously (the desktop supervision loop)")
38
+ .option("--binding <id>", "supervise only this binding (the desktop runs one child per binding)")
39
+ .option("--json", "emit the versioned JSONL watch stream on stdout")
40
+ .action(async (options) => runWatch(getGlobalOptions(), options));
41
+ sync
42
+ .command("pending")
43
+ .description("List inbound Handoff applications awaiting evaluation, decision, or a technical wait")
44
+ .option("--json", "emit the pending applications as JSON")
45
+ .action(async (options) => runPending(options));
46
+ sync
47
+ .command("approve <handoffId>")
48
+ .description("Approve an overwrite decision for a cloud Handoff (a content decision — never automated)")
49
+ .option("--binding <id>", "disambiguate when more than one binding has this Handoff")
50
+ .option("--yes", "(refused) approval is a content decision and cannot be pre-confirmed")
51
+ .option("--json", "emit the outcome as JSON")
52
+ .action(async (handoffId, options) => runApprove(getGlobalOptions(), handoffId, options));
53
+ sync
54
+ .command("retry <handoffId>")
55
+ .description("Re-evaluate a Handoff parked in a technical wait or error")
56
+ .option("--binding <id>", "disambiguate when more than one binding has this Handoff")
57
+ .option("--json", "emit the outcome as JSON")
58
+ .action(async (handoffId, options) => runRetry(getGlobalOptions(), handoffId, options));
59
+ sync
60
+ .command("undo <handoffId>")
61
+ .description("Revert a committed inbound Handoff with a normal User-identified revert commit")
62
+ .option("--binding <id>", "disambiguate when more than one binding has this Handoff")
63
+ .option("--json", "emit the outcome as JSON")
64
+ .action(async (handoffId, options) => runUndo(getGlobalOptions(), handoffId, options));
65
+ sync
66
+ .command("reconcile-history")
67
+ .description("Resolve a rewritten-history block by publishing current state or adopting it — never a default")
68
+ .option("--binding <id>", "reconcile only this binding")
69
+ .option("--publish", "publish current committed state as one Handoff, then adopt HEAD as the baseline")
70
+ .option("--adopt", "adopt HEAD as the baseline without publishing (cloud state unchanged)")
71
+ .option("--json", "emit the decision record as JSON")
72
+ .action(async (options) => runReconcileHistory(getGlobalOptions(), options));
73
+ const recovery = sync
74
+ .command("recovery")
75
+ .description("Manage machine-local recovery bundles saved before an approved overwrite");
76
+ recovery
77
+ .command("list")
78
+ .description("List recovery bundles and their expiries")
79
+ .option("--json", "emit the bundles as JSON")
80
+ .action(async (options) => runRecoveryList(options));
81
+ recovery
82
+ .command("restore <bundleId>")
83
+ .description("Restore a recovery bundle's pre-approval content outside the repository")
84
+ .option("--to <dir>", "extract into this directory instead of the default recovery area")
85
+ .option("--json", "emit the outcome as JSON")
86
+ .action(async (bundleId, options) => runRecoveryRestore(bundleId, options));
87
+ recovery
88
+ .command("delete <bundleId>")
89
+ .description("Delete a recovery bundle and its encrypted file")
90
+ .option("--json", "emit the outcome as JSON")
91
+ .action(async (bundleId, options) => runRecoveryDelete(bundleId, options));
92
+ }
93
+ async function buildInboundRuntime(store, globals) {
94
+ const baseUrl = resolveNuStackUrl(globals);
95
+ const auth = await ensureAccessToken(baseUrl).catch(() => null);
96
+ if (!auth)
97
+ return null;
98
+ const deviceState = readDeviceState(store);
99
+ return {
100
+ baseUrl,
101
+ accessToken: auth.accessToken,
102
+ userId: deviceState?.activeUserId ?? null,
103
+ deviceRef: deviceState?.deviceRef ?? null,
104
+ };
105
+ }
106
+ function resolveApplicationBinding(store, handoffId, bindingOption) {
107
+ if (bindingOption) {
108
+ const application = getApplication(store, handoffId, bindingOption);
109
+ if (!application)
110
+ return { ok: false, message: `No application for Handoff ${handoffId} on binding ${bindingOption}.` };
111
+ return { ok: true, bindingId: bindingOption };
112
+ }
113
+ const matches = listActiveBindings(store)
114
+ .filter((binding) => getApplication(store, handoffId, binding.id))
115
+ .map((binding) => binding.id);
116
+ if (matches.length === 0)
117
+ return { ok: false, message: `No application for Handoff ${handoffId} on this machine.` };
118
+ if (matches.length > 1)
119
+ return { ok: false, message: `Handoff ${handoffId} is bound to more than one binding; pass --binding <id>.` };
120
+ return { ok: true, bindingId: matches[0] };
121
+ }
122
+ function displayPath(absolute) {
123
+ const home = os.homedir();
124
+ return home.length > 1 && absolute.startsWith(home) ? `~${absolute.slice(home.length)}` : "(outside home)";
125
+ }
126
+ function parseDecisionEvidence(json) {
127
+ if (!json)
128
+ return [];
129
+ try {
130
+ const parsed = JSON.parse(json);
131
+ return Array.isArray(parsed.divergent) ? parsed.divergent : [];
132
+ }
133
+ catch {
134
+ return [];
135
+ }
136
+ }
137
+ function toPendingView(application) {
138
+ return {
139
+ handoffId: application.handoffId,
140
+ bindingId: application.bindingId,
141
+ state: application.state,
142
+ waitReason: application.technicalWaitReason,
143
+ divergence: parseDecisionEvidence(application.decisionState),
144
+ errorCode: application.errorCode,
145
+ };
146
+ }
147
+ const PENDING_STATES = ["received", "evaluating", "technical_wait", "decision_required", "approved"];
148
+ async function runPending(options) {
149
+ const store = await openLocalStore();
150
+ try {
151
+ const bindings = listActiveBindings(store);
152
+ if (bindings.length === 0) {
153
+ if (options.json)
154
+ writeLine("[]");
155
+ else
156
+ writeLine(ONBOARDING_POINTER);
157
+ return;
158
+ }
159
+ for (const binding of bindings) {
160
+ for (const application of listUnacknowledgedNotifications(store, binding.id)) {
161
+ const paths = parseDecisionEvidence(application.decisionState).map((entry) => entry.path);
162
+ if (!options.json)
163
+ for (const line of renderCommitNotification(application, paths))
164
+ writeLine(line);
165
+ acknowledgeNotification(store, application.handoffId, binding.id);
166
+ }
167
+ }
168
+ const pending = bindings.flatMap((binding) => listApplicationsForBinding(store, binding.id, PENDING_STATES));
169
+ if (options.json) {
170
+ writeLine(JSON.stringify(pending.map(toPendingView), null, 2));
171
+ return;
172
+ }
173
+ if (pending.length === 0) {
174
+ writeLine("No pending inbound Handoff applications.");
175
+ return;
176
+ }
177
+ for (const application of pending) {
178
+ const evidence = parseDecisionEvidence(application.decisionState);
179
+ const detail = application.state === "decision_required"
180
+ ? ` — ${evidence.map((entry) => `${entry.path} (${entry.divergence})`).join(", ")}`
181
+ : application.technicalWaitReason
182
+ ? ` — ${application.technicalWaitReason}`
183
+ : "";
184
+ writeLine(`${application.handoffId} ${application.state}${detail}`);
185
+ }
186
+ }
187
+ finally {
188
+ store.close();
189
+ }
190
+ }
191
+ async function runApprove(globals, handoffId, options) {
192
+ if (options.yes) {
193
+ writeLine("`--yes` is refused: approving an overwrite is a content decision and cannot be pre-confirmed.");
194
+ process.exitCode = 1;
195
+ return;
196
+ }
197
+ const store = await openLocalStore();
198
+ try {
199
+ const runtime = await buildInboundRuntime(store, globals);
200
+ if (!runtime) {
201
+ writeLine(signedOutCopy());
202
+ process.exitCode = 1;
203
+ return;
204
+ }
205
+ const resolved = resolveApplicationBinding(store, handoffId, options.binding);
206
+ if (!resolved.ok) {
207
+ writeLine(resolved.message);
208
+ process.exitCode = 1;
209
+ return;
210
+ }
211
+ const result = await approveHandoff(store, runtime, resolved.bindingId, handoffId);
212
+ if (options.json) {
213
+ writeLine(JSON.stringify(result, null, 2));
214
+ }
215
+ else if (result.ok) {
216
+ writeLine(`Approved and committed ${result.commitId.slice(0, 12)} — NuStack did not push it.`);
217
+ if (result.recoveryBundleId)
218
+ writeLine(`Recovery bundle ${result.recoveryBundleId} holds your pre-approval content.`);
219
+ }
220
+ else if (result.code === "local_state_changed" && result.warning) {
221
+ writeLine("Your local state changed since the warning — re-review before approving:");
222
+ for (const entry of result.warning.paths)
223
+ writeLine(` ${entry.path} (${entry.divergence})`);
224
+ writeLine(result.warning.recoveryNote);
225
+ }
226
+ else {
227
+ writeLine(`Approve did not proceed: ${result.code}${result.detail ? ` (${result.detail})` : ""}.`);
228
+ }
229
+ if (!result.ok && result.code !== "local_state_changed")
230
+ process.exitCode = 1;
231
+ }
232
+ finally {
233
+ store.close();
234
+ }
235
+ }
236
+ async function runRetry(globals, handoffId, options) {
237
+ const store = await openLocalStore();
238
+ try {
239
+ const runtime = await buildInboundRuntime(store, globals);
240
+ if (!runtime) {
241
+ writeLine(signedOutCopy());
242
+ process.exitCode = 1;
243
+ return;
244
+ }
245
+ const resolved = resolveApplicationBinding(store, handoffId, options.binding);
246
+ if (!resolved.ok) {
247
+ writeLine(resolved.message);
248
+ process.exitCode = 1;
249
+ return;
250
+ }
251
+ const result = await retryApplication(store, runtime, resolved.bindingId, handoffId);
252
+ if (options.json)
253
+ writeLine(JSON.stringify(result, null, 2));
254
+ else if (result.ok)
255
+ writeLine(`Re-evaluated ${handoffId} — now ${result.state}.`);
256
+ else {
257
+ writeLine(`Retry did not proceed: ${result.code}.`);
258
+ process.exitCode = 1;
259
+ }
260
+ }
261
+ finally {
262
+ store.close();
263
+ }
264
+ }
265
+ async function runUndo(globals, handoffId, options) {
266
+ const store = await openLocalStore();
267
+ try {
268
+ const runtime = await buildInboundRuntime(store, globals);
269
+ if (!runtime) {
270
+ writeLine(signedOutCopy());
271
+ process.exitCode = 1;
272
+ return;
273
+ }
274
+ const resolved = resolveApplicationBinding(store, handoffId, options.binding);
275
+ if (!resolved.ok) {
276
+ writeLine(resolved.message);
277
+ process.exitCode = 1;
278
+ return;
279
+ }
280
+ const result = await undoHandoff(store, runtime, resolved.bindingId, handoffId);
281
+ if (options.json) {
282
+ writeLine(JSON.stringify(result, null, 2));
283
+ }
284
+ else if (result.ok) {
285
+ writeLine(`Reverted with ${result.revertCommitId.slice(0, 12)} — NuStack did not push it.`);
286
+ }
287
+ else if (result.code === "revert_conflict") {
288
+ writeLine("Revert would conflict with later work — the repository is untouched. Affected paths:");
289
+ for (const path of result.conflictPaths)
290
+ writeLine(` ${path}`);
291
+ }
292
+ else if (result.code === "missing_identity") {
293
+ writeLine("Set your Git identity first: git config user.name and user.email.");
294
+ }
295
+ else {
296
+ writeLine(`Undo did not proceed: ${result.code}.`);
297
+ }
298
+ if (!result.ok)
299
+ process.exitCode = 1;
300
+ }
301
+ finally {
302
+ store.close();
303
+ }
304
+ }
305
+ async function runRecoveryList(options) {
306
+ const store = await openLocalStore();
307
+ try {
308
+ await sweepExpiredBundles(store);
309
+ const bundles = listBundles(store).map((bundle) => ({
310
+ id: bundle.id,
311
+ bindingId: bundle.bindingId,
312
+ handoffId: bundle.handoffId,
313
+ state: bundle.state,
314
+ expiresAt: new Date(bundle.expiresAtMs).toISOString(),
315
+ }));
316
+ if (options.json) {
317
+ writeLine(JSON.stringify(bundles, null, 2));
318
+ return;
319
+ }
320
+ if (bundles.length === 0) {
321
+ writeLine("No recovery bundles.");
322
+ return;
323
+ }
324
+ for (const bundle of bundles)
325
+ writeLine(`${bundle.id} ${bundle.state} handoff=${bundle.handoffId} expires=${bundle.expiresAt}`);
326
+ }
327
+ finally {
328
+ store.close();
329
+ }
330
+ }
331
+ async function runRecoveryRestore(bundleId, options) {
332
+ const store = await openLocalStore();
333
+ try {
334
+ const deviceState = readDeviceState(store);
335
+ const deviceRef = deviceState?.deviceRef;
336
+ if (!deviceRef) {
337
+ writeLine("This device has no recovery key reference — sign in first.");
338
+ process.exitCode = 1;
339
+ return;
340
+ }
341
+ const result = await restoreBundle(store, deviceRef, bundleId, options.to);
342
+ if (options.json) {
343
+ writeLine(JSON.stringify(result.ok ? { ok: true, files: result.files, dir: displayPath(result.dir) } : result, null, 2));
344
+ }
345
+ else if (result.ok) {
346
+ writeLine(`Restored ${result.files.length} file(s) to ${displayPath(result.dir)} (outside the repository).`);
347
+ for (const file of result.files)
348
+ writeLine(` ${file}`);
349
+ }
350
+ else {
351
+ writeLine(`Restore did not proceed: ${result.code}.`);
352
+ }
353
+ if (!result.ok)
354
+ process.exitCode = 1;
355
+ }
356
+ finally {
357
+ store.close();
358
+ }
359
+ }
360
+ async function runRecoveryDelete(bundleId, options) {
361
+ const store = await openLocalStore();
362
+ try {
363
+ const result = await deleteBundle(store, bundleId);
364
+ if (options.json)
365
+ writeLine(JSON.stringify(result, null, 2));
366
+ else if (result.ok)
367
+ writeLine(`Deleted recovery bundle ${bundleId}.`);
368
+ else {
369
+ writeLine(`Delete did not proceed: ${result.code}.`);
370
+ process.exitCode = 1;
371
+ }
372
+ }
373
+ finally {
374
+ store.close();
375
+ }
376
+ }
377
+ function shortId(id) {
378
+ return id ? id.slice(0, 12) : "—";
379
+ }
380
+ function outboundStatusFor(store, binding) {
381
+ const row = store.db
382
+ .prepare("SELECT COUNT(*) AS n, MIN(next_retry_at_ms) AS next FROM pending_uploads WHERE binding_id = :id")
383
+ .get({ id: binding.id });
384
+ const accepted = latestAcceptedUpload(store, binding.id);
385
+ let historyDecisionPending = false;
386
+ try {
387
+ historyDecisionPending = describeHistoryDecision(store, binding, resolveRepository(binding.repositoryRoot)).pending;
388
+ }
389
+ catch {
390
+ }
391
+ return {
392
+ pendingUploadCount: row.n,
393
+ nextRetryAt: row.next != null ? new Date(row.next).toISOString() : null,
394
+ blockedErrorCode: binding.status === "failed" ? binding.lastErrorCode : null,
395
+ lastAcceptedHandoffId: accepted?.handoffId ?? null,
396
+ lastAcceptedWorkspaceSequence: accepted?.workspaceSequence ?? null,
397
+ historyDecisionPending,
398
+ };
399
+ }
400
+ function historyTargets(store, bindingOption) {
401
+ if (bindingOption) {
402
+ const binding = getBindingById(store, bindingOption);
403
+ if (!binding)
404
+ return { ok: false, message: `No binding "${bindingOption}" on this machine.` };
405
+ return { ok: true, targets: [binding] };
406
+ }
407
+ return { ok: true, targets: listActiveBindings(store) };
408
+ }
409
+ async function runReconcileHistory(globals, options) {
410
+ const store = await openLocalStore();
411
+ try {
412
+ if (options.publish && options.adopt) {
413
+ writeLine("Choose exactly one of --publish or --adopt.");
414
+ process.exitCode = 1;
415
+ return;
416
+ }
417
+ const resolved = historyTargets(store, options.binding);
418
+ if (!resolved.ok) {
419
+ writeLine(resolved.message);
420
+ process.exitCode = 1;
421
+ return;
422
+ }
423
+ if (resolved.targets.length === 0) {
424
+ if (options.json)
425
+ writeLine("[]");
426
+ else
427
+ writeLine(ONBOARDING_POINTER);
428
+ return;
429
+ }
430
+ const records = [];
431
+ for (const binding of resolved.targets) {
432
+ let repo;
433
+ try {
434
+ repo = resolveRepository(binding.repositoryRoot);
435
+ }
436
+ catch {
437
+ records.push({ bindingId: binding.id, error: "repository_unresolved" });
438
+ if (!options.json)
439
+ writeLine(`${binding.id}: repository could not be resolved.`);
440
+ process.exitCode = 1;
441
+ continue;
442
+ }
443
+ const evidence = describeHistoryDecision(store, binding, repo);
444
+ if (!evidence.pending) {
445
+ records.push({ bindingId: binding.id, pending: false });
446
+ if (!options.json)
447
+ writeLine(`${binding.id}: no rewritten-history decision pending.`);
448
+ continue;
449
+ }
450
+ if (!options.publish && !options.adopt) {
451
+ records.push({
452
+ bindingId: binding.id,
453
+ pending: true,
454
+ oldBaselineCommitId: evidence.oldBaselineCommitId,
455
+ headCommitId: evidence.headCommitId,
456
+ baselineReadable: evidence.baselineReadable,
457
+ choiceRequired: true,
458
+ });
459
+ if (!options.json) {
460
+ writeLine(`${binding.id}: rewritten history — the recorded baseline is no longer on the bound branch.`);
461
+ writeLine(` old baseline: ${shortId(evidence.oldBaselineCommitId)} HEAD: ${shortId(evidence.headCommitId)} baseline readable: ${evidence.baselineReadable}`);
462
+ writeLine(" Choose: --publish (publish current committed state) or --adopt (adopt HEAD without publishing). NuStack never guesses.");
463
+ }
464
+ process.exitCode = 1;
465
+ continue;
466
+ }
467
+ const baseUrl = resolveNuStackUrl(globals);
468
+ const client = createUploadClient({ baseUrl, getAccessToken: deviceTokenProvider(baseUrl) });
469
+ if (options.publish) {
470
+ const result = await publishCurrentState(store, binding, repo, { client });
471
+ if (result.ok) {
472
+ records.push({ bindingId: binding.id, decision: "publish_current_state", decisionId: result.decisionId, newBaselineCommitId: result.newBaselineCommitId, mode: result.mode, handoffId: result.handoffId ?? null, noOp: result.noOp });
473
+ if (!options.json)
474
+ writeLine(`${binding.id}: published current state (${result.mode}) — HEAD ${shortId(result.newBaselineCommitId)} is the new baseline${result.handoffId ? `, handoff ${result.handoffId}` : result.noOp ? " (server no-op)" : ""}.`);
475
+ }
476
+ else {
477
+ records.push({ bindingId: binding.id, decision: "publish_current_state", error: result.code });
478
+ if (!options.json)
479
+ writeLine(`${binding.id}: publish did not proceed: ${result.code}.`);
480
+ process.exitCode = 1;
481
+ }
482
+ }
483
+ else {
484
+ const result = await adoptWithoutPublishing(store, binding, repo, { client });
485
+ if (result.ok) {
486
+ records.push({ bindingId: binding.id, decision: "adopt_without_publishing", decisionId: result.decisionId, newBaselineCommitId: result.newBaselineCommitId });
487
+ if (!options.json)
488
+ writeLine(`${binding.id}: adopted HEAD ${shortId(result.newBaselineCommitId)} as the baseline — cloud state unchanged.`);
489
+ }
490
+ else {
491
+ records.push({ bindingId: binding.id, decision: "adopt_without_publishing", error: result.code });
492
+ if (!options.json)
493
+ writeLine(`${binding.id}: adopt did not proceed: ${result.code}.`);
494
+ process.exitCode = 1;
495
+ }
496
+ }
497
+ }
498
+ if (options.json)
499
+ writeLine(JSON.stringify(records, null, 2));
500
+ }
501
+ finally {
502
+ store.close();
503
+ }
504
+ }
505
+ async function runStatus(options) {
506
+ const store = await openLocalStore();
507
+ try {
508
+ const bindings = listActiveBindings(store);
509
+ if (bindings.length === 0) {
510
+ if (options.json)
511
+ writeLine("[]");
512
+ else
513
+ writeLine(ONBOARDING_POINTER);
514
+ return;
515
+ }
516
+ const observations = bindings.map((binding) => ({ ...observeBinding(binding), outbound: outboundStatusFor(store, binding) }));
517
+ if (options.json) {
518
+ writeLine(JSON.stringify(observations, null, 2));
519
+ return;
520
+ }
521
+ for (const observation of observations) {
522
+ const head = shortId(observation.headCommitId);
523
+ const processed = shortId(observation.lastProcessedCommitId);
524
+ const branchNote = observation.symbolicBranch === observation.boundBranch
525
+ ? observation.boundBranch
526
+ : `${observation.boundBranch} (checked out: ${observation.symbolicBranch ?? "detached"})`;
527
+ const outbound = observation.outbound;
528
+ const outboundNote = (outbound.pendingUploadCount > 0 ? ` queued=${outbound.pendingUploadCount}` : "") +
529
+ (outbound.blockedErrorCode ? ` blocked=${outbound.blockedErrorCode}` : "") +
530
+ (outbound.lastAcceptedWorkspaceSequence != null ? ` accepted@${outbound.lastAcceptedWorkspaceSequence}` : "") +
531
+ (outbound.historyDecisionPending ? " history-decision-required" : "");
532
+ writeLine(`${observation.bindingId} ${observation.status} branch=${branchNote} ` +
533
+ `head=${head} processed=${processed} pending=${observation.pendingCommitCount}` +
534
+ outboundNote +
535
+ (observation.resolved ? "" : " [repository unresolved]"));
536
+ }
537
+ }
538
+ finally {
539
+ store.close();
540
+ }
541
+ }
542
+ async function runNow(globals, options) {
543
+ const store = await openLocalStore();
544
+ try {
545
+ const engine = createSyncEngine({ store });
546
+ const runtime = await buildInboundRuntime(store, globals);
547
+ if (runtime)
548
+ registerOutbound(engine, { baseUrl: runtime.baseUrl });
549
+ let targets;
550
+ if (options.binding) {
551
+ const binding = getBindingById(store, options.binding);
552
+ if (!binding) {
553
+ writeLine(`No binding "${options.binding}" on this machine.`);
554
+ process.exitCode = 1;
555
+ return;
556
+ }
557
+ targets = [binding];
558
+ }
559
+ else {
560
+ targets = listActiveBindings(store);
561
+ }
562
+ if (targets.length === 0) {
563
+ if (options.json)
564
+ writeLine("[]");
565
+ else
566
+ writeLine(ONBOARDING_POINTER);
567
+ return;
568
+ }
569
+ const outcomes = [];
570
+ for (const binding of targets) {
571
+ outcomes.push(await engine.reconcileBinding(binding.id, "manual"));
572
+ }
573
+ if (runtime) {
574
+ for (const binding of targets) {
575
+ await reconcileInboundBinding(store, runtime, binding.id).catch(() => { });
576
+ }
577
+ }
578
+ if (options.json) {
579
+ writeLine(JSON.stringify(outcomes, null, 2));
580
+ return;
581
+ }
582
+ for (const outcome of outcomes) {
583
+ const detail = outcome.outcome === "commit_range_ready"
584
+ ? ` — ${outcome.pendingCommitCount} pending commit(s)`
585
+ : outcome.waitReason
586
+ ? ` — ${outcome.waitReason}`
587
+ : outcome.errorCode
588
+ ? ` — ${outcome.errorCode}`
589
+ : "";
590
+ writeLine(`${outcome.bindingId}: ${outcome.outcome}${detail}`);
591
+ }
592
+ }
593
+ finally {
594
+ store.close();
595
+ }
596
+ }
597
+ async function runWatch(globals, options) {
598
+ const store = await openLocalStore();
599
+ if (options.binding && !getBindingById(store, options.binding)) {
600
+ if (options.json)
601
+ process.stdout.write(`${JSON.stringify({ event: "error", code: "binding_not_found", binding: options.binding })}\n`);
602
+ else
603
+ process.stderr.write(`No binding "${options.binding}" on this machine.\n`);
604
+ store.close();
605
+ process.exitCode = 1;
606
+ return;
607
+ }
608
+ const emitter = options.json ? createWatchStreamEmitter(process.stdout) : null;
609
+ const onLog = options.json
610
+ ? undefined
611
+ : (line) => {
612
+ process.stderr.write(`${line}\n`);
613
+ };
614
+ const engine = createSyncEngine({
615
+ store,
616
+ bindingIds: options.binding ? [options.binding] : null,
617
+ emitter,
618
+ onLog,
619
+ });
620
+ const runtime = await buildInboundRuntime(store, globals);
621
+ if (runtime)
622
+ registerOutbound(engine, { baseUrl: runtime.baseUrl });
623
+ let stopping = false;
624
+ const onSignal = () => void shutdown();
625
+ const shutdown = async () => {
626
+ if (stopping)
627
+ return;
628
+ stopping = true;
629
+ process.off("SIGINT", onSignal);
630
+ process.off("SIGTERM", onSignal);
631
+ await engine.stop("signal");
632
+ try {
633
+ store.close();
634
+ }
635
+ catch {
636
+ }
637
+ process.exitCode = 0;
638
+ };
639
+ process.on("SIGINT", onSignal);
640
+ process.on("SIGTERM", onSignal);
641
+ if (!options.json && listActiveBindings(store).length === 0) {
642
+ process.stderr.write(`${ONBOARDING_POINTER}\n`);
643
+ }
644
+ await engine.start();
645
+ }