@nurix/nustack 0.10.0-dev.9 → 0.11.0
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 +1 -0
- package/dist/commands/bootstrap.js +4 -4
- package/dist/commands/docs.js +13 -13
- package/dist/commands/doctor.js +51 -17
- package/dist/commands/handoff.js +8 -8
- package/dist/commands/init.js +43 -43
- package/dist/commands/login.js +19 -6
- package/dist/commands/logout.js +14 -6
- package/dist/commands/logscope.js +55 -0
- package/dist/commands/{workspace-sync.js → project-sync.js} +5 -5
- package/dist/commands/{workspace.js → project.js} +133 -133
- package/dist/commands/review.js +38 -15
- package/dist/commands/search.js +7 -7
- package/dist/commands/sessions.js +156 -5
- package/dist/commands/sync.js +10 -10
- package/dist/commands/token.js +102 -0
- package/dist/commands/toolkit.js +19 -19
- package/dist/index.js +88 -36
- package/dist/lib/auth_grant.js +32 -0
- package/dist/lib/auth_recovery.js +3 -1
- package/dist/lib/credential_store.js +106 -0
- package/dist/lib/device_identity.js +6 -6
- package/dist/lib/device_keychain.js +16 -37
- package/dist/lib/etna/profile.js +9 -9
- package/dist/lib/etna/reconcile.js +11 -11
- package/dist/lib/git-sync/core/engine.js +4 -4
- package/dist/lib/git-sync/inbound/apply_commit.js +3 -3
- package/dist/lib/git-sync/inbound/approval.js +1 -1
- package/dist/lib/git-sync/inbound/download.js +8 -8
- package/dist/lib/git-sync/inbound/event_sync.js +13 -13
- package/dist/lib/git-sync/inbound/notification.js +1 -1
- package/dist/lib/git-sync/outbound/extraction.js +4 -4
- package/dist/lib/git-sync/outbound/history_decision.js +2 -2
- package/dist/lib/git-sync/outbound/publisher.js +2 -2
- package/dist/lib/git-sync/outbound/queue.js +3 -3
- package/dist/lib/git-sync/outbound/upload_client.js +4 -4
- package/dist/lib/git-sync/whitelist/index.js +1 -1
- package/dist/lib/git-sync/whitelist/types.js +1 -1
- package/dist/lib/git-sync/whitelist/validation.js +5 -5
- package/dist/lib/libsecret_store.js +47 -0
- package/dist/lib/local_store/accepted_uploads.js +5 -5
- package/dist/lib/local_store/bindings.js +11 -11
- package/dist/lib/local_store/index.js +1 -1
- package/dist/lib/local_store/migrations.js +13 -0
- package/dist/lib/local_store/project_events_cache.js +54 -0
- package/dist/lib/logs/answer.js +99 -0
- package/dist/lib/logs/library.js +19 -0
- package/dist/lib/logs/lifecycle.js +159 -0
- package/dist/lib/nustack_client.js +88 -46
- package/dist/lib/onboarding/attach_existing_git.js +7 -7
- package/dist/lib/onboarding/binding.js +11 -11
- package/dist/lib/onboarding/folder_state.js +10 -10
- package/dist/lib/onboarding/handoff_apply.js +2 -2
- package/dist/lib/onboarding/init_fresh_folder.js +18 -18
- package/dist/lib/onboarding/manifest_writer.js +31 -31
- package/dist/lib/onboarding/web_first_attach.js +12 -12
- package/dist/lib/{workspace_context.js → project_context.js} +24 -24
- package/dist/lib/review/client.js +1 -1
- package/dist/lib/{lanes → session-engine}/claudeAdapter.js +3 -3
- package/dist/lib/{lanes → session-engine}/frames.js +36 -27
- package/dist/lib/session-engine/prune.js +233 -0
- package/dist/lib/session-engine/registry.js +83 -0
- package/dist/lib/session-engine/supervisor.js +321 -0
- package/dist/lib/session-engine/worktree.js +89 -0
- package/dist/lib/session_telemetry.js +2 -2
- package/dist/lib/sessions/client.js +8 -8
- package/dist/lib/sessions/outsideSessions.js +6 -6
- package/dist/lib/telemetry/config_renderer.js +3 -3
- package/dist/lib/telemetry/health_report.js +1 -1
- package/package.json +5 -4
- package/dist/commands/lane.js +0 -12
- package/dist/lib/lanes/supervisor.js +0 -247
- package/dist/lib/lanes/worktree.js +0 -33
- package/dist/lib/local_store/workspace_events_cache.js +0 -54
package/README.md
CHANGED
|
@@ -74,4 +74,5 @@ nustack telemetry [status|enable|disable|reinstall]
|
|
|
74
74
|
|
|
75
75
|
- **`--json` almost everywhere** — most commands emit the `nustackJsonVersion: 1` machine envelope with `--json`; errors are typed (`{ error: { code, message } }`), never a bare HTTP status. Four surfaces break the pattern by design — `describe`, `search`, `search services`, and the `workspace sync` family emit a bare array/object instead, because the desktop and the agents pin those exact shapes.
|
|
76
76
|
- **Base URL** — `--nustack-url` flag → `NUSTACK_URL` env → the hosted default (`https://apps.nustack.ai`).
|
|
77
|
+
- **Credential** — `NUSTACK_TOKEN` (a personal access token from `nustack token create`, for pipelines and agents; wins over every store, never refreshes, never opens a browser) → the OS store (macOS Keychain, libsecret on Linux) → `~/.nustack/credentials.json` (`0600`) where no OS store exists. `NUSTACK_CREDENTIAL_STORE=auto|keychain|libsecret|file` overrides the probe.
|
|
77
78
|
- **Organization context** — `nustack organization use` persists the selection locally; commands send it as a header the server re-proves against your memberships on every request.
|
|
@@ -22,7 +22,7 @@ export async function runBootstrap(options) {
|
|
|
22
22
|
return;
|
|
23
23
|
await ensureDeviceTelemetry({ baseUrl, log: (message) => p.log.info(message) });
|
|
24
24
|
const state = classifyFolder(cwd);
|
|
25
|
-
const bound = state.class === "git_worktree" && state.repository && state.git?.
|
|
25
|
+
const bound = state.class === "git_worktree" && state.repository && state.git?.markerProjectId
|
|
26
26
|
? await printBoundStatus(state.repository.repositoryRoot)
|
|
27
27
|
: false;
|
|
28
28
|
if (bound)
|
|
@@ -42,10 +42,10 @@ async function printBoundStatus(root) {
|
|
|
42
42
|
"decision_required",
|
|
43
43
|
"approved",
|
|
44
44
|
]);
|
|
45
|
-
p.log.success(`This repository is bound to
|
|
46
|
-
p.log.info(`Binding ${binding.id} · ${binding.status} · applied
|
|
45
|
+
p.log.success(`This repository is bound to Project ${binding.projectId} on branch ${binding.boundBranch}.`);
|
|
46
|
+
p.log.info(`Binding ${binding.id} · ${binding.status} · applied Project sequence ${binding.lastAppliedProjectSequence}` +
|
|
47
47
|
(pending.length > 0 ? ` · ${pending.length} pending inbound` : ""));
|
|
48
|
-
p.outro("Up to date locally — `nustack
|
|
48
|
+
p.outro("Up to date locally — `nustack project status` for the cloud view, `nustack project sync now` to reconcile.");
|
|
49
49
|
return true;
|
|
50
50
|
}
|
|
51
51
|
finally {
|
package/dist/commands/docs.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { getDocument, listDocuments, resolveNuStackUrl, } from "../lib/nustack_client.js";
|
|
2
2
|
import { ensureAuthWithRecovery, lastAuthFailure } from "../lib/auth_recovery.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveProjectId } from "../lib/project_context.js";
|
|
4
4
|
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
5
5
|
const INLINE_BODY_LIMIT = 16 * 1024;
|
|
6
6
|
async function resolveContext(command, json, options, globals) {
|
|
7
7
|
const baseUrl = resolveNuStackUrl(globals);
|
|
8
|
-
const
|
|
9
|
-
if (!
|
|
10
|
-
emitFail(command, json, "
|
|
8
|
+
const project = await resolveProjectId(process.cwd(), options);
|
|
9
|
+
if (!project.ok) {
|
|
10
|
+
emitFail(command, json, "PROJECT_CONTEXT_MISSING", project.message);
|
|
11
11
|
return null;
|
|
12
12
|
}
|
|
13
13
|
const deviceAuth = await ensureAuthWithRecovery(baseUrl, json);
|
|
@@ -16,7 +16,7 @@ async function resolveContext(command, json, options, globals) {
|
|
|
16
16
|
emitFail(command, json, failure.code, failure.message);
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
|
-
return { baseUrl, accessToken: deviceAuth.accessToken,
|
|
19
|
+
return { baseUrl, accessToken: deviceAuth.accessToken, projectId: project.projectId };
|
|
20
20
|
}
|
|
21
21
|
function emitFail(command, json, code, message) {
|
|
22
22
|
if (json)
|
|
@@ -31,13 +31,13 @@ export async function runDocsList(options, globals) {
|
|
|
31
31
|
const ctx = await resolveContext("docs list", json, options, globals);
|
|
32
32
|
if (!ctx)
|
|
33
33
|
return;
|
|
34
|
-
const documents = await listDocuments(ctx.baseUrl, ctx.accessToken, ctx.
|
|
34
|
+
const documents = await listDocuments(ctx.baseUrl, ctx.accessToken, ctx.projectId);
|
|
35
35
|
if (json) {
|
|
36
|
-
printJsonSuccess("docs list", {
|
|
36
|
+
printJsonSuccess("docs list", { projectId: ctx.projectId, documents });
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
if (documents.length === 0) {
|
|
40
|
-
console.log("This
|
|
40
|
+
console.log("This Project has no Documents yet.");
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
for (const doc of documents) {
|
|
@@ -50,10 +50,10 @@ export async function runDocsShow(target, options, globals) {
|
|
|
50
50
|
const ctx = await resolveContext("docs show", json, options, globals);
|
|
51
51
|
if (!ctx)
|
|
52
52
|
return;
|
|
53
|
-
const documents = await listDocuments(ctx.baseUrl, ctx.accessToken, ctx.
|
|
53
|
+
const documents = await listDocuments(ctx.baseUrl, ctx.accessToken, ctx.projectId);
|
|
54
54
|
const match = documents.find((d) => d.id === target || d.slug === target);
|
|
55
55
|
if (!match) {
|
|
56
|
-
emitFail("docs show", json, "DOCUMENT_NOT_FOUND", `No Document "${target}" in this
|
|
56
|
+
emitFail("docs show", json, "DOCUMENT_NOT_FOUND", `No Document "${target}" in this Project.`);
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
const payload = await getDocument(ctx.baseUrl, ctx.accessToken, match.id);
|
|
@@ -97,7 +97,7 @@ export async function runDocsStatus(options, globals) {
|
|
|
97
97
|
const ctx = await resolveContext("docs status", json, options, globals);
|
|
98
98
|
if (!ctx)
|
|
99
99
|
return;
|
|
100
|
-
const documents = await listDocuments(ctx.baseUrl, ctx.accessToken, ctx.
|
|
100
|
+
const documents = await listDocuments(ctx.baseUrl, ctx.accessToken, ctx.projectId);
|
|
101
101
|
const rows = documents.map((doc) => ({
|
|
102
102
|
slug: doc.slug,
|
|
103
103
|
documentId: doc.id,
|
|
@@ -106,11 +106,11 @@ export async function runDocsStatus(options, globals) {
|
|
|
106
106
|
convergence: "unmapped",
|
|
107
107
|
}));
|
|
108
108
|
if (json) {
|
|
109
|
-
printJsonSuccess("docs status", {
|
|
109
|
+
printJsonSuccess("docs status", { projectId: ctx.projectId, paths: rows });
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
112
|
if (rows.length === 0) {
|
|
113
|
-
console.log("This
|
|
113
|
+
console.log("This Project has no Documents to converge.");
|
|
114
114
|
return;
|
|
115
115
|
}
|
|
116
116
|
for (const row of rows) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import { ensureAccessToken, ENV_TOKEN_VARIABLE, envToken } from "../lib/auth_grant.js";
|
|
5
|
+
import { credentialsFilePath, selectCredentialStore } from "../lib/credential_store.js";
|
|
6
|
+
import { resolveProjectId } from "../lib/project_context.js";
|
|
6
7
|
import { readManagedValues } from "../lib/env_file.js";
|
|
7
|
-
import {
|
|
8
|
+
import { readProjectManifests, diagnoseManifests } from "../lib/onboarding/manifest_writer.js";
|
|
8
9
|
import { fetchToolkitProfile, ToolkitProfileUnavailableError } from "../lib/etna/profile.js";
|
|
9
10
|
import { toolkitCompatibility } from "../lib/etna/reconcile.js";
|
|
10
11
|
import { openLocalStore } from "../lib/local_store/database.js";
|
|
@@ -19,6 +20,33 @@ import { NotARepositoryError } from "../lib/git-sync/core/git_runner.js";
|
|
|
19
20
|
import { listBundles, sweepExpiredBundles } from "../lib/git-sync/recovery/store.js";
|
|
20
21
|
import { inspectRecoveryKey } from "../lib/git-sync/recovery/crypto.js";
|
|
21
22
|
import { NUSTACK_JSON_VERSION } from "../lib/json_output.js";
|
|
23
|
+
async function credentialStoreRows(environmentInUse) {
|
|
24
|
+
if (environmentInUse)
|
|
25
|
+
return [{ check: "Credential source", status: "ok", detail: "environment — no store is read" }];
|
|
26
|
+
let store;
|
|
27
|
+
try {
|
|
28
|
+
store = selectCredentialStore();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return [{ check: "Credential source", status: "error", detail: error instanceof Error ? error.message : String(error) }];
|
|
32
|
+
}
|
|
33
|
+
const rows = [
|
|
34
|
+
{ check: "Credential source", status: "ok", detail: store.name === "file" ? "file — no OS keychain on this machine" : store.name },
|
|
35
|
+
];
|
|
36
|
+
if (store.name === "file") {
|
|
37
|
+
const filePath = credentialsFilePath();
|
|
38
|
+
try {
|
|
39
|
+
const mode = (await stat(filePath)).mode & 0o777;
|
|
40
|
+
rows.push(mode === 0o600
|
|
41
|
+
? { check: "Credential file mode", status: "ok", detail: "0600" }
|
|
42
|
+
: { check: "Credential file mode", status: "warn", detail: `0${mode.toString(8)} — expected 0600; run \`chmod 600 ~/.nustack/credentials.json\`` });
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
rows.push({ check: "Credential file mode", status: "info", detail: "no credential file yet — sign in to create it" });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return rows;
|
|
49
|
+
}
|
|
22
50
|
export async function runDoctor(options) {
|
|
23
51
|
const cwd = process.cwd();
|
|
24
52
|
const baseUrl = resolveNuStackUrl(options);
|
|
@@ -28,12 +56,18 @@ export async function runDoctor(options) {
|
|
|
28
56
|
const add = (area, check, status, detail = "") => {
|
|
29
57
|
rows.push({ area, check, status, detail });
|
|
30
58
|
};
|
|
59
|
+
const fromEnv = envToken();
|
|
31
60
|
const deviceAuth = await ensureAccessToken(baseUrl).catch(() => null);
|
|
32
|
-
if (
|
|
61
|
+
if (fromEnv?.kind === "malformed")
|
|
62
|
+
add("identity", "Device sign-in", "error", fromEnv.message);
|
|
63
|
+
else if (fromEnv)
|
|
64
|
+
add("identity", "Device sign-in", "ok", `${ENV_TOKEN_VARIABLE} is set — the environment's token is used`);
|
|
65
|
+
else if (deviceAuth)
|
|
33
66
|
add("identity", "Device sign-in", "ok", `signed in as ${deviceAuth.userEmail}`);
|
|
34
67
|
else
|
|
35
68
|
add("identity", "Device sign-in", "error", "signed out — run `nustack login`");
|
|
36
|
-
|
|
69
|
+
for (const row of await credentialStoreRows(fromEnv !== null))
|
|
70
|
+
add("identity", row.check, row.status, row.detail);
|
|
37
71
|
add("api", "NuStack reachability", ...(await probeReachability(baseUrl)));
|
|
38
72
|
let store = null;
|
|
39
73
|
try {
|
|
@@ -62,9 +96,9 @@ export async function runDoctor(options) {
|
|
|
62
96
|
addRepositoryRows(add, repository);
|
|
63
97
|
}
|
|
64
98
|
const contextRoot = repository?.repositoryRoot ?? cwd;
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const cloudProfile = deviceAuth &&
|
|
99
|
+
const projectResolution = await resolveProjectId(contextRoot, { project: options.project });
|
|
100
|
+
const projectId = projectResolution.ok ? projectResolution.projectId : null;
|
|
101
|
+
const cloudProfile = deviceAuth && projectId ? await fetchCloudProfile(baseUrl, deviceAuth.accessToken, projectId) : null;
|
|
68
102
|
const binding = store && repository ? getBindingByRepositoryRoot(store, repository.repositoryRoot) : null;
|
|
69
103
|
await addManifestRows(add, contextRoot, cloudProfile);
|
|
70
104
|
addBindingRows(add, store, binding, cloudProfile);
|
|
@@ -127,9 +161,9 @@ function addRepositoryRows(add, repository) {
|
|
|
127
161
|
else
|
|
128
162
|
add("repository", "HEAD", "info", "unborn branch — no commits yet");
|
|
129
163
|
}
|
|
130
|
-
async function fetchCloudProfile(baseUrl, accessToken,
|
|
164
|
+
async function fetchCloudProfile(baseUrl, accessToken, projectId) {
|
|
131
165
|
try {
|
|
132
|
-
return await fetchToolkitProfile(baseUrl, accessToken,
|
|
166
|
+
return await fetchToolkitProfile(baseUrl, accessToken, projectId);
|
|
133
167
|
}
|
|
134
168
|
catch (error) {
|
|
135
169
|
if (error instanceof ToolkitProfileUnavailableError)
|
|
@@ -138,12 +172,12 @@ async function fetchCloudProfile(baseUrl, accessToken, workspaceId) {
|
|
|
138
172
|
}
|
|
139
173
|
}
|
|
140
174
|
async function addManifestRows(add, root, cloud) {
|
|
141
|
-
const manifests = await
|
|
142
|
-
if (!manifests.
|
|
143
|
-
add("manifests", "
|
|
175
|
+
const manifests = await readProjectManifests(root);
|
|
176
|
+
if (!manifests.projectManifest && !manifests.serviceToolkit) {
|
|
177
|
+
add("manifests", "Project binding markers", "info", "no `.nustack/app.json` or Service toolkit block — repository not attached");
|
|
144
178
|
return;
|
|
145
179
|
}
|
|
146
|
-
const diagnosis = diagnoseManifests(cloud ? { name: cloud.name, version: cloud.version } : null, manifests.
|
|
180
|
+
const diagnosis = diagnoseManifests(cloud ? { name: cloud.name, version: cloud.version } : null, manifests.projectManifest?.toolkit ?? null, manifests.serviceToolkit);
|
|
147
181
|
const status = diagnosis === "aligned" ? "ok" : diagnosis === "missing" ? "warn" : "warn";
|
|
148
182
|
const suffix = diagnosis === "aligned" || diagnosis === "missing" ? "" : " — run `nustack toolkit reconcile`";
|
|
149
183
|
add("manifests", "Marker / Service agreement", status, `${diagnosis}${cloud ? "" : " (no cloud pin reachable)"}${suffix}`);
|
|
@@ -154,7 +188,7 @@ function addBindingRows(add, store, binding, cloud) {
|
|
|
154
188
|
return;
|
|
155
189
|
}
|
|
156
190
|
add("sync", "Sync Whitelist cache", binding.whitelistManifestSha256 ? "ok" : "info", binding.whitelistManifestSha256 ? "cached" : "not cached yet");
|
|
157
|
-
add("sync", "
|
|
191
|
+
add("sync", "Project cursor", "ok", `applied Project sequence ${binding.lastAppliedProjectSequence}`);
|
|
158
192
|
if (store) {
|
|
159
193
|
const pending = listApplicationsForBinding(store, binding.id, [
|
|
160
194
|
"received",
|
|
@@ -208,7 +242,7 @@ function addTelemetryRows(add, store) {
|
|
|
208
242
|
async function addRecoveryRows(add, store) {
|
|
209
243
|
await sweepExpiredBundles(store).catch(() => { });
|
|
210
244
|
const active = listBundles(store).filter((bundle) => bundle.state === "active");
|
|
211
|
-
add("recovery", "Recovery bundles", active.length > 0 ? "info" : "ok", active.length > 0 ? `${active.length} active — \`nustack
|
|
245
|
+
add("recovery", "Recovery bundles", active.length > 0 ? "info" : "ok", active.length > 0 ? `${active.length} active — \`nustack project sync recovery list\`` : "none");
|
|
212
246
|
const deviceRef = readDeviceState(store)?.deviceRef ?? null;
|
|
213
247
|
if (!deviceRef)
|
|
214
248
|
return;
|
package/dist/commands/handoff.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { fetchHandoffManifest, getHandoff, listHandoffs, resolveNuStackUrl, } from "../lib/nustack_client.js";
|
|
2
2
|
import { ensureAuthWithRecovery, lastAuthFailure } from "../lib/auth_recovery.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveProjectId } from "../lib/project_context.js";
|
|
4
4
|
import { openLocalStore } from "../lib/local_store/database.js";
|
|
5
5
|
import { getBindingByRepositoryRoot } from "../lib/local_store/bindings.js";
|
|
6
6
|
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
7
7
|
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
8
8
|
async function resolveContext(command, json, options, globals) {
|
|
9
9
|
const baseUrl = resolveNuStackUrl(globals);
|
|
10
|
-
const
|
|
11
|
-
if (!
|
|
12
|
-
emitFail(command, json, "
|
|
10
|
+
const project = await resolveProjectId(process.cwd(), options);
|
|
11
|
+
if (!project.ok) {
|
|
12
|
+
emitFail(command, json, "PROJECT_CONTEXT_MISSING", project.message);
|
|
13
13
|
return null;
|
|
14
14
|
}
|
|
15
15
|
const deviceAuth = await ensureAuthWithRecovery(baseUrl, json);
|
|
@@ -18,7 +18,7 @@ async function resolveContext(command, json, options, globals) {
|
|
|
18
18
|
emitFail(command, json, failure.code, failure.message);
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
|
-
return { baseUrl, accessToken: deviceAuth.accessToken,
|
|
21
|
+
return { baseUrl, accessToken: deviceAuth.accessToken, projectId: project.projectId };
|
|
22
22
|
}
|
|
23
23
|
function emitFail(command, json, code, message) {
|
|
24
24
|
if (json)
|
|
@@ -33,13 +33,13 @@ export async function runHandoffList(options, globals) {
|
|
|
33
33
|
const ctx = await resolveContext("handoff list", json, options, globals);
|
|
34
34
|
if (!ctx)
|
|
35
35
|
return;
|
|
36
|
-
const handoffs = await listHandoffs(ctx.baseUrl, ctx.accessToken, ctx.
|
|
36
|
+
const handoffs = await listHandoffs(ctx.baseUrl, ctx.accessToken, ctx.projectId);
|
|
37
37
|
if (json) {
|
|
38
|
-
printJsonSuccess("handoff list", {
|
|
38
|
+
printJsonSuccess("handoff list", { projectId: ctx.projectId, handoffs });
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
if (handoffs.length === 0) {
|
|
42
|
-
console.log("This
|
|
42
|
+
console.log("This Project has no Handoffs yet.");
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
45
45
|
for (const h of handoffs) {
|
package/dist/commands/init.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import * as p from "@clack/prompts";
|
|
4
|
-
import {
|
|
4
|
+
import { createProjectViaAsk, createOrganization, getProject, listProjects, listOrganizations, resolveNuStackUrl, } from "../lib/nustack_client.js";
|
|
5
5
|
import { ensureAccessToken } from "../lib/auth_grant.js";
|
|
6
6
|
import { getOrCreateDeviceId } from "../lib/device_identity.js";
|
|
7
7
|
import { isInteractive } from "../lib/interactive.js";
|
|
8
|
-
import { resolveOrganizationId } from "../lib/
|
|
8
|
+
import { resolveOrganizationId } from "../lib/project_context.js";
|
|
9
9
|
import { openLocalStore } from "../lib/local_store/database.js";
|
|
10
10
|
import { readDeviceState, upsertDeviceState } from "../lib/local_store/device_state.js";
|
|
11
11
|
import { hasManifest, scaffoldManifest } from "../lib/manifest.js";
|
|
12
|
-
import {
|
|
12
|
+
import { readProjectManifests } from "../lib/onboarding/manifest_writer.js";
|
|
13
13
|
import { attachRejection, classifyFolder } from "../lib/onboarding/folder_state.js";
|
|
14
14
|
import { attachExistingGit } from "../lib/onboarding/attach_existing_git.js";
|
|
15
15
|
import { initFreshFolder } from "../lib/onboarding/init_fresh_folder.js";
|
|
@@ -30,7 +30,7 @@ export async function runInit(options) {
|
|
|
30
30
|
return;
|
|
31
31
|
await runOnboardingFlow(baseUrl, accessToken, {
|
|
32
32
|
yes: options.yes,
|
|
33
|
-
|
|
33
|
+
project: options.project,
|
|
34
34
|
organization: options.organization,
|
|
35
35
|
});
|
|
36
36
|
}
|
|
@@ -75,18 +75,18 @@ export async function runOnboardingFlow(baseUrl, accessToken, options) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
async function routeOnboarding(ctx, options) {
|
|
78
|
-
if (options.
|
|
79
|
-
await routeWebFirst(ctx, options, options.
|
|
78
|
+
if (options.projectId) {
|
|
79
|
+
await routeWebFirst(ctx, options, options.projectId);
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
const state = classifyFolder(ctx.cwd);
|
|
83
83
|
if (state.class === "git_worktree" && state.repository && state.git) {
|
|
84
|
-
await routeExistingGit(ctx, options, state.repository.repositoryRoot, state.git.
|
|
84
|
+
await routeExistingGit(ctx, options, state.repository.repositoryRoot, state.git.markerProjectId);
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
await routeFreshFolder(ctx, options, state.class === "populated_non_git");
|
|
88
88
|
}
|
|
89
|
-
async function routeExistingGit(ctx, options, root,
|
|
89
|
+
async function routeExistingGit(ctx, options, root, markerProjectId) {
|
|
90
90
|
const state = classifyFolder(ctx.cwd);
|
|
91
91
|
const rejection = state.git ? attachRejection(state.git) : null;
|
|
92
92
|
if (rejection) {
|
|
@@ -96,12 +96,12 @@ async function routeExistingGit(ctx, options, root, markerWorkspaceId) {
|
|
|
96
96
|
if (!hasManifest(root)) {
|
|
97
97
|
await scaffoldManifest(root, deriveRepoName(path.basename(root)));
|
|
98
98
|
}
|
|
99
|
-
let
|
|
99
|
+
let projectId;
|
|
100
100
|
let organizationId;
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
const manifests = await
|
|
104
|
-
const recorded = manifests.
|
|
101
|
+
if (markerProjectId) {
|
|
102
|
+
projectId = markerProjectId;
|
|
103
|
+
const manifests = await readProjectManifests(root);
|
|
104
|
+
const recorded = manifests.projectManifest?.organizationId ?? manifests.serviceProjectBinding?.organizationId ?? null;
|
|
105
105
|
const resolved = recorded ?? (await resolveOrganizationInteractive(ctx, options));
|
|
106
106
|
if (!resolved)
|
|
107
107
|
return;
|
|
@@ -112,14 +112,14 @@ async function routeExistingGit(ctx, options, root, markerWorkspaceId) {
|
|
|
112
112
|
if (!resolved)
|
|
113
113
|
return;
|
|
114
114
|
organizationId = resolved;
|
|
115
|
-
const chosen = await
|
|
115
|
+
const chosen = await chooseOrCreateProject(ctx, organizationId, root, "existing_local");
|
|
116
116
|
if (!chosen)
|
|
117
117
|
return;
|
|
118
|
-
|
|
118
|
+
projectId = chosen;
|
|
119
119
|
}
|
|
120
120
|
const spinner = p.spinner();
|
|
121
121
|
spinner.start("Attaching this repository");
|
|
122
|
-
const outcome = await attachExistingGit(ctx, {
|
|
122
|
+
const outcome = await attachExistingGit(ctx, { projectId, organizationId, confirmRebind: true });
|
|
123
123
|
spinner.stop(outcome.ok ? "Attached" : "Attach failed", outcome.ok ? 0 : 1);
|
|
124
124
|
if (outcome.ok)
|
|
125
125
|
reportSuccess(outcome.result);
|
|
@@ -130,7 +130,7 @@ async function routeFreshFolder(ctx, options, populated) {
|
|
|
130
130
|
const organizationId = await resolveOrganizationInteractive(ctx, options);
|
|
131
131
|
if (!organizationId)
|
|
132
132
|
return;
|
|
133
|
-
const identity = await
|
|
133
|
+
const identity = await promptProjectIdentity(ctx);
|
|
134
134
|
if (!identity)
|
|
135
135
|
return;
|
|
136
136
|
const input = { organizationId, name: identity.name, slug: identity.slug, hasUi: true };
|
|
@@ -148,26 +148,26 @@ async function routeFreshFolder(ctx, options, populated) {
|
|
|
148
148
|
else
|
|
149
149
|
reportFailure(outcome);
|
|
150
150
|
}
|
|
151
|
-
async function routeWebFirst(ctx, options,
|
|
151
|
+
async function routeWebFirst(ctx, options, projectId) {
|
|
152
152
|
const organizationId = await resolveOrganizationInteractive(ctx, options);
|
|
153
153
|
if (!organizationId)
|
|
154
154
|
return;
|
|
155
|
-
let outcome = await webFirstAttach(ctx, {
|
|
155
|
+
let outcome = await webFirstAttach(ctx, { projectId, organizationId, confirmPopulated: ctx.yes || undefined });
|
|
156
156
|
if (!outcome.ok && outcome.code === "CONFIRMATION_REQUIRED") {
|
|
157
157
|
const confirmed = ctx.yes || (ctx.interactive && (await confirm(outcome.message)));
|
|
158
158
|
if (!confirmed) {
|
|
159
159
|
p.outro("No changes made — the folder was left untouched.");
|
|
160
160
|
return;
|
|
161
161
|
}
|
|
162
|
-
outcome = await webFirstAttach(ctx, {
|
|
162
|
+
outcome = await webFirstAttach(ctx, { projectId, organizationId, confirmPopulated: true });
|
|
163
163
|
}
|
|
164
164
|
if (!outcome.ok) {
|
|
165
165
|
reportFailure(outcome);
|
|
166
166
|
return;
|
|
167
167
|
}
|
|
168
168
|
if (outcome.result.overwriteRequired) {
|
|
169
|
-
p.log.warn(`Local content diverges from the
|
|
170
|
-
"Nothing was changed — review and approve with `nustack
|
|
169
|
+
p.log.warn(`Local content diverges from the Project on ${outcome.result.divergent.map((d) => d.path).join(", ")}. ` +
|
|
170
|
+
"Nothing was changed — review and approve with `nustack project sync approve` (a recovery bundle is kept).");
|
|
171
171
|
}
|
|
172
172
|
reportSuccess(outcome.result);
|
|
173
173
|
}
|
|
@@ -192,7 +192,7 @@ async function resolveOrganizationInteractive(ctx, options) {
|
|
|
192
192
|
return organization.id;
|
|
193
193
|
}
|
|
194
194
|
const pick = await p.select({
|
|
195
|
-
message: "Which Organization should this
|
|
195
|
+
message: "Which Organization should this Project live in?",
|
|
196
196
|
options: memberships.map((m) => ({ value: m.organization.id, label: `${m.organization.label} (${m.organization.slug})` })),
|
|
197
197
|
});
|
|
198
198
|
if (p.isCancel(pick))
|
|
@@ -200,15 +200,15 @@ async function resolveOrganizationInteractive(ctx, options) {
|
|
|
200
200
|
upsertDeviceState(ctx.store, { activeOrganizationId: String(pick) });
|
|
201
201
|
return String(pick);
|
|
202
202
|
}
|
|
203
|
-
async function
|
|
203
|
+
async function chooseOrCreateProject(ctx, organizationId, root, entryMode) {
|
|
204
204
|
const CREATE = "__create__";
|
|
205
|
-
const
|
|
205
|
+
const projects = ctx.interactive ? await listProjects(ctx.baseUrl, ctx.accessToken, organizationId) : [];
|
|
206
206
|
const pick = ctx.interactive
|
|
207
207
|
? await p.select({
|
|
208
|
-
message: "Attach this repository to which
|
|
208
|
+
message: "Attach this repository to which Project?",
|
|
209
209
|
options: [
|
|
210
|
-
{ value: CREATE, label: "Create a new
|
|
211
|
-
...
|
|
210
|
+
{ value: CREATE, label: "Create a new Project" },
|
|
211
|
+
...projects.map((a) => ({ value: a.id, label: `${a.label} (${a.id})` })),
|
|
212
212
|
],
|
|
213
213
|
})
|
|
214
214
|
: CREATE;
|
|
@@ -216,33 +216,33 @@ async function chooseOrCreateWorkspace(ctx, organizationId, root, entryMode) {
|
|
|
216
216
|
return null;
|
|
217
217
|
if (pick !== CREATE)
|
|
218
218
|
return String(pick);
|
|
219
|
-
const identity = await
|
|
219
|
+
const identity = await promptProjectIdentity(ctx, deriveRepoName(path.basename(root)));
|
|
220
220
|
if (!identity)
|
|
221
221
|
return null;
|
|
222
|
-
return
|
|
222
|
+
return createAndAwaitProject(ctx, organizationId, identity, entryMode);
|
|
223
223
|
}
|
|
224
|
-
async function
|
|
224
|
+
async function createAndAwaitProject(ctx, organizationId, identity, entryMode) {
|
|
225
225
|
void entryMode;
|
|
226
226
|
const spinner = p.spinner();
|
|
227
|
-
spinner.start(`Creating
|
|
227
|
+
spinner.start(`Creating Project "${identity.name}"`);
|
|
228
228
|
try {
|
|
229
|
-
const outcome = await
|
|
230
|
-
const final = await
|
|
229
|
+
const outcome = await createProjectViaAsk(ctx.baseUrl, ctx.accessToken, organizationId, `Create a project named "${identity.name}".`);
|
|
230
|
+
const final = await getProject(ctx.baseUrl, ctx.accessToken, outcome.projectId);
|
|
231
231
|
const failed = final.status === "initialization_failed" || final.initStatus === "failed";
|
|
232
|
-
spinner.stop(failed ? "
|
|
233
|
-
return failed ? null : outcome.
|
|
232
|
+
spinner.stop(failed ? "Project initialization failed" : `Project ${outcome.projectId} ready`, failed ? 1 : 0);
|
|
233
|
+
return failed ? null : outcome.projectId;
|
|
234
234
|
}
|
|
235
235
|
catch (error) {
|
|
236
|
-
spinner.stop("
|
|
236
|
+
spinner.stop("Project creation failed", 1);
|
|
237
237
|
p.log.error(error instanceof Error ? error.message : String(error));
|
|
238
238
|
return null;
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
-
async function
|
|
241
|
+
async function promptProjectIdentity(ctx, fallback) {
|
|
242
242
|
const defaultName = fallback ?? deriveRepoName(path.basename(ctx.cwd));
|
|
243
243
|
if (!ctx.interactive)
|
|
244
244
|
return { name: defaultName, slug: deriveRepoName(defaultName) };
|
|
245
|
-
const name = await p.text({ message: "Name this
|
|
245
|
+
const name = await p.text({ message: "Name this Project:", defaultValue: defaultName, placeholder: defaultName });
|
|
246
246
|
if (p.isCancel(name))
|
|
247
247
|
return null;
|
|
248
248
|
const resolved = String(name).trim() || defaultName;
|
|
@@ -253,9 +253,9 @@ async function confirm(message) {
|
|
|
253
253
|
return !p.isCancel(answer) && answer === true;
|
|
254
254
|
}
|
|
255
255
|
function reportSuccess(result) {
|
|
256
|
-
p.log.success(`Bound to
|
|
256
|
+
p.log.success(`Bound to Project ${result.projectId} on branch ${result.boundBranch} (binding ${result.bindingId}).`);
|
|
257
257
|
p.log.info(`Toolkit ${result.toolkit.name}@${result.toolkit.version}. No Git push, pull, or remote was created.`);
|
|
258
|
-
p.outro("Ready — `nustack
|
|
258
|
+
p.outro("Ready — `nustack project status` · `nustack docs list` · `nustack project sync now`.");
|
|
259
259
|
}
|
|
260
260
|
function reportFailure(failure) {
|
|
261
261
|
p.log.error(failure.message);
|
|
@@ -264,8 +264,8 @@ function reportFailure(failure) {
|
|
|
264
264
|
}
|
|
265
265
|
function rejectionMessage(rejection) {
|
|
266
266
|
switch (rejection) {
|
|
267
|
-
case "
|
|
268
|
-
return "This repository is nested inside another NuStack
|
|
267
|
+
case "nested_project":
|
|
268
|
+
return "This repository is nested inside another NuStack Project — one Project has one logical repository root (F-014).";
|
|
269
269
|
case "operation_in_progress":
|
|
270
270
|
return "A merge, rebase, cherry-pick, or revert is in progress — finish or abort it, then re-run.";
|
|
271
271
|
case "detached_head":
|
package/dist/commands/login.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
3
|
import { launchBrowser } from "../lib/browser_opener.js";
|
|
4
|
-
import {
|
|
4
|
+
import { selectCredentialStore } from "../lib/credential_store.js";
|
|
5
5
|
import { isInteractive } from "../lib/interactive.js";
|
|
6
|
-
import { ensureAccessToken, loginWithLoopback, loginWithPastedCode, prepareLogin, } from "../lib/auth_grant.js";
|
|
6
|
+
import { ensureAccessToken, ENV_TOKEN_VARIABLE, envToken, loginWithLoopback, loginWithPastedCode, prepareLogin, } from "../lib/auth_grant.js";
|
|
7
7
|
async function readStdinLine() {
|
|
8
8
|
return new Promise((resolve) => {
|
|
9
9
|
let buffered = "";
|
|
@@ -79,8 +79,16 @@ async function loopbackFlow(ctx, flags) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
export async function performDeviceLogin(baseUrl, _cwd = process.cwd(), flags = {}) {
|
|
82
|
-
if (
|
|
83
|
-
p.log.
|
|
82
|
+
if (envToken()) {
|
|
83
|
+
p.log.info(`${ENV_TOKEN_VARIABLE} is set — this process authenticates with it, and no sign-in is stored on this machine. Unset it to sign in as a person.`);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
let store;
|
|
87
|
+
try {
|
|
88
|
+
store = selectCredentialStore();
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
p.log.error(error instanceof Error ? error.message : String(error));
|
|
84
92
|
return false;
|
|
85
93
|
}
|
|
86
94
|
if (!flags.force && !flags.reauthorize) {
|
|
@@ -118,7 +126,12 @@ export async function performDeviceLogin(baseUrl, _cwd = process.cwd(), flags =
|
|
|
118
126
|
return false;
|
|
119
127
|
}
|
|
120
128
|
p.log.success(`Signed in as ${result.userEmail || "this account"}`);
|
|
121
|
-
|
|
129
|
+
if (store.name === "file") {
|
|
130
|
+
p.log.info(`This machine's grant is saved to ~/.nustack/credentials.json, readable only by you — no OS keychain here. Manage it with \`nustack device list\`.`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
p.log.info("This machine's grant is saved to your keychain — the desktop shares it. Manage it with `nustack device list`.");
|
|
134
|
+
}
|
|
122
135
|
return true;
|
|
123
136
|
}
|
|
124
137
|
export async function runLogin(options, flags = {}) {
|
|
@@ -130,5 +143,5 @@ export async function runLogin(options, flags = {}) {
|
|
|
130
143
|
process.exitCode = 1;
|
|
131
144
|
return;
|
|
132
145
|
}
|
|
133
|
-
p.outro("Signed in on this device.");
|
|
146
|
+
p.outro(envToken() ? "Nothing was written." : "Signed in on this device.");
|
|
134
147
|
}
|
package/dist/commands/logout.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
3
|
import { clearDeviceId } from "../lib/device_identity.js";
|
|
4
|
-
import { deleteDeviceCredential
|
|
5
|
-
import {
|
|
4
|
+
import { deleteDeviceCredential } from "../lib/device_keychain.js";
|
|
5
|
+
import { selectCredentialStore } from "../lib/credential_store.js";
|
|
6
|
+
import { ENV_TOKEN_VARIABLE, envToken, fetchAuthConfig, readGrant, revokeGrant } from "../lib/auth_grant.js";
|
|
6
7
|
const SHARED_SIGNOUT_NOTE = "This signs out both the CLI and the desktop on this machine.";
|
|
7
8
|
export async function runLogout(options, flags = {}) {
|
|
8
9
|
p.intro("nustack logout");
|
|
9
10
|
const baseUrl = resolveNuStackUrl(options);
|
|
10
|
-
|
|
11
|
+
if (envToken()) {
|
|
12
|
+
p.log.info(`${ENV_TOKEN_VARIABLE} is set — the environment holds this process's credential, and there is nothing stored on this machine to end. Unset the variable, or revoke the token with \`nustack token revoke\`.`);
|
|
13
|
+
p.outro("Nothing was changed.");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const store = selectCredentialStore();
|
|
17
|
+
const grant = await readGrant();
|
|
11
18
|
if (grant) {
|
|
12
19
|
const clientId = grant.clientId ?? (await fetchAuthConfig(baseUrl).then((c) => c.clientId).catch(() => null));
|
|
13
20
|
const outcome = clientId ? await revokeGrant({ authBaseUrl: grant.authBaseUrl, refreshToken: grant.grantToken, clientId }) : "unreachable";
|
|
@@ -25,12 +32,13 @@ export async function runLogout(options, flags = {}) {
|
|
|
25
32
|
}
|
|
26
33
|
const cleared = await deleteDeviceCredential(grant.deviceId);
|
|
27
34
|
if (cleared)
|
|
28
|
-
p.log.success("Cleared the keychain grant.");
|
|
35
|
+
p.log.success(store.name === "keychain" ? "Cleared the keychain grant." : `Cleared the stored grant (${store.name}).`);
|
|
29
36
|
}
|
|
30
37
|
else {
|
|
31
|
-
p.log.info("No keychain credential to clear on this machine.");
|
|
38
|
+
p.log.info(store.name === "keychain" ? "No keychain credential to clear on this machine." : `No stored credential to clear on this machine (${store.name}).`);
|
|
32
39
|
}
|
|
33
|
-
|
|
40
|
+
if (store.name === "keychain")
|
|
41
|
+
p.log.info(SHARED_SIGNOUT_NOTE);
|
|
34
42
|
if (flags.forgetDevice) {
|
|
35
43
|
const forgot = await clearDeviceId();
|
|
36
44
|
if (forgot) {
|