@nurix/nustack 0.10.0-dev.13 → 0.10.0-dev.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/bootstrap.js +4 -4
- package/dist/commands/docs.js +13 -13
- package/dist/commands/doctor.js +13 -13
- package/dist/commands/handoff.js +8 -8
- package/dist/commands/init.js +43 -43
- 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 +5 -5
- package/dist/commands/search.js +7 -7
- package/dist/commands/sessions.js +1 -1
- package/dist/commands/sync.js +10 -10
- package/dist/commands/toolkit.js +19 -19
- package/dist/index.js +29 -29
- 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/lanes/supervisor.js +8 -8
- 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/nustack_client.js +45 -45
- 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/session_telemetry.js +2 -2
- package/dist/lib/sessions/client.js +8 -8
- package/dist/lib/sessions/outsideSessions.js +3 -3
- package/dist/lib/telemetry/config_renderer.js +3 -3
- package/dist/lib/telemetry/health_report.js +1 -1
- package/package.json +3 -3
- package/dist/lib/local_store/workspace_events_cache.js +0 -54
|
@@ -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
|
@@ -2,9 +2,9 @@ import path from "node:path";
|
|
|
2
2
|
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
3
|
import { ensureAccessToken } from "../lib/auth_grant.js";
|
|
4
4
|
import { isKeychainAvailable } from "../lib/device_keychain.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveProjectId } from "../lib/project_context.js";
|
|
6
6
|
import { readManagedValues } from "../lib/env_file.js";
|
|
7
|
-
import {
|
|
7
|
+
import { readProjectManifests, diagnoseManifests } from "../lib/onboarding/manifest_writer.js";
|
|
8
8
|
import { fetchToolkitProfile, ToolkitProfileUnavailableError } from "../lib/etna/profile.js";
|
|
9
9
|
import { toolkitCompatibility } from "../lib/etna/reconcile.js";
|
|
10
10
|
import { openLocalStore } from "../lib/local_store/database.js";
|
|
@@ -62,9 +62,9 @@ export async function runDoctor(options) {
|
|
|
62
62
|
addRepositoryRows(add, repository);
|
|
63
63
|
}
|
|
64
64
|
const contextRoot = repository?.repositoryRoot ?? cwd;
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const cloudProfile = deviceAuth &&
|
|
65
|
+
const projectResolution = await resolveProjectId(contextRoot, { project: options.project });
|
|
66
|
+
const projectId = projectResolution.ok ? projectResolution.projectId : null;
|
|
67
|
+
const cloudProfile = deviceAuth && projectId ? await fetchCloudProfile(baseUrl, deviceAuth.accessToken, projectId) : null;
|
|
68
68
|
const binding = store && repository ? getBindingByRepositoryRoot(store, repository.repositoryRoot) : null;
|
|
69
69
|
await addManifestRows(add, contextRoot, cloudProfile);
|
|
70
70
|
addBindingRows(add, store, binding, cloudProfile);
|
|
@@ -127,9 +127,9 @@ function addRepositoryRows(add, repository) {
|
|
|
127
127
|
else
|
|
128
128
|
add("repository", "HEAD", "info", "unborn branch — no commits yet");
|
|
129
129
|
}
|
|
130
|
-
async function fetchCloudProfile(baseUrl, accessToken,
|
|
130
|
+
async function fetchCloudProfile(baseUrl, accessToken, projectId) {
|
|
131
131
|
try {
|
|
132
|
-
return await fetchToolkitProfile(baseUrl, accessToken,
|
|
132
|
+
return await fetchToolkitProfile(baseUrl, accessToken, projectId);
|
|
133
133
|
}
|
|
134
134
|
catch (error) {
|
|
135
135
|
if (error instanceof ToolkitProfileUnavailableError)
|
|
@@ -138,12 +138,12 @@ async function fetchCloudProfile(baseUrl, accessToken, workspaceId) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
async function addManifestRows(add, root, cloud) {
|
|
141
|
-
const manifests = await
|
|
142
|
-
if (!manifests.
|
|
143
|
-
add("manifests", "
|
|
141
|
+
const manifests = await readProjectManifests(root);
|
|
142
|
+
if (!manifests.projectManifest && !manifests.serviceToolkit) {
|
|
143
|
+
add("manifests", "Project binding markers", "info", "no `.nustack/app.json` or Service toolkit block — repository not attached");
|
|
144
144
|
return;
|
|
145
145
|
}
|
|
146
|
-
const diagnosis = diagnoseManifests(cloud ? { name: cloud.name, version: cloud.version } : null, manifests.
|
|
146
|
+
const diagnosis = diagnoseManifests(cloud ? { name: cloud.name, version: cloud.version } : null, manifests.projectManifest?.toolkit ?? null, manifests.serviceToolkit);
|
|
147
147
|
const status = diagnosis === "aligned" ? "ok" : diagnosis === "missing" ? "warn" : "warn";
|
|
148
148
|
const suffix = diagnosis === "aligned" || diagnosis === "missing" ? "" : " — run `nustack toolkit reconcile`";
|
|
149
149
|
add("manifests", "Marker / Service agreement", status, `${diagnosis}${cloud ? "" : " (no cloud pin reachable)"}${suffix}`);
|
|
@@ -154,7 +154,7 @@ function addBindingRows(add, store, binding, cloud) {
|
|
|
154
154
|
return;
|
|
155
155
|
}
|
|
156
156
|
add("sync", "Sync Whitelist cache", binding.whitelistManifestSha256 ? "ok" : "info", binding.whitelistManifestSha256 ? "cached" : "not cached yet");
|
|
157
|
-
add("sync", "
|
|
157
|
+
add("sync", "Project cursor", "ok", `applied Project sequence ${binding.lastAppliedProjectSequence}`);
|
|
158
158
|
if (store) {
|
|
159
159
|
const pending = listApplicationsForBinding(store, binding.id, [
|
|
160
160
|
"received",
|
|
@@ -208,7 +208,7 @@ function addTelemetryRows(add, store) {
|
|
|
208
208
|
async function addRecoveryRows(add, store) {
|
|
209
209
|
await sweepExpiredBundles(store).catch(() => { });
|
|
210
210
|
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
|
|
211
|
+
add("recovery", "Recovery bundles", active.length > 0 ? "info" : "ok", active.length > 0 ? `${active.length} active — \`nustack project sync recovery list\`` : "none");
|
|
212
212
|
const deviceRef = readDeviceState(store)?.deviceRef ?? null;
|
|
213
213
|
if (!deviceRef)
|
|
214
214
|
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":
|
|
@@ -12,13 +12,13 @@ import { listBundles, restoreBundle, deleteBundle, sweepExpiredBundles, } from "
|
|
|
12
12
|
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
13
13
|
import { registerOutbound, createUploadClient, deviceTokenProvider, describeHistoryDecision, publishCurrentState, adoptWithoutPublishing, } from "../lib/git-sync/outbound/index.js";
|
|
14
14
|
import { latestAcceptedUpload } from "../lib/local_store/accepted_uploads.js";
|
|
15
|
-
const ONBOARDING_POINTER = "No NuStack
|
|
15
|
+
const ONBOARDING_POINTER = "No NuStack Project bindings on this machine. Run `nustack project attach` inside a Git repository to bind it.";
|
|
16
16
|
function writeLine(line) {
|
|
17
17
|
process.stdout.write(`${line}\n`);
|
|
18
18
|
}
|
|
19
|
-
export function
|
|
19
|
+
export function registerProjectSyncCommands(projectCommand, getGlobalOptions) {
|
|
20
20
|
void getGlobalOptions;
|
|
21
|
-
const sync =
|
|
21
|
+
const sync = projectCommand
|
|
22
22
|
.command("sync")
|
|
23
23
|
.description("Synchronize NuStack Documents with your local Git repository");
|
|
24
24
|
sync
|
|
@@ -393,7 +393,7 @@ function outboundStatusFor(store, binding) {
|
|
|
393
393
|
nextRetryAt: row.next != null ? new Date(row.next).toISOString() : null,
|
|
394
394
|
blockedErrorCode: binding.status === "failed" ? binding.lastErrorCode : null,
|
|
395
395
|
lastAcceptedHandoffId: accepted?.handoffId ?? null,
|
|
396
|
-
|
|
396
|
+
lastAcceptedProjectSequence: accepted?.projectSequence ?? null,
|
|
397
397
|
historyDecisionPending,
|
|
398
398
|
};
|
|
399
399
|
}
|
|
@@ -527,7 +527,7 @@ async function runStatus(options) {
|
|
|
527
527
|
const outbound = observation.outbound;
|
|
528
528
|
const outboundNote = (outbound.pendingUploadCount > 0 ? ` queued=${outbound.pendingUploadCount}` : "") +
|
|
529
529
|
(outbound.blockedErrorCode ? ` blocked=${outbound.blockedErrorCode}` : "") +
|
|
530
|
-
(outbound.
|
|
530
|
+
(outbound.lastAcceptedProjectSequence != null ? ` accepted@${outbound.lastAcceptedProjectSequence}` : "") +
|
|
531
531
|
(outbound.historyDecisionPending ? " history-decision-required" : "");
|
|
532
532
|
writeLine(`${observation.bindingId} ${observation.status} branch=${branchNote} ` +
|
|
533
533
|
`head=${head} processed=${processed} pending=${observation.pendingCommitCount}` +
|