@nurix/nustack 0.10.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/dist/commands/add.js +130 -0
- package/dist/commands/agent.js +100 -0
- package/dist/commands/bootstrap.js +54 -0
- package/dist/commands/describe.js +34 -0
- package/dist/commands/device.js +159 -0
- package/dist/commands/docs.js +119 -0
- package/dist/commands/doctor.js +284 -0
- package/dist/commands/graph.js +34 -0
- package/dist/commands/handoff.js +108 -0
- package/dist/commands/init.js +280 -0
- package/dist/commands/lane.js +12 -0
- package/dist/commands/list.js +20 -0
- package/dist/commands/login.js +134 -0
- package/dist/commands/logout.js +44 -0
- package/dist/commands/open.js +54 -0
- package/dist/commands/organization.js +114 -0
- package/dist/commands/review.js +152 -0
- package/dist/commands/search.js +57 -0
- package/dist/commands/sessions.js +62 -0
- package/dist/commands/sync.js +85 -0
- package/dist/commands/telemetry.js +112 -0
- package/dist/commands/toolkit.js +167 -0
- package/dist/commands/validate.js +67 -0
- package/dist/commands/workspace-sync.js +645 -0
- package/dist/commands/workspace.js +479 -0
- package/dist/index.js +357 -0
- package/dist/lib/agent/claudeHooks.js +77 -0
- package/dist/lib/auth_grant.js +398 -0
- package/dist/lib/auth_recovery.js +49 -0
- package/dist/lib/browser_opener.js +24 -0
- package/dist/lib/device_identity.js +28 -0
- package/dist/lib/device_keychain.js +101 -0
- package/dist/lib/env_file.js +75 -0
- package/dist/lib/etna/manager.js +43 -0
- package/dist/lib/etna/profile.js +41 -0
- package/dist/lib/etna/reconcile.js +77 -0
- package/dist/lib/filebeat_installer.js +246 -0
- package/dist/lib/git-sync/core/commit_range.js +37 -0
- package/dist/lib/git-sync/core/engine.js +263 -0
- package/dist/lib/git-sync/core/git_runner.js +137 -0
- package/dist/lib/git-sync/core/operation_state.js +22 -0
- package/dist/lib/git-sync/core/path_normalizer.js +98 -0
- package/dist/lib/git-sync/core/reconciliation_lock.js +103 -0
- package/dist/lib/git-sync/core/repository.js +49 -0
- package/dist/lib/git-sync/core/watch_stream.js +86 -0
- package/dist/lib/git-sync/core/watcher.js +61 -0
- package/dist/lib/git-sync/inbound/applier.js +131 -0
- package/dist/lib/git-sync/inbound/apply_commit.js +225 -0
- package/dist/lib/git-sync/inbound/approval.js +117 -0
- package/dist/lib/git-sync/inbound/content_decision.js +148 -0
- package/dist/lib/git-sync/inbound/download.js +223 -0
- package/dist/lib/git-sync/inbound/event_sync.js +133 -0
- package/dist/lib/git-sync/inbound/index.js +10 -0
- package/dist/lib/git-sync/inbound/notification.js +39 -0
- package/dist/lib/git-sync/inbound/state_fingerprint.js +32 -0
- package/dist/lib/git-sync/inbound/technical_wait.js +39 -0
- package/dist/lib/git-sync/inbound/undo.js +198 -0
- package/dist/lib/git-sync/outbound/extraction.js +107 -0
- package/dist/lib/git-sync/outbound/history_decision.js +112 -0
- package/dist/lib/git-sync/outbound/index.js +26 -0
- package/dist/lib/git-sync/outbound/manifest_builder.js +58 -0
- package/dist/lib/git-sync/outbound/publisher.js +51 -0
- package/dist/lib/git-sync/outbound/queue.js +123 -0
- package/dist/lib/git-sync/outbound/state.js +12 -0
- package/dist/lib/git-sync/outbound/upload_client.js +141 -0
- package/dist/lib/git-sync/recovery/bundle.js +56 -0
- package/dist/lib/git-sync/recovery/crypto.js +114 -0
- package/dist/lib/git-sync/recovery/index.js +3 -0
- package/dist/lib/git-sync/recovery/store.js +55 -0
- package/dist/lib/git-sync/whitelist/index.js +4 -0
- package/dist/lib/git-sync/whitelist/matcher.js +62 -0
- package/dist/lib/git-sync/whitelist/store.js +99 -0
- package/dist/lib/git-sync/whitelist/types.js +46 -0
- package/dist/lib/git-sync/whitelist/validation.js +60 -0
- package/dist/lib/graph/answer.js +171 -0
- package/dist/lib/graph/freshness.js +29 -0
- package/dist/lib/graph/library.js +19 -0
- package/dist/lib/guards.js +106 -0
- package/dist/lib/interactive.js +12 -0
- package/dist/lib/json_output.js +8 -0
- package/dist/lib/lanes/claudeAdapter.js +135 -0
- package/dist/lib/lanes/frames.js +73 -0
- package/dist/lib/lanes/supervisor.js +247 -0
- package/dist/lib/lanes/worktree.js +33 -0
- package/dist/lib/local_store/accepted_uploads.js +49 -0
- package/dist/lib/local_store/bindings.js +163 -0
- package/dist/lib/local_store/commit_origins.js +25 -0
- package/dist/lib/local_store/database.js +71 -0
- package/dist/lib/local_store/device_state.js +72 -0
- package/dist/lib/local_store/filebeat_state.js +133 -0
- package/dist/lib/local_store/handoff_applications.js +166 -0
- package/dist/lib/local_store/history_decisions.js +48 -0
- package/dist/lib/local_store/index.js +14 -0
- package/dist/lib/local_store/migrations.js +240 -0
- package/dist/lib/local_store/paths.js +24 -0
- package/dist/lib/local_store/pending_uploads.js +76 -0
- package/dist/lib/local_store/recovery_bundles.js +66 -0
- package/dist/lib/local_store/sync_vocabulary.js +33 -0
- package/dist/lib/local_store/workspace_events_cache.js +54 -0
- package/dist/lib/loopback.js +78 -0
- package/dist/lib/manifest.js +83 -0
- package/dist/lib/nustack_client.js +547 -0
- package/dist/lib/nustack_store.js +17 -0
- package/dist/lib/onboarding/attach_existing_git.js +65 -0
- package/dist/lib/onboarding/binding.js +91 -0
- package/dist/lib/onboarding/folder_state.js +99 -0
- package/dist/lib/onboarding/git_init.js +10 -0
- package/dist/lib/onboarding/handoff_apply.js +68 -0
- package/dist/lib/onboarding/init_fresh_folder.js +100 -0
- package/dist/lib/onboarding/manifest_writer.js +161 -0
- package/dist/lib/onboarding/web_first_attach.js +133 -0
- package/dist/lib/package_manager.js +23 -0
- package/dist/lib/review/client.js +66 -0
- package/dist/lib/schema_validator.js +24 -0
- package/dist/lib/secure_file.js +10 -0
- package/dist/lib/session_telemetry.js +263 -0
- package/dist/lib/sessions/client.js +48 -0
- package/dist/lib/sessions/outsideSessions.js +54 -0
- package/dist/lib/sse_framing.js +48 -0
- package/dist/lib/telemetry/config_renderer.js +175 -0
- package/dist/lib/telemetry/health_report.js +173 -0
- package/dist/lib/telemetry/provider_paths.js +38 -0
- package/dist/lib/telemetry/redaction_policy.js +90 -0
- package/dist/lib/telemetry/reload.js +84 -0
- package/dist/lib/workspace_context.js +121 -0
- package/dist/spec_schema.json +422 -0
- package/dist/wire/auth.js +1 -0
- package/dist/wire/sync-paths.js +25 -0
- package/dist/wire/telemetry.js +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import { createWorkspaceViaAsk, getWorkspace, listWorkspaces, listOrganizations, publishWorkspace, resolveNuStackUrl, unpublishWorkspace, } from "../lib/nustack_client.js";
|
|
5
|
+
import { ensureAccessToken } from "../lib/auth_grant.js";
|
|
6
|
+
import { ensureAuthWithRecovery, lastAuthFailure } from "../lib/auth_recovery.js";
|
|
7
|
+
import { resolveWorkspaceId, resolveOrganizationId } from "../lib/workspace_context.js";
|
|
8
|
+
import { readActiveOrganizationContext } from "./organization.js";
|
|
9
|
+
import { confirmOrFallback, isInteractive } from "../lib/interactive.js";
|
|
10
|
+
import { launchBrowser } from "../lib/browser_opener.js";
|
|
11
|
+
import { assertSafeBrowserUrl } from "../lib/guards.js";
|
|
12
|
+
import { openLocalStore } from "../lib/local_store/database.js";
|
|
13
|
+
import { readDeviceState } from "../lib/local_store/device_state.js";
|
|
14
|
+
import { getBindingByRepositoryRoot, listBindings } from "../lib/local_store/bindings.js";
|
|
15
|
+
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
16
|
+
import { readWorkspaceManifests } from "../lib/onboarding/manifest_writer.js";
|
|
17
|
+
import { hasManifest, scaffoldManifest } from "../lib/manifest.js";
|
|
18
|
+
import { toolkitCompatibility } from "../lib/etna/reconcile.js";
|
|
19
|
+
import { attachExistingGit } from "../lib/onboarding/attach_existing_git.js";
|
|
20
|
+
import { webFirstAttach } from "../lib/onboarding/web_first_attach.js";
|
|
21
|
+
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
22
|
+
function emitFail(command, json, code, message) {
|
|
23
|
+
if (json)
|
|
24
|
+
printJsonError(command, { code, message });
|
|
25
|
+
else {
|
|
26
|
+
console.error(message);
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function requireAuth(command, json, globals) {
|
|
31
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
32
|
+
const deviceAuth = await ensureAuthWithRecovery(baseUrl, json);
|
|
33
|
+
if (!deviceAuth) {
|
|
34
|
+
const failure = lastAuthFailure();
|
|
35
|
+
emitFail(command, json, failure.code, failure.message);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return { baseUrl, accessToken: deviceAuth.accessToken };
|
|
39
|
+
}
|
|
40
|
+
async function resolveOrganizationForWorkspace(auth, options) {
|
|
41
|
+
const memberships = await listOrganizations(auth.baseUrl, auth.accessToken);
|
|
42
|
+
const views = memberships.map((m) => ({ id: m.organization.id, slug: m.organization.slug }));
|
|
43
|
+
const active = await readActiveOrganizationContext();
|
|
44
|
+
const resolution = resolveOrganizationId({ organization: options.organization }, active, views);
|
|
45
|
+
return resolution.ok ? { ok: true, organizationId: resolution.organizationId } : { ok: false, message: resolution.message };
|
|
46
|
+
}
|
|
47
|
+
export async function runWorkspaceList(options, globals) {
|
|
48
|
+
const json = options.json === true;
|
|
49
|
+
const auth = await requireAuth("workspace list", json, globals);
|
|
50
|
+
if (!auth)
|
|
51
|
+
return;
|
|
52
|
+
const ws = await resolveOrganizationForWorkspace(auth, options);
|
|
53
|
+
if (!ws.ok) {
|
|
54
|
+
emitFail("workspace list", json, "ORGANIZATION_CONTEXT_MISSING", ws.message);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const workspaces = await listWorkspaces(auth.baseUrl, auth.accessToken, ws.organizationId);
|
|
58
|
+
if (json) {
|
|
59
|
+
printJsonSuccess("workspace list", { organizationId: ws.organizationId, workspaces });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (workspaces.length === 0) {
|
|
63
|
+
console.log("No Workspaces in this Organization. Create one with `nustack workspace create`.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const workspace of workspaces) {
|
|
67
|
+
console.log(`${workspace.id.padEnd(24)} ${workspace.status.padEnd(12)} ${workspace.publicationState.padEnd(12)} ${workspace.label}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export async function runWorkspaceCreate(options, globals) {
|
|
71
|
+
const json = options.json === true;
|
|
72
|
+
const auth = await requireAuth("workspace create", json, globals);
|
|
73
|
+
if (!auth)
|
|
74
|
+
return;
|
|
75
|
+
const ws = await resolveOrganizationForWorkspace(auth, options);
|
|
76
|
+
if (!ws.ok) {
|
|
77
|
+
emitFail("workspace create", json, "ORGANIZATION_CONTEXT_MISSING", ws.message);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const gathered = await gatherCreatePrompt(options, json);
|
|
81
|
+
if (!gathered.ok) {
|
|
82
|
+
emitFail("workspace create", json, "VALIDATION_FAILED", gathered.message);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const workspace = await createWorkspaceFromPrompt(auth, ws.organizationId, gathered.prompt, "workspace create", json);
|
|
86
|
+
if (!workspace)
|
|
87
|
+
return;
|
|
88
|
+
if (json) {
|
|
89
|
+
printJsonSuccess("workspace create", { workspace, organizationId: ws.organizationId });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
console.log(`Created Workspace ${workspace.label} (${workspace.id}), status ${workspace.status}.`);
|
|
93
|
+
console.log(`Attach a local folder with \`nustack workspace attach --workspace ${workspace.id}\`.`);
|
|
94
|
+
if (isInteractive() && (await confirmOrFallback("Attach the current folder to this Workspace now?", false))) {
|
|
95
|
+
const store = await openLocalStore();
|
|
96
|
+
try {
|
|
97
|
+
const ctx = buildOnboardingContext(auth, store, json, options.yes === true);
|
|
98
|
+
const outcome = await webFirstAttach(ctx, {
|
|
99
|
+
workspaceId: workspace.id,
|
|
100
|
+
organizationId: ws.organizationId,
|
|
101
|
+
confirmPopulated: options.yes === true,
|
|
102
|
+
});
|
|
103
|
+
reportWebFirst("workspace create", outcome, json);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
store.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function runWorkspaceAttach(options, globals) {
|
|
111
|
+
const json = options.json === true;
|
|
112
|
+
const auth = await requireAuth("workspace attach", json, globals);
|
|
113
|
+
if (!auth)
|
|
114
|
+
return;
|
|
115
|
+
const store = await openLocalStore();
|
|
116
|
+
try {
|
|
117
|
+
const ctx = buildOnboardingContext(auth, store, json, options.yes === true);
|
|
118
|
+
if (options.workspace) {
|
|
119
|
+
const ws = await resolveOrganizationForWorkspace(auth, options);
|
|
120
|
+
if (!ws.ok) {
|
|
121
|
+
emitFail("workspace attach", json, "ORGANIZATION_CONTEXT_MISSING", ws.message);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const outcome = await webFirstAttach(ctx, {
|
|
125
|
+
workspaceId: options.workspace,
|
|
126
|
+
organizationId: ws.organizationId,
|
|
127
|
+
confirmPopulated: options.yes === true,
|
|
128
|
+
});
|
|
129
|
+
reportWebFirst("workspace attach", outcome, json);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
let root;
|
|
133
|
+
try {
|
|
134
|
+
root = resolveRepository(ctx.cwd).repositoryRoot;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
emitFail("workspace attach", json, "NOT_A_WORKTREE", "`nustack workspace attach` needs a Git worktree — run `nustack init` to establish one, or pass `--workspace <id>` for a web-created Workspace.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const resolved = await resolveOrCreateWorkspaceForAttach(auth, options, root, json);
|
|
141
|
+
if (!resolved.ok)
|
|
142
|
+
return;
|
|
143
|
+
if (!hasManifest(root))
|
|
144
|
+
await scaffoldManifest(root, deriveSlug(path.basename(root)));
|
|
145
|
+
const outcome = await attachExistingGit(ctx, {
|
|
146
|
+
workspaceId: resolved.workspaceId,
|
|
147
|
+
organizationId: resolved.organizationId,
|
|
148
|
+
confirmRebind: options.yes === true,
|
|
149
|
+
});
|
|
150
|
+
reportAttach("workspace attach", outcome, json);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
store.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export async function runWorkspaceStatus(options, globals) {
|
|
157
|
+
const json = options.json === true;
|
|
158
|
+
const cwd = process.cwd();
|
|
159
|
+
const workspace = await resolveWorkspaceId(cwd, { workspace: options.workspace });
|
|
160
|
+
if (!workspace.ok) {
|
|
161
|
+
emitFail("workspace status", json, "WORKSPACE_CONTEXT_MISSING", workspace.message);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
165
|
+
const auth = await ensureAccessToken(baseUrl).catch(() => null);
|
|
166
|
+
const store = await openLocalStore();
|
|
167
|
+
let binding = null;
|
|
168
|
+
try {
|
|
169
|
+
binding = findBindingForWorkspace(store, workspace.workspaceId, cwd);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
store.close();
|
|
173
|
+
}
|
|
174
|
+
let cloudWorkspace = null;
|
|
175
|
+
let cloud = null;
|
|
176
|
+
if (auth) {
|
|
177
|
+
try {
|
|
178
|
+
cloudWorkspace = await getWorkspace(baseUrl, auth.accessToken, workspace.workspaceId);
|
|
179
|
+
if (cloudWorkspace.toolkit && cloudWorkspace.toolkit.version && cloudWorkspace.toolkit.rendererVersion && cloudWorkspace.toolkit.placementMapVersion) {
|
|
180
|
+
cloud = {
|
|
181
|
+
name: cloudWorkspace.toolkit.name,
|
|
182
|
+
version: cloudWorkspace.toolkit.version,
|
|
183
|
+
rendererVersion: cloudWorkspace.toolkit.rendererVersion,
|
|
184
|
+
placementMapVersion: cloudWorkspace.toolkit.placementMapVersion,
|
|
185
|
+
catalogVersionId: null,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
cloudWorkspace = null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const compatibility = toolkitCompatibility(binding ?? { etnaProfileJson: null }, cloud);
|
|
194
|
+
if (json) {
|
|
195
|
+
printJsonSuccess("workspace status", {
|
|
196
|
+
workspaceId: workspace.workspaceId,
|
|
197
|
+
cloudReachable: cloudWorkspace !== null,
|
|
198
|
+
cloud: cloudWorkspace
|
|
199
|
+
? { label: cloudWorkspace.label, status: cloudWorkspace.status, publicationState: cloudWorkspace.publicationState, initStatus: cloudWorkspace.initStatus }
|
|
200
|
+
: null,
|
|
201
|
+
binding: binding
|
|
202
|
+
? {
|
|
203
|
+
id: binding.id,
|
|
204
|
+
boundBranch: binding.boundBranch,
|
|
205
|
+
lastProcessedCommitId: binding.lastProcessedCommitId,
|
|
206
|
+
lastAppliedWorkspaceSequence: binding.lastAppliedWorkspaceSequence,
|
|
207
|
+
status: binding.status,
|
|
208
|
+
}
|
|
209
|
+
: null,
|
|
210
|
+
toolkit: { compatible: compatibility.compatible, blocksWrites: compatibility.blocksWrites, reason: compatibility.reason },
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
console.log(`Workspace ${workspace.workspaceId}`);
|
|
215
|
+
console.log(` cloud: ${cloudWorkspace ? `${cloudWorkspace.label} — ${cloudWorkspace.status} / ${cloudWorkspace.publicationState}` : "(unreachable — local-only view)"}`);
|
|
216
|
+
if (binding) {
|
|
217
|
+
console.log(` binding: ${binding.id} on ${binding.boundBranch} (${binding.status})`);
|
|
218
|
+
console.log(` cursor: processed=${binding.lastProcessedCommitId?.slice(0, 12) ?? "—"} applied@${binding.lastAppliedWorkspaceSequence}`);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.log(" binding: (this folder is not bound on this device)");
|
|
222
|
+
}
|
|
223
|
+
console.log(` toolkit: ${compatibility.blocksWrites ? `BLOCKED — ${compatibility.reason}` : "clear (projection and sync writes allowed)"}`);
|
|
224
|
+
}
|
|
225
|
+
export async function runWorkspaceOpen(options, globals) {
|
|
226
|
+
const json = options.json === true;
|
|
227
|
+
const workspace = await resolveWorkspaceId(process.cwd(), { workspace: options.workspace });
|
|
228
|
+
if (!workspace.ok) {
|
|
229
|
+
emitFail("workspace open", json, "WORKSPACE_CONTEXT_MISSING", workspace.message);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
233
|
+
let url;
|
|
234
|
+
try {
|
|
235
|
+
url = assertSafeBrowserUrl(`${baseUrl}/workspaces/${encodeURIComponent(workspace.workspaceId)}`);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
emitFail("workspace open", json, "UNSAFE_URL", error instanceof Error ? error.message : String(error));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (json) {
|
|
242
|
+
printJsonSuccess("workspace open", { workspaceId: workspace.workspaceId, url });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
launchBrowser(url);
|
|
246
|
+
console.log(`Opening ${url}`);
|
|
247
|
+
}
|
|
248
|
+
export async function runWorkspacePublish(options, globals) {
|
|
249
|
+
return runPublication("publish", "workspace publish", options, globals);
|
|
250
|
+
}
|
|
251
|
+
export async function runWorkspaceUnpublish(options, globals) {
|
|
252
|
+
return runPublication("unpublish", "workspace unpublish", options, globals);
|
|
253
|
+
}
|
|
254
|
+
async function runPublication(action, command, options, globals) {
|
|
255
|
+
const json = options.json === true;
|
|
256
|
+
const auth = await requireAuth(command, json, globals);
|
|
257
|
+
if (!auth)
|
|
258
|
+
return;
|
|
259
|
+
const workspace = await resolveWorkspaceId(process.cwd(), { workspace: options.workspace });
|
|
260
|
+
if (!workspace.ok) {
|
|
261
|
+
emitFail(command, json, "WORKSPACE_CONTEXT_MISSING", workspace.message);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
if (action === "publish")
|
|
266
|
+
await publishWorkspace(auth.baseUrl, auth.accessToken, workspace.workspaceId, {}, randomUUID());
|
|
267
|
+
else
|
|
268
|
+
await unpublishWorkspace(auth.baseUrl, auth.accessToken, workspace.workspaceId, randomUUID());
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
emitFail(command, json, action === "publish" ? "PUBLISH_FAILED" : "UNPUBLISH_FAILED", error instanceof Error ? error.message : String(error));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (json) {
|
|
275
|
+
printJsonSuccess(command, { workspaceId: workspace.workspaceId, publicationState: action === "publish" ? "public" : "private" });
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
console.log(action === "publish" ? `Published Workspace ${workspace.workspaceId}.` : `Unpublished Workspace ${workspace.workspaceId}.`);
|
|
279
|
+
}
|
|
280
|
+
function buildOnboardingContext(auth, store, json, yes) {
|
|
281
|
+
const deviceState = readDeviceState(store);
|
|
282
|
+
return {
|
|
283
|
+
store,
|
|
284
|
+
baseUrl: auth.baseUrl,
|
|
285
|
+
accessToken: auth.accessToken,
|
|
286
|
+
deviceRef: deviceState?.deviceRef ?? "",
|
|
287
|
+
userId: deviceState?.activeUserId ?? null,
|
|
288
|
+
cwd: process.cwd(),
|
|
289
|
+
json,
|
|
290
|
+
yes,
|
|
291
|
+
interactive: isInteractive(),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
async function gatherCreatePrompt(options, json) {
|
|
295
|
+
const flagPrompt = options.prompt?.trim();
|
|
296
|
+
if (flagPrompt)
|
|
297
|
+
return { ok: true, prompt: flagPrompt };
|
|
298
|
+
const name = options.name?.trim();
|
|
299
|
+
if (name)
|
|
300
|
+
return { ok: true, prompt: `Create a workspace named "${name}".` };
|
|
301
|
+
if (json || !isInteractive()) {
|
|
302
|
+
return { ok: false, message: "A prompt is required — pass `--prompt <text>` (or `--name <name>`)." };
|
|
303
|
+
}
|
|
304
|
+
const answer = await p.text({ message: "What should this workspace be?" });
|
|
305
|
+
if (p.isCancel(answer) || !String(answer).trim())
|
|
306
|
+
return { ok: false, message: "A prompt is required." };
|
|
307
|
+
return { ok: true, prompt: String(answer).trim() };
|
|
308
|
+
}
|
|
309
|
+
async function createWorkspaceFromPrompt(auth, organizationId, prompt, command, json) {
|
|
310
|
+
let outcome;
|
|
311
|
+
try {
|
|
312
|
+
outcome = await createWorkspaceViaAsk(auth.baseUrl, auth.accessToken, organizationId, prompt, {
|
|
313
|
+
onPhase: json ? undefined : (message) => p.log.info(message),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
emitFail(command, json, "WORKSPACE_CREATE_FAILED", error instanceof Error ? error.message : String(error));
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
if (outcome.generationError && !json) {
|
|
321
|
+
p.log.warn(`The first document did not finish: ${outcome.generationError}`);
|
|
322
|
+
}
|
|
323
|
+
return getWorkspace(auth.baseUrl, auth.accessToken, outcome.workspaceId);
|
|
324
|
+
}
|
|
325
|
+
async function resolveOrCreateWorkspaceForAttach(auth, options, root, json) {
|
|
326
|
+
const marker = await resolveWorkspaceId(root, {});
|
|
327
|
+
if (marker.ok) {
|
|
328
|
+
const manifests = await readWorkspaceManifests(root);
|
|
329
|
+
const markerOrganization = manifests.workspaceManifest?.organizationId ?? manifests.serviceWorkspaceBinding?.organizationId ?? null;
|
|
330
|
+
if (markerOrganization)
|
|
331
|
+
return { ok: true, workspaceId: marker.workspaceId, organizationId: markerOrganization };
|
|
332
|
+
const ws = await resolveOrganizationForWorkspace(auth, options);
|
|
333
|
+
if (!ws.ok) {
|
|
334
|
+
emitFail("workspace attach", json, "ORGANIZATION_CONTEXT_MISSING", ws.message);
|
|
335
|
+
return { ok: false };
|
|
336
|
+
}
|
|
337
|
+
return { ok: true, workspaceId: marker.workspaceId, organizationId: ws.organizationId };
|
|
338
|
+
}
|
|
339
|
+
const ws = await resolveOrganizationForWorkspace(auth, options);
|
|
340
|
+
if (!ws.ok) {
|
|
341
|
+
emitFail("workspace attach", json, "ORGANIZATION_CONTEXT_MISSING", ws.message);
|
|
342
|
+
return { ok: false };
|
|
343
|
+
}
|
|
344
|
+
const gathered = await gatherCreatePrompt(options, json);
|
|
345
|
+
if (!gathered.ok) {
|
|
346
|
+
emitFail("workspace attach", json, "WORKSPACE_CONTEXT_MISSING", `No Workspace to attach — ${gathered.message} Or pass \`--workspace <id>\` for a web-created Workspace.`);
|
|
347
|
+
return { ok: false };
|
|
348
|
+
}
|
|
349
|
+
const workspace = await createWorkspaceFromPrompt(auth, ws.organizationId, gathered.prompt, "workspace attach", json);
|
|
350
|
+
if (!workspace)
|
|
351
|
+
return { ok: false };
|
|
352
|
+
return { ok: true, workspaceId: workspace.id, organizationId: ws.organizationId };
|
|
353
|
+
}
|
|
354
|
+
function findBindingForWorkspace(store, workspaceId, cwd) {
|
|
355
|
+
try {
|
|
356
|
+
const byRoot = getBindingByRepositoryRoot(store, resolveRepository(cwd).repositoryRoot);
|
|
357
|
+
if (byRoot)
|
|
358
|
+
return byRoot;
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
}
|
|
362
|
+
return listBindings(store).find((binding) => binding.workspaceId === workspaceId) ?? null;
|
|
363
|
+
}
|
|
364
|
+
function deriveSlug(value) {
|
|
365
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "workspace";
|
|
366
|
+
}
|
|
367
|
+
function toResultJson(result) {
|
|
368
|
+
return {
|
|
369
|
+
flow: result.flow,
|
|
370
|
+
workspaceId: result.workspaceId,
|
|
371
|
+
organizationId: result.organizationId,
|
|
372
|
+
bindingId: result.bindingId,
|
|
373
|
+
boundBranch: result.boundBranch,
|
|
374
|
+
baselineCommitId: result.baselineCommitId,
|
|
375
|
+
toolkit: result.toolkit,
|
|
376
|
+
reused: result.reused,
|
|
377
|
+
install: { ok: result.install.ok, installed: result.install.installed, skipped: result.install.skipped },
|
|
378
|
+
noPush: result.noPush,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function printOnboardingHuman(result) {
|
|
382
|
+
console.log(`Bound Workspace ${result.workspaceId} on branch ${result.boundBranch} (binding ${result.bindingId})${result.reused ? " [reused]" : ""}.`);
|
|
383
|
+
const installNote = result.install.skipped
|
|
384
|
+
? result.install.reason ?? "no install"
|
|
385
|
+
: result.install.installed
|
|
386
|
+
? "installed"
|
|
387
|
+
: "not installed";
|
|
388
|
+
console.log(`Toolkit ${result.toolkit.name}@${result.toolkit.version} — ${installNote}.`);
|
|
389
|
+
console.log("NuStack did not push any Git history.");
|
|
390
|
+
}
|
|
391
|
+
function reportWebFirst(command, outcome, json) {
|
|
392
|
+
if (!outcome.ok) {
|
|
393
|
+
emitFail(command, json, outcome.code, outcome.message);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const result = outcome.result;
|
|
397
|
+
if (json) {
|
|
398
|
+
printJsonSuccess(command, {
|
|
399
|
+
...toResultJson(result),
|
|
400
|
+
appliedCommitId: result.appliedCommitId,
|
|
401
|
+
changedPaths: result.changedPaths,
|
|
402
|
+
handoffId: result.handoffId,
|
|
403
|
+
overwriteRequired: result.overwriteRequired,
|
|
404
|
+
divergent: result.divergent,
|
|
405
|
+
});
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
printOnboardingHuman(result);
|
|
409
|
+
if (result.overwriteRequired) {
|
|
410
|
+
console.log(`Divergent target(s) — nothing was changed. Review, then run \`nustack workspace sync approve ${result.handoffId ?? ""}\`:`);
|
|
411
|
+
for (const entry of result.divergent)
|
|
412
|
+
console.log(` ${entry.path} (${entry.divergence})`);
|
|
413
|
+
}
|
|
414
|
+
else if (result.appliedCommitId) {
|
|
415
|
+
console.log(`Applied Handoff ${result.handoffId ?? ""} as ${result.appliedCommitId.slice(0, 12)} — NuStack did not push it.`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function reportAttach(command, outcome, json) {
|
|
419
|
+
if (!outcome.ok) {
|
|
420
|
+
emitFail(command, json, outcome.code, outcome.message);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (json) {
|
|
424
|
+
printJsonSuccess(command, toResultJson(outcome.result));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
printOnboardingHuman(outcome.result);
|
|
428
|
+
}
|
|
429
|
+
export function registerWorkspaceCommands(workspaceCommand, getGlobalOptions) {
|
|
430
|
+
workspaceCommand
|
|
431
|
+
.command("list")
|
|
432
|
+
.description("List the Workspaces in the active (or --organization) Organization")
|
|
433
|
+
.option("--organization <id>", "the Organization to list (defaults to the active Organization)")
|
|
434
|
+
.option("--json", "emit the versioned machine envelope")
|
|
435
|
+
.action(async (options) => runWorkspaceList(options, getGlobalOptions()));
|
|
436
|
+
workspaceCommand
|
|
437
|
+
.command("create")
|
|
438
|
+
.description("Create a Workspace from a prompt, then optionally attach the current folder")
|
|
439
|
+
.option("--organization <id>", "the owning Organization (defaults to the active Organization)")
|
|
440
|
+
.option("--prompt <text>", "what the Workspace should be — the first prompt creates it")
|
|
441
|
+
.option("--name <name>", "shorthand: builds the create prompt from a name")
|
|
442
|
+
.option("--yes", "assume yes for onboarding confirmations")
|
|
443
|
+
.option("--json", "emit the versioned machine envelope")
|
|
444
|
+
.action(async (options) => runWorkspaceCreate(options, getGlobalOptions()));
|
|
445
|
+
workspaceCommand
|
|
446
|
+
.command("attach")
|
|
447
|
+
.description("Bind the current folder to a Workspace (existing-Git flow, or --workspace for a web-created Workspace)")
|
|
448
|
+
.option("--workspace <id>", "attach to this web-created Workspace (web-first flow)")
|
|
449
|
+
.option("--organization <id>", "the owning Organization (defaults to the active Organization)")
|
|
450
|
+
.option("--prompt <text>", "the create prompt when creating a Workspace for this worktree")
|
|
451
|
+
.option("--name <name>", "shorthand: builds the create prompt from a name")
|
|
452
|
+
.option("--yes", "confirm populated-folder onboarding and rebind")
|
|
453
|
+
.option("--json", "emit the versioned machine envelope")
|
|
454
|
+
.action(async (options) => runWorkspaceAttach(options, getGlobalOptions()));
|
|
455
|
+
workspaceCommand
|
|
456
|
+
.command("status")
|
|
457
|
+
.description("Show the cloud Workspace summary merged with the local binding (degraded offline)")
|
|
458
|
+
.option("--workspace <id>", "the Workspace to act as (defaults to the bound repository)")
|
|
459
|
+
.option("--json", "emit the versioned machine envelope")
|
|
460
|
+
.action(async (options) => runWorkspaceStatus(options, getGlobalOptions()));
|
|
461
|
+
workspaceCommand
|
|
462
|
+
.command("open")
|
|
463
|
+
.description("Open the Workspace in the browser (--json prints the URL instead of launching)")
|
|
464
|
+
.option("--workspace <id>", "the Workspace to open (defaults to the bound repository)")
|
|
465
|
+
.option("--json", "print the resolved URL as the machine envelope instead of launching")
|
|
466
|
+
.action(async (options) => runWorkspaceOpen(options, getGlobalOptions()));
|
|
467
|
+
workspaceCommand
|
|
468
|
+
.command("publish")
|
|
469
|
+
.description("Publish the Workspace (role-gated)")
|
|
470
|
+
.option("--workspace <id>", "the Workspace to publish (defaults to the bound repository)")
|
|
471
|
+
.option("--json", "emit the versioned machine envelope")
|
|
472
|
+
.action(async (options) => runWorkspacePublish(options, getGlobalOptions()));
|
|
473
|
+
workspaceCommand
|
|
474
|
+
.command("unpublish")
|
|
475
|
+
.description("Return the Workspace to private (role-gated)")
|
|
476
|
+
.option("--workspace <id>", "the Workspace to unpublish (defaults to the bound repository)")
|
|
477
|
+
.option("--json", "emit the versioned machine envelope")
|
|
478
|
+
.action(async (options) => runWorkspaceUnpublish(options, getGlobalOptions()));
|
|
479
|
+
}
|