@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,547 @@
1
+ import { assertSafeNuStackUrl, assertSafeOpaqueId } from "./guards.js";
2
+ import { createSseParser } from "./sse_framing.js";
3
+ import { DEVICE_HEADER, knownDeviceId, refreshAccessTokenAfterRefusal, userAgent } from "./auth_grant.js";
4
+ import { getOrCreateDeviceId } from "./device_identity.js";
5
+ const DEFAULT_NUSTACK_URL = "https://apps.nustack.ai";
6
+ const REQUEST_TIMEOUT_MS = 10_000;
7
+ async function authorizedFetch(input, init = {}) {
8
+ const headers = new Headers(init.headers);
9
+ headers.set(DEVICE_HEADER, knownDeviceId() ?? (await getOrCreateDeviceId()));
10
+ headers.set("User-Agent", userAgent());
11
+ const response = await globalThis.fetch(input, { ...init, headers });
12
+ const bearer = headers.get("Authorization")?.replace(/^Bearer\s+/i, "");
13
+ if (response.status !== 401 || !bearer || isStreamBody(init.body))
14
+ return response;
15
+ const fresh = await refreshAccessTokenAfterRefusal(bearer).catch(() => null);
16
+ if (!fresh)
17
+ return response;
18
+ headers.set("Authorization", `Bearer ${fresh}`);
19
+ return globalThis.fetch(input, { ...init, headers });
20
+ }
21
+ function isStreamBody(body) {
22
+ return body != null && typeof body !== "string" && !(body instanceof ArrayBuffer) && !ArrayBuffer.isView(body) && !(body instanceof URLSearchParams);
23
+ }
24
+ const KEYS_PLANE = "the credential plane (/api/v1/keys)";
25
+ function errorMessage(payload, status, label, plane) {
26
+ if (plane && status === 404 && typeof payload?.error === "string") {
27
+ return `${label}: this NuStack deployment does not serve ${plane} — the server half of this command is not deployed, so it cannot succeed against this host.`;
28
+ }
29
+ const enveloped = typeof payload?.error === "object" ? payload.error?.message : undefined;
30
+ return enveloped ?? payload?.message ?? `${label}: HTTP ${status}`;
31
+ }
32
+ export function resolveNuStackUrl(options) {
33
+ return assertSafeNuStackUrl(options.nustackUrl ?? process.env.NUSTACK_URL ?? DEFAULT_NUSTACK_URL);
34
+ }
35
+ export async function fetchCatalogue(baseUrl) {
36
+ const response = await authorizedFetch(`${baseUrl}/api/catalog.json`, {
37
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
38
+ });
39
+ if (!response.ok) {
40
+ throw new Error(`catalogue fetch failed: ${response.status} ${response.statusText}`);
41
+ }
42
+ return (await response.json());
43
+ }
44
+ function narrowProjection(projection, service) {
45
+ if (!projection.endpoint || !projection.serviceKey || !projection.grantId) {
46
+ throw new Error(`the projection for '${service}' is incomplete`);
47
+ }
48
+ if (projection.environmentKey !== "dev") {
49
+ throw new Error(`refusing a '${projection.environmentKey}' credential for '${service}' — the CLI is development-plane only`);
50
+ }
51
+ return {
52
+ service: projection.service,
53
+ endpoint: projection.endpoint,
54
+ serviceKey: projection.serviceKey,
55
+ grantId: projection.grantId,
56
+ environmentKey: projection.environmentKey,
57
+ };
58
+ }
59
+ export async function exchangeServiceCredential(baseUrl, accessToken, params) {
60
+ const response = await authorizedFetch(`${baseUrl}/api/v1/keys/exchange`, {
61
+ method: "POST",
62
+ headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
63
+ body: JSON.stringify({ workspaceId: params.workspaceId, service: params.service }),
64
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
65
+ });
66
+ const payload = (await response.json().catch(() => null));
67
+ if (!response.ok || !payload?.serviceKey) {
68
+ throw new Error(errorMessage(payload, response.status, "exchange failed", KEYS_PLANE));
69
+ }
70
+ return narrowProjection(payload, params.service);
71
+ }
72
+ export async function fetchServiceCredential(baseUrl, accessToken, params) {
73
+ const url = new URL(`${baseUrl}/api/v1/keys/credentials`);
74
+ url.searchParams.set("workspaceId", assertSafeOpaqueId("The Workspace id", params.workspaceId));
75
+ url.searchParams.set("service", assertSafeOpaqueId("The Service slug", params.service));
76
+ const response = await authorizedFetch(url, {
77
+ headers: { Authorization: `Bearer ${accessToken}` },
78
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
79
+ });
80
+ const payload = (await response.json().catch(() => null));
81
+ if (!response.ok || !payload?.serviceKey) {
82
+ throw new Error(errorMessage(payload, response.status, `projection for '${params.service}' failed`, KEYS_PLANE));
83
+ }
84
+ return narrowProjection(payload, params.service);
85
+ }
86
+ export async function listWorkspaceIntegrations(baseUrl, accessToken, workspaceId) {
87
+ const url = new URL(`${baseUrl}/api/v1/keys/integrations`);
88
+ url.searchParams.set("workspaceId", assertSafeOpaqueId("The Workspace id", workspaceId));
89
+ const response = await authorizedFetch(url, {
90
+ headers: { Authorization: `Bearer ${accessToken}` },
91
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
92
+ });
93
+ const payload = (await response.json().catch(() => null));
94
+ if (!response.ok || !Array.isArray(payload?.integrations)) {
95
+ throw new Error(errorMessage(payload, response.status, "integration list failed", KEYS_PLANE));
96
+ }
97
+ return payload.integrations;
98
+ }
99
+ export async function fetchBrokeredCredential(baseUrl, accessToken, params) {
100
+ const service = assertSafeOpaqueId("The Service slug", params.service);
101
+ const response = await authorizedFetch(`${baseUrl}/api/v1/keys/broker/${encodeURIComponent(service)}`, {
102
+ method: "POST",
103
+ headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
104
+ body: JSON.stringify({ workspaceId: params.workspaceId }),
105
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
106
+ });
107
+ const payload = (await response.json().catch(() => null));
108
+ if (!response.ok || !payload?.key || !payload?.grantId) {
109
+ throw new Error(errorMessage(payload, response.status, "broker fetch failed", KEYS_PLANE));
110
+ }
111
+ return { url: payload.url ?? null, key: payload.key, meta: payload.meta ?? null, grantId: payload.grantId };
112
+ }
113
+ export async function searchWorkspaces(baseUrl, query, opts = {}) {
114
+ const url = new URL(`${baseUrl}/public/v1/workspaces`);
115
+ url.searchParams.set("q", query);
116
+ if (opts.category)
117
+ url.searchParams.set("category", opts.category);
118
+ if (opts.limit)
119
+ url.searchParams.set("limit", String(opts.limit));
120
+ const response = await authorizedFetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
121
+ const payload = (await response.json().catch(() => null));
122
+ if (!response.ok || !Array.isArray(payload?.data?.items)) {
123
+ throw new Error(errorMessage(payload, response.status, "workspace search failed"));
124
+ }
125
+ return payload.data.items;
126
+ }
127
+ export async function searchCatalogue(baseUrl, query, opts = {}) {
128
+ const url = new URL(`${baseUrl}/api/catalog/search`);
129
+ url.searchParams.set("q", query);
130
+ if (opts.type)
131
+ url.searchParams.set("type", opts.type);
132
+ if (opts.limit)
133
+ url.searchParams.set("limit", String(opts.limit));
134
+ const response = await authorizedFetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
135
+ const payload = (await response.json().catch(() => null));
136
+ if (!response.ok || !Array.isArray(payload?.results)) {
137
+ throw new Error(payload?.message ?? `service search failed: HTTP ${response.status}`);
138
+ }
139
+ return payload.results;
140
+ }
141
+ export async function fetchHandoffManifest(baseUrl, accessToken, params) {
142
+ const handoffId = assertSafeOpaqueId("The handoff id", params.handoffId);
143
+ const url = new URL(`${baseUrl}/api/v2/handoffs/${encodeURIComponent(handoffId)}/manifest`);
144
+ url.searchParams.set("bindingId", assertSafeOpaqueId("The binding id", params.bindingId));
145
+ const response = await authorizedFetch(url, {
146
+ headers: { Authorization: `Bearer ${accessToken}` },
147
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
148
+ });
149
+ const payload = (await response.json().catch(() => null));
150
+ if (!response.ok || !payload?.data?.manifestSha256) {
151
+ throw new Error(errorMessage(payload, response.status, "handoff manifest fetch failed"));
152
+ }
153
+ return payload.data;
154
+ }
155
+ export async function downloadTargetContent(baseUrl, accessToken, contentUrl) {
156
+ if (!contentUrl.startsWith("/")) {
157
+ throw new Error("target content URL must be a server-relative path");
158
+ }
159
+ const response = await authorizedFetch(`${baseUrl}${contentUrl}`, {
160
+ headers: { Authorization: `Bearer ${accessToken}` },
161
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
162
+ });
163
+ if (!response.ok) {
164
+ throw new Error(`target content fetch failed: HTTP ${response.status}`);
165
+ }
166
+ return new Uint8Array(await response.arrayBuffer());
167
+ }
168
+ export async function fetchBindingEvents(baseUrl, accessToken, params) {
169
+ const bindingId = assertSafeOpaqueId("The binding id", params.bindingId);
170
+ const url = new URL(`${baseUrl}/api/v2/bindings/${encodeURIComponent(bindingId)}/events`);
171
+ url.searchParams.set("after", String(Math.max(0, Math.trunc(params.after))));
172
+ if (params.limit)
173
+ url.searchParams.set("limit", String(params.limit));
174
+ const response = await authorizedFetch(url, {
175
+ headers: { Authorization: `Bearer ${accessToken}` },
176
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
177
+ });
178
+ const payload = (await response.json().catch(() => null));
179
+ if (!response.ok || !Array.isArray(payload?.data?.items)) {
180
+ throw new Error(errorMessage(payload, response.status, "binding events fetch failed"));
181
+ }
182
+ return payload.data.items;
183
+ }
184
+ export async function acknowledgeBindingCursor(baseUrl, accessToken, params) {
185
+ const bindingId = assertSafeOpaqueId("The binding id", params.bindingId);
186
+ const response = await authorizedFetch(`${baseUrl}/api/v2/bindings/${encodeURIComponent(bindingId)}/cursor`, {
187
+ method: "POST",
188
+ headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
189
+ body: JSON.stringify({ lastAppliedSequence: Math.max(0, Math.trunc(params.lastAppliedSequence)) }),
190
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
191
+ });
192
+ const payload = (await response.json().catch(() => null));
193
+ if (!response.ok || typeof payload?.data?.lastAppliedWorkspaceSequence !== "number") {
194
+ throw new Error(errorMessage(payload, response.status, "cursor acknowledgement failed"));
195
+ }
196
+ return payload.data.lastAppliedWorkspaceSequence;
197
+ }
198
+ export async function fetchSyncWhitelist(baseUrl, accessToken, bindingId) {
199
+ const safeId = assertSafeOpaqueId("The binding id", bindingId);
200
+ const response = await authorizedFetch(`${baseUrl}/api/v2/bindings/${encodeURIComponent(safeId)}/sync-whitelist`, {
201
+ headers: { Authorization: `Bearer ${accessToken}` },
202
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
203
+ });
204
+ const payload = (await response.json().catch(() => null));
205
+ if (!response.ok || !payload?.data?.manifestSha256) {
206
+ throw new Error(errorMessage(payload, response.status, "sync whitelist fetch failed"));
207
+ }
208
+ return payload.data;
209
+ }
210
+ export async function reportApplicationDecision(baseUrl, accessToken, report) {
211
+ const response = await postApplicationReport(baseUrl, accessToken, report, "decision", {
212
+ decision: report.decision,
213
+ localStateFingerprint: report.localStateFingerprint,
214
+ affectedPaths: report.affectedPaths,
215
+ });
216
+ const payload = (await response.json().catch(() => null));
217
+ if (response.status === 409 && payload?.error?.code === "HANDOFF_SUPERSEDED") {
218
+ return { ok: false, code: "HANDOFF_SUPERSEDED" };
219
+ }
220
+ if (!response.ok || payload?.data?.recorded !== true) {
221
+ throw new Error(errorMessage(payload, response.status, "decision report failed"));
222
+ }
223
+ return { ok: true };
224
+ }
225
+ export async function reportApplicationResult(baseUrl, accessToken, report) {
226
+ const response = await postApplicationReport(baseUrl, accessToken, report, "result", {
227
+ commitId: report.commitId,
228
+ status: report.status,
229
+ manifestHash: report.manifestHash ?? null,
230
+ noPush: true,
231
+ diagnostics: report.diagnostics ?? null,
232
+ affectedPaths: report.affectedPaths ?? [],
233
+ });
234
+ await assertReportRecorded(response, "result report failed");
235
+ }
236
+ export async function reportApplicationUndoResult(baseUrl, accessToken, report) {
237
+ const response = await postApplicationReport(baseUrl, accessToken, report, "undo-result", {
238
+ outcome: report.outcome,
239
+ affectedPaths: report.affectedPaths,
240
+ commitId: report.commitId ?? null,
241
+ });
242
+ await assertReportRecorded(response, "undo-result report failed");
243
+ }
244
+ function postApplicationReport(baseUrl, accessToken, report, action, body) {
245
+ const bindingId = assertSafeOpaqueId("The binding id", report.bindingId);
246
+ const handoffId = assertSafeOpaqueId("The handoff id", report.handoffId);
247
+ return authorizedFetch(`${baseUrl}/api/v2/bindings/${encodeURIComponent(bindingId)}/handoff-applications/${encodeURIComponent(handoffId)}/${action}`, {
248
+ method: "POST",
249
+ headers: {
250
+ Authorization: `Bearer ${accessToken}`,
251
+ "Content-Type": "application/json",
252
+ "Idempotency-Key": report.idempotencyKey,
253
+ },
254
+ body: JSON.stringify(body),
255
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
256
+ });
257
+ }
258
+ async function assertReportRecorded(response, label) {
259
+ const payload = (await response.json().catch(() => null));
260
+ if (!response.ok || payload?.data?.recorded !== true) {
261
+ throw new Error(errorMessage(payload, response.status, label));
262
+ }
263
+ }
264
+ function authHeaders(accessToken) {
265
+ return { Authorization: `Bearer ${accessToken}` };
266
+ }
267
+ function mutationHeaders(accessToken, idempotencyKey) {
268
+ return {
269
+ Authorization: `Bearer ${accessToken}`,
270
+ "Content-Type": "application/json",
271
+ "Idempotency-Key": idempotencyKey,
272
+ };
273
+ }
274
+ export async function listOrganizations(baseUrl, accessToken) {
275
+ const response = await authorizedFetch(`${baseUrl}/api/v2/organizations`, {
276
+ headers: authHeaders(accessToken),
277
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
278
+ });
279
+ const payload = (await response.json().catch(() => null));
280
+ if (!response.ok || !Array.isArray(payload?.data?.items)) {
281
+ throw new Error(errorMessage(payload, response.status, "organization list failed"));
282
+ }
283
+ return payload.data.items;
284
+ }
285
+ export async function createOrganization(baseUrl, accessToken, params, idempotencyKey) {
286
+ const response = await authorizedFetch(`${baseUrl}/api/v2/organizations`, {
287
+ method: "POST",
288
+ headers: mutationHeaders(accessToken, idempotencyKey),
289
+ body: JSON.stringify({ label: params.label, ...(params.slug ? { slug: params.slug } : {}) }),
290
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
291
+ });
292
+ const payload = (await response.json().catch(() => null));
293
+ if (!response.ok || !payload?.data?.id) {
294
+ throw new Error(errorMessage(payload, response.status, "organization create failed"));
295
+ }
296
+ return payload.data;
297
+ }
298
+ export async function listWorkspaces(baseUrl, accessToken, organizationId) {
299
+ const safeId = assertSafeOpaqueId("The Organization id", organizationId);
300
+ const response = await authorizedFetch(`${baseUrl}/api/v2/workspaces`, {
301
+ headers: { ...authHeaders(accessToken), "X-NuStack-Organization": safeId },
302
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
303
+ });
304
+ const payload = (await response.json().catch(() => null));
305
+ if (!response.ok || !Array.isArray(payload?.data?.items)) {
306
+ throw new Error(errorMessage(payload, response.status, "workspace list failed"));
307
+ }
308
+ return payload.data.items;
309
+ }
310
+ export async function getWorkspace(baseUrl, accessToken, workspaceId) {
311
+ const safeId = assertSafeOpaqueId("The Workspace id", workspaceId);
312
+ const response = await authorizedFetch(`${baseUrl}/api/v2/workspaces/${encodeURIComponent(safeId)}`, {
313
+ headers: authHeaders(accessToken),
314
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
315
+ });
316
+ const payload = (await response.json().catch(() => null));
317
+ if (!response.ok || !payload?.data?.id) {
318
+ throw new Error(errorMessage(payload, response.status, "workspace fetch failed"));
319
+ }
320
+ return payload.data;
321
+ }
322
+ export function mintWorkspaceId() {
323
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
324
+ const ms = Date.now();
325
+ bytes[0] = Math.floor(ms / 2 ** 40) & 0xff;
326
+ bytes[1] = Math.floor(ms / 2 ** 32) & 0xff;
327
+ bytes[2] = Math.floor(ms / 2 ** 24) & 0xff;
328
+ bytes[3] = Math.floor(ms / 2 ** 16) & 0xff;
329
+ bytes[4] = Math.floor(ms / 2 ** 8) & 0xff;
330
+ bytes[5] = ms & 0xff;
331
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x70;
332
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
333
+ let hex = "";
334
+ for (const byte of bytes)
335
+ hex += byte.toString(16).padStart(2, "0");
336
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
337
+ }
338
+ async function* readSseFrames(body) {
339
+ const reader = body.getReader();
340
+ const decoder = new TextDecoder();
341
+ const parser = createSseParser();
342
+ const toFrame = (block) => {
343
+ let data = {};
344
+ try {
345
+ const parsed = JSON.parse(block.data);
346
+ if (parsed && typeof parsed === "object")
347
+ data = parsed;
348
+ }
349
+ catch {
350
+ }
351
+ return { event: block.event, data };
352
+ };
353
+ for (;;) {
354
+ const { done, value } = await reader.read();
355
+ if (done)
356
+ break;
357
+ for (const block of parser.push(decoder.decode(value, { stream: true })))
358
+ yield toFrame(block);
359
+ }
360
+ for (const block of parser.flush())
361
+ yield toFrame(block);
362
+ }
363
+ export async function createWorkspaceViaAsk(baseUrl, accessToken, organizationId, prompt, opts = {}) {
364
+ const safeOrganization = assertSafeOpaqueId("The Organization id", organizationId);
365
+ const workspaceId = opts.workspaceId ?? mintWorkspaceId();
366
+ const fetchImpl = opts.fetchImpl ?? fetch;
367
+ const response = await fetchImpl(`${baseUrl}/api/v2/ask`, {
368
+ method: "POST",
369
+ headers: {
370
+ ...authHeaders(accessToken),
371
+ "Content-Type": "application/json",
372
+ "X-NuStack-Organization": safeOrganization,
373
+ },
374
+ body: JSON.stringify({ prompt, workspaceId }),
375
+ });
376
+ if (!response.ok || !response.body) {
377
+ const payload = (await response.json().catch(() => null));
378
+ throw new Error(errorMessage(payload, response.status, "workspace create failed"));
379
+ }
380
+ let started = false;
381
+ let turnId = null;
382
+ let generationError = null;
383
+ for await (const frame of readSseFrames(response.body)) {
384
+ if (frame.event === "start") {
385
+ started = true;
386
+ turnId = typeof frame.data.turnId === "string" ? frame.data.turnId : null;
387
+ opts.onPhase?.("Workspace created — writing the first document");
388
+ }
389
+ else if (frame.event === "thinking" && typeof frame.data.message === "string") {
390
+ opts.onPhase?.(frame.data.message);
391
+ }
392
+ else if (frame.event === "error") {
393
+ const message = typeof frame.data.message === "string" ? frame.data.message : "generation failed";
394
+ if (!started)
395
+ throw new Error(`workspace create failed: ${message}`);
396
+ generationError = message;
397
+ break;
398
+ }
399
+ else if (frame.event === "done") {
400
+ break;
401
+ }
402
+ }
403
+ if (!started)
404
+ throw new Error("workspace create failed: the stream ended before the Workspace was reported");
405
+ return { workspaceId, turnId, generationError };
406
+ }
407
+ async function postWorkspacePublication(baseUrl, accessToken, workspaceId, action, body, idempotencyKey) {
408
+ const safeId = assertSafeOpaqueId("The Workspace id", workspaceId);
409
+ const response = await authorizedFetch(`${baseUrl}/api/v2/workspaces/${encodeURIComponent(safeId)}/${action}`, {
410
+ method: "POST",
411
+ headers: mutationHeaders(accessToken, idempotencyKey),
412
+ body: JSON.stringify(body),
413
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
414
+ });
415
+ const payload = (await response.json().catch(() => null));
416
+ if (!response.ok || payload?.data === undefined) {
417
+ throw new Error(errorMessage(payload, response.status, `workspace ${action} failed`));
418
+ }
419
+ return payload.data;
420
+ }
421
+ export async function publishWorkspace(baseUrl, accessToken, workspaceId, profile, idempotencyKey) {
422
+ return postWorkspacePublication(baseUrl, accessToken, workspaceId, "publish", profile, idempotencyKey);
423
+ }
424
+ export async function unpublishWorkspace(baseUrl, accessToken, workspaceId, idempotencyKey) {
425
+ return postWorkspacePublication(baseUrl, accessToken, workspaceId, "unpublish", {}, idempotencyKey);
426
+ }
427
+ export async function listDocuments(baseUrl, accessToken, workspaceId) {
428
+ const safeId = assertSafeOpaqueId("The Workspace id", workspaceId);
429
+ const response = await authorizedFetch(`${baseUrl}/api/v2/documents?workspaceId=${encodeURIComponent(safeId)}`, {
430
+ headers: authHeaders(accessToken),
431
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
432
+ });
433
+ const payload = (await response.json().catch(() => null));
434
+ if (!response.ok || !Array.isArray(payload?.data?.items)) {
435
+ throw new Error(errorMessage(payload, response.status, "document list failed"));
436
+ }
437
+ return payload.data.items;
438
+ }
439
+ export async function getDocument(baseUrl, accessToken, documentId) {
440
+ const safeId = assertSafeOpaqueId("The Document id", documentId);
441
+ const response = await authorizedFetch(`${baseUrl}/api/v2/documents/${encodeURIComponent(safeId)}`, {
442
+ headers: authHeaders(accessToken),
443
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
444
+ });
445
+ const payload = (await response.json().catch(() => null));
446
+ if (!response.ok || !payload?.data?.document?.id) {
447
+ throw new Error(errorMessage(payload, response.status, "document fetch failed"));
448
+ }
449
+ return payload.data;
450
+ }
451
+ export async function listHandoffs(baseUrl, accessToken, workspaceId) {
452
+ const safeId = assertSafeOpaqueId("The Workspace id", workspaceId);
453
+ const response = await authorizedFetch(`${baseUrl}/api/v2/workspaces/${encodeURIComponent(safeId)}/handoffs`, {
454
+ headers: authHeaders(accessToken),
455
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
456
+ });
457
+ const payload = (await response.json().catch(() => null));
458
+ if (!response.ok || !Array.isArray(payload?.data?.items)) {
459
+ throw new Error(errorMessage(payload, response.status, "handoff list failed"));
460
+ }
461
+ return payload.data.items;
462
+ }
463
+ export async function getHandoff(baseUrl, accessToken, handoffId) {
464
+ const safeId = assertSafeOpaqueId("The handoff id", handoffId);
465
+ const response = await authorizedFetch(`${baseUrl}/api/v2/handoffs/${encodeURIComponent(safeId)}`, {
466
+ headers: authHeaders(accessToken),
467
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
468
+ });
469
+ const payload = (await response.json().catch(() => null));
470
+ if (!response.ok || !payload?.data?.id) {
471
+ throw new Error(errorMessage(payload, response.status, "handoff fetch failed"));
472
+ }
473
+ return payload.data;
474
+ }
475
+ export async function latestHandoff(baseUrl, accessToken, workspaceId) {
476
+ const handoffs = await listHandoffs(baseUrl, accessToken, workspaceId);
477
+ return handoffs.length > 0 ? handoffs[0] : null;
478
+ }
479
+ export async function createBinding(baseUrl, accessToken, workspaceId, input, idempotencyKey) {
480
+ const safeId = assertSafeOpaqueId("The Workspace id", workspaceId);
481
+ const response = await authorizedFetch(`${baseUrl}/api/v2/workspaces/${encodeURIComponent(safeId)}/bindings`, {
482
+ method: "POST",
483
+ headers: mutationHeaders(accessToken, idempotencyKey),
484
+ body: JSON.stringify({
485
+ repositoryFingerprint: input.repositoryFingerprint,
486
+ boundBranch: input.boundBranch,
487
+ protocolVersion: input.protocolVersion,
488
+ ...(input.displayName ? { displayName: input.displayName } : {}),
489
+ }),
490
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
491
+ });
492
+ const payload = (await response.json().catch(() => null));
493
+ if (!response.ok || !payload?.data?.id) {
494
+ throw new Error(errorMessage(payload, response.status, "binding create failed"));
495
+ }
496
+ return payload.data;
497
+ }
498
+ export async function getBinding(baseUrl, accessToken, bindingId) {
499
+ const safeId = assertSafeOpaqueId("The binding id", bindingId);
500
+ const response = await authorizedFetch(`${baseUrl}/api/v2/bindings/${encodeURIComponent(safeId)}`, {
501
+ headers: authHeaders(accessToken),
502
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
503
+ });
504
+ const payload = (await response.json().catch(() => null));
505
+ if (!response.ok || !payload?.data?.id) {
506
+ throw new Error(errorMessage(payload, response.status, "binding fetch failed"));
507
+ }
508
+ return payload.data;
509
+ }
510
+ export async function deleteBinding(baseUrl, accessToken, bindingId) {
511
+ const safeId = assertSafeOpaqueId("The binding id", bindingId);
512
+ const response = await authorizedFetch(`${baseUrl}/api/v2/bindings/${encodeURIComponent(safeId)}`, {
513
+ method: "DELETE",
514
+ headers: authHeaders(accessToken),
515
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
516
+ });
517
+ const payload = (await response.json().catch(() => null));
518
+ if (!response.ok || !payload?.data?.id) {
519
+ throw new Error(errorMessage(payload, response.status, "binding remove failed"));
520
+ }
521
+ return { id: payload.data.id, status: payload.data.status ?? "removed" };
522
+ }
523
+ export async function postBindingHealth(baseUrl, accessToken, bindingId, report) {
524
+ const safeId = assertSafeOpaqueId("The binding id", bindingId);
525
+ const response = await authorizedFetch(`${baseUrl}/api/v2/bindings/${encodeURIComponent(safeId)}/health`, {
526
+ method: "POST",
527
+ headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
528
+ body: JSON.stringify(report),
529
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
530
+ });
531
+ const payload = (await response.json().catch(() => null));
532
+ if (!response.ok || !payload?.data?.id) {
533
+ throw new Error(errorMessage(payload, response.status, "binding health failed"));
534
+ }
535
+ return payload.data;
536
+ }
537
+ export async function listDevices(baseUrl, accessToken) {
538
+ const response = await authorizedFetch(`${baseUrl}/api/v2/devices`, {
539
+ headers: { Authorization: `Bearer ${accessToken}` },
540
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
541
+ });
542
+ const payload = (await response.json().catch(() => null));
543
+ if (!response.ok || !Array.isArray(payload?.data?.items)) {
544
+ throw new Error(errorMessage(payload, response.status, "device list failed"));
545
+ }
546
+ return payload.data.items;
547
+ }
@@ -0,0 +1,17 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export function nustackDir() {
5
+ return path.join(os.homedir(), ".nustack");
6
+ }
7
+ function legacyDeviceFilePath() {
8
+ return path.join(nustackDir(), "device.json");
9
+ }
10
+ export async function readLegacyDeviceRecord() {
11
+ try {
12
+ return JSON.parse(await readFile(legacyDeviceFilePath(), "utf8"));
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
@@ -0,0 +1,65 @@
1
+ import { readHeadCommit } from "../git-sync/core/repository.js";
2
+ import { readSymbolicBranch } from "../git-sync/core/repository.js";
3
+ import { reconcileInboundBinding } from "../git-sync/inbound/index.js";
4
+ import { fetchToolkitProfile } from "../etna/profile.js";
5
+ import { classifyFolder, attachRejection } from "./folder_state.js";
6
+ import { runRepositoryOnboarding, } from "./binding.js";
7
+ export async function attachExistingGit(ctx, options) {
8
+ const state = classifyFolder(ctx.cwd);
9
+ if (state.class !== "git_worktree" || !state.repository || !state.git) {
10
+ return { ok: false, code: "NOT_A_WORKTREE", message: "`nustack workspace attach` needs an existing Git worktree — run `nustack init` to establish one." };
11
+ }
12
+ const rejection = attachRejection(state.git);
13
+ if (rejection) {
14
+ return { ok: false, code: rejection.toUpperCase(), message: rejectionMessage(rejection) };
15
+ }
16
+ if (state.git.markerWorkspaceId && state.git.markerWorkspaceId !== options.workspaceId && !options.confirmRebind) {
17
+ return {
18
+ ok: false,
19
+ code: "REBIND_CONFIRMATION_REQUIRED",
20
+ message: `This repository is bound to Workspace ${state.git.markerWorkspaceId}. Re-run with confirmation to rebind it to ${options.workspaceId}.`,
21
+ };
22
+ }
23
+ const boundBranch = readSymbolicBranch(state.repository);
24
+ if (!boundBranch) {
25
+ return { ok: false, code: "DETACHED_HEAD", message: "HEAD is detached — check out a branch before attaching." };
26
+ }
27
+ const profile = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, options.workspaceId);
28
+ const baselineCommitId = readHeadCommit(state.repository);
29
+ const result = await runRepositoryOnboarding({
30
+ store: ctx.store,
31
+ baseUrl: ctx.baseUrl,
32
+ accessToken: ctx.accessToken,
33
+ deviceRef: ctx.deviceRef,
34
+ flow: "existing_git",
35
+ workspaceId: options.workspaceId,
36
+ organizationId: options.organizationId,
37
+ repository: state.repository,
38
+ boundBranch,
39
+ profile,
40
+ baselineCommitId,
41
+ toolkitDeps: ctx.toolkitDeps,
42
+ runTelemetry: ctx.runTelemetry,
43
+ });
44
+ await reconcileLatest(ctx, result.bindingId);
45
+ return { ok: true, result };
46
+ }
47
+ async function reconcileLatest(ctx, bindingId) {
48
+ const runtime = {
49
+ baseUrl: ctx.baseUrl,
50
+ accessToken: ctx.accessToken,
51
+ userId: ctx.userId,
52
+ deviceRef: ctx.deviceRef,
53
+ };
54
+ await reconcileInboundBinding(ctx.store, runtime, bindingId).catch(() => { });
55
+ }
56
+ function rejectionMessage(rejection) {
57
+ switch (rejection) {
58
+ case "nested_workspace":
59
+ return "This repository is nested inside another NuStack Workspace — one Workspace has one logical repository root (F-014).";
60
+ case "operation_in_progress":
61
+ return "A merge, rebase, cherry-pick, or revert is in progress — finish or abort it, then re-attach.";
62
+ case "detached_head":
63
+ return "HEAD is detached — check out a branch before attaching.";
64
+ }
65
+ }