@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
package/dist/commands/sync.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import * as p from "@clack/prompts";
|
|
3
|
-
import { exchangeServiceCredential, fetchServiceCredential,
|
|
3
|
+
import { exchangeServiceCredential, fetchServiceCredential, listProjectIntegrations, resolveNuStackUrl, } from "../lib/nustack_client.js";
|
|
4
4
|
import { readConsumesEdges } from "../lib/manifest.js";
|
|
5
5
|
import { ensureAccessToken, signedOutCopy } from "../lib/auth_grant.js";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveProjectId } from "../lib/project_context.js";
|
|
7
7
|
import { readManagedValues, writeManagedValues } from "../lib/env_file.js";
|
|
8
8
|
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
9
9
|
const COMMAND = "sync";
|
|
@@ -18,23 +18,23 @@ export async function runSync(options) {
|
|
|
18
18
|
if (!deviceAuth)
|
|
19
19
|
return failSync(json, "SIGNED_OUT", signedOut(), "The managed block was not touched.");
|
|
20
20
|
const accessToken = deviceAuth.accessToken;
|
|
21
|
-
const
|
|
22
|
-
if (!
|
|
23
|
-
return failSync(json, "
|
|
21
|
+
const project = await resolveProjectId(cwd, { project: options.project });
|
|
22
|
+
if (!project.ok)
|
|
23
|
+
return failSync(json, "PROJECT_UNRESOLVED", project.message, "The managed block was not touched.");
|
|
24
24
|
const spinner = json ? null : p.spinner();
|
|
25
25
|
spinner?.start("Exchanging declared services");
|
|
26
26
|
let activeServices;
|
|
27
27
|
let projections;
|
|
28
28
|
try {
|
|
29
29
|
for (const edge of await readConsumesEdges(cwd)) {
|
|
30
|
-
await exchangeServiceCredential(baseUrl, accessToken, {
|
|
30
|
+
await exchangeServiceCredential(baseUrl, accessToken, { projectId: project.projectId, service: edge.service });
|
|
31
31
|
}
|
|
32
32
|
spinner?.message("Enumerating active integrations");
|
|
33
|
-
const integrations = await
|
|
33
|
+
const integrations = await listProjectIntegrations(baseUrl, accessToken, project.projectId);
|
|
34
34
|
activeServices = integrations.filter((i) => i.status === "active").map((i) => i.serviceSlug);
|
|
35
35
|
projections = [];
|
|
36
36
|
for (const service of activeServices) {
|
|
37
|
-
projections.push(await fetchServiceCredential(baseUrl, accessToken, {
|
|
37
|
+
projections.push(await fetchServiceCredential(baseUrl, accessToken, { projectId: project.projectId, service }));
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
catch (error) {
|
|
@@ -59,7 +59,7 @@ export async function runSync(options) {
|
|
|
59
59
|
await writeManagedValues(path.join(cwd, ".env.example"), placeholders);
|
|
60
60
|
if (json) {
|
|
61
61
|
printJsonSuccess(COMMAND, {
|
|
62
|
-
|
|
62
|
+
projectId: project.projectId,
|
|
63
63
|
integrations: activeServices.map((service) => ({ service, status: "active" })),
|
|
64
64
|
envKeysWritten: Object.keys(nextValues),
|
|
65
65
|
removedKeys,
|
|
@@ -69,7 +69,7 @@ export async function runSync(options) {
|
|
|
69
69
|
p.log.success(`Managed block rewritten: ${projections.length} service credential(s).`);
|
|
70
70
|
if (removedKeys.length > 0)
|
|
71
71
|
p.log.info(`Removed stale entries: ${removedKeys.join(", ")}`);
|
|
72
|
-
p.outro("In sync with NuStack — Service credentials only (Documents & Git are `nustack
|
|
72
|
+
p.outro("In sync with NuStack — Service credentials only (Documents & Git are `nustack project sync`).");
|
|
73
73
|
}
|
|
74
74
|
function failSync(json, code, message, humanTail) {
|
|
75
75
|
if (json) {
|
package/dist/commands/toolkit.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as p from "@clack/prompts";
|
|
2
2
|
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
3
|
import { ensureAuthWithRecovery, lastAuthFailure } from "../lib/auth_recovery.js";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveProjectId } from "../lib/project_context.js";
|
|
5
5
|
import { isInteractive } from "../lib/interactive.js";
|
|
6
6
|
import { openLocalStore } from "../lib/local_store/database.js";
|
|
7
7
|
import { getBindingByRepositoryRoot } from "../lib/local_store/bindings.js";
|
|
8
8
|
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
9
|
-
import {
|
|
9
|
+
import { readProjectManifests, diagnoseManifests } from "../lib/onboarding/manifest_writer.js";
|
|
10
10
|
import { fetchToolkitProfile, ToolkitProfileUnavailableError } from "../lib/etna/profile.js";
|
|
11
11
|
import { buildReconcilePlan, applyReconcile, toolkitCompatibility } from "../lib/etna/reconcile.js";
|
|
12
12
|
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
@@ -28,9 +28,9 @@ async function resolveContext(command, json, options, globals) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
const baseUrl = resolveNuStackUrl(globals);
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
33
|
-
emitFail(command, json, "
|
|
31
|
+
const project = await resolveProjectId(root, options);
|
|
32
|
+
if (!project.ok) {
|
|
33
|
+
emitFail(command, json, "PROJECT_CONTEXT_MISSING", project.message);
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
36
|
const deviceAuth = await ensureAuthWithRecovery(baseUrl, json);
|
|
@@ -39,7 +39,7 @@ async function resolveContext(command, json, options, globals) {
|
|
|
39
39
|
emitFail(command, json, failure.code, failure.message);
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
|
-
return { baseUrl, accessToken: deviceAuth.accessToken,
|
|
42
|
+
return { baseUrl, accessToken: deviceAuth.accessToken, projectId: project.projectId, root };
|
|
43
43
|
}
|
|
44
44
|
export async function runToolkitStatus(options, globals) {
|
|
45
45
|
const json = options.json === true;
|
|
@@ -48,7 +48,7 @@ export async function runToolkitStatus(options, globals) {
|
|
|
48
48
|
return;
|
|
49
49
|
let cloud;
|
|
50
50
|
try {
|
|
51
|
-
cloud = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, ctx.
|
|
51
|
+
cloud = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, ctx.projectId);
|
|
52
52
|
}
|
|
53
53
|
catch (error) {
|
|
54
54
|
if (error instanceof ToolkitProfileUnavailableError) {
|
|
@@ -57,24 +57,24 @@ export async function runToolkitStatus(options, globals) {
|
|
|
57
57
|
}
|
|
58
58
|
throw error;
|
|
59
59
|
}
|
|
60
|
-
const manifests = await
|
|
61
|
-
const diagnosis = diagnoseManifests({ name: cloud.name, version: cloud.version }, manifests.
|
|
60
|
+
const manifests = await readProjectManifests(ctx.root);
|
|
61
|
+
const diagnosis = diagnoseManifests({ name: cloud.name, version: cloud.version }, manifests.projectManifest?.toolkit ?? null, manifests.serviceToolkit);
|
|
62
62
|
const binding = await withStore((store) => getBindingByRepositoryRoot(store, ctx.root));
|
|
63
63
|
const compatibility = toolkitCompatibility(binding ?? { etnaProfileJson: null }, cloud);
|
|
64
64
|
if (json) {
|
|
65
65
|
printJsonSuccess("toolkit status", {
|
|
66
|
-
|
|
66
|
+
projectId: ctx.projectId,
|
|
67
67
|
cloud,
|
|
68
|
-
|
|
68
|
+
projectJson: manifests.projectManifest?.toolkit ?? null,
|
|
69
69
|
serviceJson: manifests.serviceToolkit,
|
|
70
70
|
diagnosis,
|
|
71
71
|
compatibility,
|
|
72
72
|
});
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
|
-
console.log(`Toolkit for
|
|
75
|
+
console.log(`Toolkit for Project ${ctx.projectId}`);
|
|
76
76
|
console.log(` cloud pin: ${cloud.name}@${cloud.version} (renderer ${cloud.rendererVersion}, placement ${cloud.placementMapVersion})`);
|
|
77
|
-
console.log(` .nustack/app.json: ${fmtToolkit(manifests.
|
|
77
|
+
console.log(` .nustack/app.json: ${fmtToolkit(manifests.projectManifest?.toolkit)}`);
|
|
78
78
|
console.log(` nustack.service.json:${fmtToolkit(manifests.serviceToolkit)}`);
|
|
79
79
|
console.log(` diagnosis: ${diagnosis}`);
|
|
80
80
|
console.log(` write gate: ${compatibility.blocksWrites ? `BLOCKED — ${compatibility.reason}` : "clear (projection and sync writes allowed)"}`);
|
|
@@ -89,7 +89,7 @@ export async function runToolkitReconcile(options, globals) {
|
|
|
89
89
|
return;
|
|
90
90
|
let cloud;
|
|
91
91
|
try {
|
|
92
|
-
cloud = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, ctx.
|
|
92
|
+
cloud = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, ctx.projectId);
|
|
93
93
|
}
|
|
94
94
|
catch (error) {
|
|
95
95
|
if (error instanceof ToolkitProfileUnavailableError) {
|
|
@@ -99,12 +99,12 @@ export async function runToolkitReconcile(options, globals) {
|
|
|
99
99
|
throw error;
|
|
100
100
|
}
|
|
101
101
|
const binding = await withStore((store) => getBindingByRepositoryRoot(store, ctx.root));
|
|
102
|
-
const manifests = await
|
|
103
|
-
const identity = manifests.
|
|
104
|
-
? {
|
|
105
|
-
: manifests.
|
|
102
|
+
const manifests = await readProjectManifests(ctx.root);
|
|
103
|
+
const identity = manifests.projectManifest
|
|
104
|
+
? { projectId: manifests.projectManifest.workspaceId, organizationId: manifests.projectManifest.organizationId }
|
|
105
|
+
: manifests.serviceProjectBinding;
|
|
106
106
|
if (!identity) {
|
|
107
|
-
emitFail("toolkit reconcile", json, "
|
|
107
|
+
emitFail("toolkit reconcile", json, "PROJECT_CONTEXT_MISSING", "No `.nustack/app.json` binding to reconcile — attach the repository first.");
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
const plan = await buildReconcilePlan(ctx.root, binding ?? { etnaProfileJson: null }, cloud);
|
package/dist/index.js
CHANGED
|
@@ -16,8 +16,8 @@ import { runLogout } from "./commands/logout.js";
|
|
|
16
16
|
import { runTelemetry } from "./commands/telemetry.js";
|
|
17
17
|
import { runSearch, runSearchServices } from "./commands/search.js";
|
|
18
18
|
import { runDeviceList, runDeviceRevoke, runDeviceStatus } from "./commands/device.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { registerProjectSyncCommands } from "./commands/project-sync.js";
|
|
20
|
+
import { registerProjectCommands } from "./commands/project.js";
|
|
21
21
|
import { runOrganizationList, runOrganizationCreate, runOrganizationUse } from "./commands/organization.js";
|
|
22
22
|
import { runDocsList, runDocsShow, runDocsStatus } from "./commands/docs.js";
|
|
23
23
|
import { runHandoffList, runHandoffShow } from "./commands/handoff.js";
|
|
@@ -52,8 +52,8 @@ program
|
|
|
52
52
|
.action(async (options) => runLogout(program.opts(), { forgetDevice: options.forgetDevice }));
|
|
53
53
|
program
|
|
54
54
|
.command("init")
|
|
55
|
-
.description("Onboard this folder: sign in, then bind it to a
|
|
56
|
-
.option("--organization <id>", "the Organization to create or select the
|
|
55
|
+
.description("Onboard this folder: sign in, then bind it to a Project (the onboarding router)")
|
|
56
|
+
.option("--organization <id>", "the Organization to create or select the Project in")
|
|
57
57
|
.option("--yes", "pre-confirm the populated-folder onboarding guard")
|
|
58
58
|
.action(async (options) => runInit({ ...program.opts(), ...options }));
|
|
59
59
|
program
|
|
@@ -69,8 +69,8 @@ program
|
|
|
69
69
|
.action(async (options) => runList(options, program.opts()));
|
|
70
70
|
program
|
|
71
71
|
.command("sync")
|
|
72
|
-
.description("Project this
|
|
73
|
-
.option("--
|
|
72
|
+
.description("Project this Project's Service credentials into the managed .env block (not Documents or Git)")
|
|
73
|
+
.option("--project <id>", "the Project to act as (defaults to nustack.service.json / NUSTACK_WORKSPACE_ID)")
|
|
74
74
|
.option("--json", "emit the versioned machine envelope instead of interactive output")
|
|
75
75
|
.action(async (options) => runSync({ ...program.opts(), ...options }));
|
|
76
76
|
program
|
|
@@ -98,7 +98,7 @@ program
|
|
|
98
98
|
program
|
|
99
99
|
.command("doctor")
|
|
100
100
|
.description("Run the full local diagnostics matrix (identity, repository, manifests, toolkit, telemetry, credentials)")
|
|
101
|
-
.option("--
|
|
101
|
+
.option("--project <id>", "the Project to diagnose (defaults to the bound repository)")
|
|
102
102
|
.option("--json", "emit the versioned machine envelope (secrets and absolute paths redacted)")
|
|
103
103
|
.option("--local-verbose", "include Device-local absolute paths in the output")
|
|
104
104
|
.action(async (options) => runDoctor({ ...program.opts(), ...options }));
|
|
@@ -144,9 +144,9 @@ program
|
|
|
144
144
|
const toLimit = (value) => Number.parseInt(value, 10);
|
|
145
145
|
const search = program
|
|
146
146
|
.command("search [keywords...]")
|
|
147
|
-
.description("Search the NuStack Directory for
|
|
147
|
+
.description("Search the NuStack Directory for projects by keyword")
|
|
148
148
|
.option("--json", "emit the raw results array as JSON")
|
|
149
|
-
.option("--category <cat>", "filter
|
|
149
|
+
.option("--category <cat>", "filter projects by category")
|
|
150
150
|
.option("--limit <n>", "max results (default 20)", toLimit)
|
|
151
151
|
.action(async (keywords, options) => runSearch(keywords, options, program.opts()));
|
|
152
152
|
search
|
|
@@ -156,8 +156,8 @@ search
|
|
|
156
156
|
.option("--type <type>", "filter services by type")
|
|
157
157
|
.option("--limit <n>", "max results (default 20)", toLimit)
|
|
158
158
|
.action(async (keywords, options) => runSearchServices(keywords, { ...search.opts(), ...options }, program.opts()));
|
|
159
|
-
search.addHelpText("after", '\n`services` is a reserved subcommand. To search
|
|
160
|
-
"explicit separator: `nustack search -- services` (everything after `--` is an
|
|
159
|
+
search.addHelpText("after", '\n`services` is a reserved subcommand. To search projects for the literal word "services", use the\n' +
|
|
160
|
+
"explicit separator: `nustack search -- services` (everything after `--` is an projects query).");
|
|
161
161
|
const organization = program.command("organization").description("Work with your NuStack Organizations");
|
|
162
162
|
organization
|
|
163
163
|
.command("list")
|
|
@@ -174,40 +174,40 @@ organization
|
|
|
174
174
|
.description("Set the active Organization context for this device")
|
|
175
175
|
.option("--json", "emit the versioned machine envelope")
|
|
176
176
|
.action(async (target, options) => runOrganizationUse(target, options, program.opts()));
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
const project = program.command("project").description("Work with NuStack Projects");
|
|
178
|
+
registerProjectCommands(project, () => program.opts());
|
|
179
|
+
registerProjectSyncCommands(project, () => program.opts());
|
|
180
180
|
registerReviewCommands(program.command("review").description("Read and mirror NuStack Review items"), () => program.opts());
|
|
181
|
-
const docs = program.command("docs").description("Work with a
|
|
181
|
+
const docs = program.command("docs").description("Work with a Project's Documents");
|
|
182
182
|
docs
|
|
183
183
|
.command("list")
|
|
184
|
-
.description("List the
|
|
185
|
-
.option("--
|
|
184
|
+
.description("List the Project's Documents")
|
|
185
|
+
.option("--project <id>", "the Project to act as (defaults to the bound repository)")
|
|
186
186
|
.option("--json", "emit the versioned machine envelope")
|
|
187
187
|
.action(async (options) => runDocsList(options, program.opts()));
|
|
188
188
|
docs
|
|
189
189
|
.command("show <slug-or-id>")
|
|
190
190
|
.description("Show a Document: a bounded inline body or its content URL")
|
|
191
|
-
.option("--
|
|
191
|
+
.option("--project <id>", "the Project to act as (defaults to the bound repository)")
|
|
192
192
|
.option("--json", "emit the versioned machine envelope")
|
|
193
193
|
.action(async (target, options) => runDocsShow(target, options, program.opts()));
|
|
194
194
|
docs
|
|
195
195
|
.command("status")
|
|
196
196
|
.description("Report per-path local/cloud Document convergence for the bound repository")
|
|
197
|
-
.option("--
|
|
197
|
+
.option("--project <id>", "the Project to act as (defaults to the bound repository)")
|
|
198
198
|
.option("--json", "emit the versioned machine envelope")
|
|
199
199
|
.action(async (options) => runDocsStatus(options, program.opts()));
|
|
200
|
-
const handoff = program.command("handoff").description("Work with a
|
|
200
|
+
const handoff = program.command("handoff").description("Work with a Project's Handoffs");
|
|
201
201
|
handoff
|
|
202
202
|
.command("list")
|
|
203
|
-
.description("List the
|
|
204
|
-
.option("--
|
|
203
|
+
.description("List the Project's Handoffs")
|
|
204
|
+
.option("--project <id>", "the Project to act as (defaults to the bound repository)")
|
|
205
205
|
.option("--json", "emit the versioned machine envelope")
|
|
206
206
|
.action(async (options) => runHandoffList(options, program.opts()));
|
|
207
207
|
handoff
|
|
208
208
|
.command("show <id>")
|
|
209
209
|
.description("Show a Handoff (add --targets for the binding-scoped per-target manifest)")
|
|
210
|
-
.option("--
|
|
210
|
+
.option("--project <id>", "the Project to act as (defaults to the bound repository)")
|
|
211
211
|
.option("--targets", "list each target's path, operation, and base semantics")
|
|
212
212
|
.option("--json", "emit the versioned machine envelope")
|
|
213
213
|
.action(async (id, options) => runHandoffShow(id, options, program.opts()));
|
|
@@ -223,17 +223,17 @@ sessions
|
|
|
223
223
|
.option("--scan <json>", "the scan payload as JSON, or @<path> to read it from a file")
|
|
224
224
|
.option("--json", "emit the versioned machine envelope")
|
|
225
225
|
.action(async (options) => runSessionsRecord(options, program.opts()));
|
|
226
|
-
const toolkit = program.command("toolkit").description("Inspect and reconcile the
|
|
226
|
+
const toolkit = program.command("toolkit").description("Inspect and reconcile the Project's toolkit pin");
|
|
227
227
|
toolkit
|
|
228
228
|
.command("status")
|
|
229
229
|
.description("Compare the cloud pin against .nustack/workspace.json and nustack.service.json (F-026)")
|
|
230
|
-
.option("--
|
|
230
|
+
.option("--project <id>", "the Project to act as (defaults to the bound repository)")
|
|
231
231
|
.option("--json", "emit the versioned machine envelope")
|
|
232
232
|
.action(async (options) => runToolkitStatus(options, program.opts()));
|
|
233
233
|
toolkit
|
|
234
234
|
.command("reconcile")
|
|
235
235
|
.description("Preview and apply the deterministic toolkit repair (no User content is touched)")
|
|
236
|
-
.option("--
|
|
236
|
+
.option("--project <id>", "the Project to act as (defaults to the bound repository)")
|
|
237
237
|
.option("--yes", "apply the deterministic preview without prompting")
|
|
238
238
|
.option("--json", "emit the versioned machine envelope")
|
|
239
239
|
.action(async (options) => runToolkitReconcile(options, program.opts()));
|
|
@@ -253,14 +253,14 @@ agentHook
|
|
|
253
253
|
.action(async () => runAgentHookRemove());
|
|
254
254
|
function describeError(error) {
|
|
255
255
|
const directCode = error?.code;
|
|
256
|
-
if (directCode === "
|
|
257
|
-
return "no
|
|
256
|
+
if (directCode === "PROJECT_CONTEXT_MISSING") {
|
|
257
|
+
return "no Project in context — run inside a bound repository (`.nustack/workspace.json`) or pass `--project <id>`.";
|
|
258
258
|
}
|
|
259
259
|
if (directCode === "ORGANIZATION_CONTEXT_MISSING") {
|
|
260
260
|
return "no Organization in context — run `nustack organization use <id>` or pass `--organization <id>`.";
|
|
261
261
|
}
|
|
262
262
|
if (directCode === "TOOLKIT_INCOMPATIBLE") {
|
|
263
|
-
return "the installed toolkit is incompatible with the
|
|
263
|
+
return "the installed toolkit is incompatible with the Project's cloud pin — run `nustack toolkit reconcile`.";
|
|
264
264
|
}
|
|
265
265
|
const cause = error?.cause;
|
|
266
266
|
const code = cause?.code;
|
package/dist/lib/etna/profile.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getProject } from "../nustack_client.js";
|
|
2
2
|
export class ToolkitProfileUnavailableError extends Error {
|
|
3
|
-
|
|
4
|
-
constructor(
|
|
5
|
-
super("The
|
|
3
|
+
projectId;
|
|
4
|
+
constructor(projectId) {
|
|
5
|
+
super("The Project summary did not project a Toolkit Profile pin — the owning server plan must expose it on GET /api/v2/projects/{projectId}.");
|
|
6
6
|
this.name = "ToolkitProfileUnavailableError";
|
|
7
|
-
this.
|
|
7
|
+
this.projectId = projectId;
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
-
export async function fetchToolkitProfile(baseUrl, accessToken,
|
|
11
|
-
const
|
|
12
|
-
const toolkit =
|
|
10
|
+
export async function fetchToolkitProfile(baseUrl, accessToken, projectId) {
|
|
11
|
+
const project = await getProject(baseUrl, accessToken, projectId);
|
|
12
|
+
const toolkit = project.toolkit;
|
|
13
13
|
if (!toolkit || typeof toolkit.name !== "string" || typeof toolkit.version !== "string") {
|
|
14
|
-
throw new ToolkitProfileUnavailableError(
|
|
14
|
+
throw new ToolkitProfileUnavailableError(projectId);
|
|
15
15
|
}
|
|
16
16
|
return {
|
|
17
17
|
name: toolkit.name,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { writeProjectManifests, readProjectManifests, diagnoseManifests, } from "../onboarding/manifest_writer.js";
|
|
2
2
|
import { serializeProfileForBinding } from "./profile.js";
|
|
3
3
|
import { updateBindingEtnaProfile } from "../local_store/bindings.js";
|
|
4
4
|
export function parseRecordedToolkit(binding) {
|
|
@@ -35,16 +35,16 @@ export function toolkitCompatibility(binding, cloud) {
|
|
|
35
35
|
return { compatible: true, blocksWrites: false, reason: null };
|
|
36
36
|
}
|
|
37
37
|
export async function buildReconcilePlan(root, binding, cloud) {
|
|
38
|
-
const manifests = await
|
|
38
|
+
const manifests = await readProjectManifests(root);
|
|
39
39
|
const cloudId = { name: cloud.name, version: cloud.version };
|
|
40
|
-
const diagnosis = diagnoseManifests(cloudId, manifests.
|
|
40
|
+
const diagnosis = diagnoseManifests(cloudId, manifests.projectManifest?.toolkit ?? null, manifests.serviceToolkit);
|
|
41
41
|
const changes = [];
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
pushIf(changes, ".nustack/workspace.json", "toolkit.name",
|
|
45
|
-
pushIf(changes, ".nustack/workspace.json", "toolkit.version",
|
|
46
|
-
pushIf(changes, ".nustack/workspace.json", "toolkit.rendererVersion",
|
|
47
|
-
pushIf(changes, ".nustack/workspace.json", "toolkit.placementMapVersion",
|
|
42
|
+
const project = manifests.projectManifest?.toolkit;
|
|
43
|
+
if (project) {
|
|
44
|
+
pushIf(changes, ".nustack/workspace.json", "toolkit.name", project.name, cloud.name);
|
|
45
|
+
pushIf(changes, ".nustack/workspace.json", "toolkit.version", project.version, cloud.version);
|
|
46
|
+
pushIf(changes, ".nustack/workspace.json", "toolkit.rendererVersion", project.rendererVersion, cloud.rendererVersion);
|
|
47
|
+
pushIf(changes, ".nustack/workspace.json", "toolkit.placementMapVersion", project.placementMapVersion, cloud.placementMapVersion);
|
|
48
48
|
}
|
|
49
49
|
const svc = manifests.serviceToolkit;
|
|
50
50
|
if (svc) {
|
|
@@ -63,8 +63,8 @@ function pushIf(changes, file, field, from, to) {
|
|
|
63
63
|
changes.push({ file, field, from, to });
|
|
64
64
|
}
|
|
65
65
|
export async function applyReconcile(store, bindingId, root, identity, cloud) {
|
|
66
|
-
await
|
|
67
|
-
|
|
66
|
+
await writeProjectManifests(root, {
|
|
67
|
+
projectId: identity.projectId,
|
|
68
68
|
organizationId: identity.organizationId,
|
|
69
69
|
toolkit: {
|
|
70
70
|
name: cloud.name,
|
|
@@ -10,7 +10,7 @@ const DEFAULT_HEARTBEAT_MS = 30_000;
|
|
|
10
10
|
export function observeBinding(binding) {
|
|
11
11
|
const base = {
|
|
12
12
|
bindingId: binding.id,
|
|
13
|
-
|
|
13
|
+
projectId: binding.projectId,
|
|
14
14
|
boundBranch: binding.boundBranch,
|
|
15
15
|
status: binding.status,
|
|
16
16
|
resolved: false,
|
|
@@ -18,7 +18,7 @@ export function observeBinding(binding) {
|
|
|
18
18
|
operationState: "unknown",
|
|
19
19
|
headCommitId: null,
|
|
20
20
|
lastProcessedCommitId: binding.lastProcessedCommitId,
|
|
21
|
-
|
|
21
|
+
lastAppliedProjectSequence: binding.lastAppliedProjectSequence,
|
|
22
22
|
pendingCommitCount: 0,
|
|
23
23
|
};
|
|
24
24
|
let repo;
|
|
@@ -209,12 +209,12 @@ export function createSyncEngine(options) {
|
|
|
209
209
|
safeEmit({
|
|
210
210
|
event: "binding_status",
|
|
211
211
|
bindingId: observation.bindingId,
|
|
212
|
-
|
|
212
|
+
projectId: observation.projectId,
|
|
213
213
|
boundBranch: observation.boundBranch,
|
|
214
214
|
status: observation.status,
|
|
215
215
|
headCommitId: observation.headCommitId,
|
|
216
216
|
lastProcessedCommitId: observation.lastProcessedCommitId,
|
|
217
|
-
|
|
217
|
+
lastAppliedProjectSequence: observation.lastAppliedProjectSequence,
|
|
218
218
|
pendingCommitCount: observation.pendingCommitCount,
|
|
219
219
|
});
|
|
220
220
|
}
|
|
@@ -11,12 +11,12 @@ import { readTargetState } from "./content_decision.js";
|
|
|
11
11
|
import { computeStateFingerprint } from "./state_fingerprint.js";
|
|
12
12
|
import { persistRecoveryBundle } from "../recovery/store.js";
|
|
13
13
|
export const NUSTACK_SYNC_IDENTITY = { name: "NuStack Sync", email: "sync@nustack.local" };
|
|
14
|
-
export function buildCommitMessage(handoffId,
|
|
14
|
+
export function buildCommitMessage(handoffId, projectId, approvedByUserId) {
|
|
15
15
|
const lines = [
|
|
16
16
|
`nustack(sync): apply cloud handoff ${handoffId.slice(0, 12)}`,
|
|
17
17
|
"",
|
|
18
18
|
`NuStack-Handoff: ${handoffId}`,
|
|
19
|
-
`NuStack-
|
|
19
|
+
`NuStack-Project: ${projectId}`,
|
|
20
20
|
"NuStack-Origin: cloud",
|
|
21
21
|
];
|
|
22
22
|
if (approvedByUserId)
|
|
@@ -186,7 +186,7 @@ export async function applyCommit(input) {
|
|
|
186
186
|
if (target.operation === "upsert" && target.blobPath)
|
|
187
187
|
bytesByPath.set(target.path, readFileSync(target.blobPath));
|
|
188
188
|
}
|
|
189
|
-
const message = buildCommitMessage(handoffId, manifest.
|
|
189
|
+
const message = buildCommitMessage(handoffId, manifest.projectId, approved ? (input.approvedByUserId ?? null) : null);
|
|
190
190
|
const built = buildCommit(repo, oldHead, targets, bytesByPath, message, {
|
|
191
191
|
authorName: NUSTACK_SYNC_IDENTITY.name,
|
|
192
192
|
authorEmail: NUSTACK_SYNC_IDENTITY.email,
|
|
@@ -12,7 +12,7 @@ export const APPROVAL_RECOVERY_NOTE = "Approving overwrites the listed paths. Gi
|
|
|
12
12
|
export function toDownloadBinding(binding) {
|
|
13
13
|
return {
|
|
14
14
|
id: binding.id,
|
|
15
|
-
|
|
15
|
+
projectId: binding.projectId,
|
|
16
16
|
boundBranch: binding.boundBranch,
|
|
17
17
|
repositoryRoot: binding.repositoryRoot,
|
|
18
18
|
etnaProfileJson: binding.etnaProfileJson,
|
|
@@ -27,7 +27,7 @@ function recomputeManifestHash(manifest) {
|
|
|
27
27
|
const core = {
|
|
28
28
|
schemaVersion: 1,
|
|
29
29
|
handoffId: manifest.handoffId,
|
|
30
|
-
|
|
30
|
+
projectId: manifest.projectId,
|
|
31
31
|
documentSetVersionId: manifest.documentSetVersionId,
|
|
32
32
|
toolkit: {
|
|
33
33
|
name: manifest.toolkit.name,
|
|
@@ -62,7 +62,7 @@ function toolkitTechnicalWait(binding, manifest) {
|
|
|
62
62
|
return "harness_incompatible";
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
|
-
function
|
|
65
|
+
function crossProjectMarker(body, projectId) {
|
|
66
66
|
if (!body.startsWith("---\n"))
|
|
67
67
|
return false;
|
|
68
68
|
const secondLineEnd = body.indexOf("\n", 4);
|
|
@@ -71,8 +71,8 @@ function crossWorkspaceMarker(body, workspaceId) {
|
|
|
71
71
|
return false;
|
|
72
72
|
const fence = body.indexOf("\n---", 4);
|
|
73
73
|
const block = body.slice(0, fence === -1 ? body.length : fence);
|
|
74
|
-
const match = /\n\s*
|
|
75
|
-
return match !== null && match[1] !==
|
|
74
|
+
const match = /\n\s*project_id:\s*(\S+)/.exec(block);
|
|
75
|
+
return match !== null && match[1] !== projectId;
|
|
76
76
|
}
|
|
77
77
|
function isUnsupportedMedia(bytes) {
|
|
78
78
|
for (const byte of bytes)
|
|
@@ -136,8 +136,8 @@ export async function downloadHandoff(deps, binding, handoffId) {
|
|
|
136
136
|
const downloadContent = deps.downloadContent ?? downloadTargetContent;
|
|
137
137
|
const fetchWhitelist = deps.fetchWhitelist ?? fetchSyncWhitelist;
|
|
138
138
|
const manifest = await fetchManifest(deps.baseUrl, deps.accessToken, { handoffId, bindingId: binding.id });
|
|
139
|
-
if (manifest.
|
|
140
|
-
return reject("
|
|
139
|
+
if (manifest.projectId !== binding.projectId)
|
|
140
|
+
return reject("project_mismatch", "manifest Project does not match the binding");
|
|
141
141
|
if (recomputeManifestHash(manifest) !== manifest.manifestSha256) {
|
|
142
142
|
return reject("manifest_hash", "manifest hash does not verify");
|
|
143
143
|
}
|
|
@@ -207,8 +207,8 @@ export async function downloadHandoff(deps, binding, handoffId) {
|
|
|
207
207
|
const actualSha256 = sha256(bytes);
|
|
208
208
|
if (actualSha256 !== target.targetSha256)
|
|
209
209
|
return reject("hash_mismatch", "downloaded body hash does not match the manifest");
|
|
210
|
-
if (
|
|
211
|
-
return reject("
|
|
210
|
+
if (crossProjectMarker(new TextDecoder().decode(bytes), binding.projectId)) {
|
|
211
|
+
return reject("cross_project_marker", "target body declares a foreign Project id");
|
|
212
212
|
}
|
|
213
213
|
const blobPath = blobCachePath(actualSha256);
|
|
214
214
|
await mkdirSecure(path.dirname(blobPath));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getBindingById, updateBindingCursor } from "../../local_store/bindings.js";
|
|
2
2
|
import { hasCloudOriginForHandoff } from "../../local_store/commit_origins.js";
|
|
3
|
-
import {
|
|
3
|
+
import { cacheProjectEvents, listCachedProjectEvents, pruneProjectEvents, } from "../../local_store/project_events_cache.js";
|
|
4
4
|
import { getApplication, upsertApplication, transitionApplicationState, listApplicationsForBinding, } from "../../local_store/handoff_applications.js";
|
|
5
5
|
import { fetchBindingEvents, acknowledgeBindingCursor } from "../../nustack_client.js";
|
|
6
6
|
const PENDING_STATES = new Set([
|
|
@@ -29,7 +29,7 @@ function supersedeOlderApplications(store, bindingId, newerHandoffId) {
|
|
|
29
29
|
return flipped;
|
|
30
30
|
}
|
|
31
31
|
function handoffIdOf(event) {
|
|
32
|
-
return event.type === "version.created" && event.entity.kind === "
|
|
32
|
+
return event.type === "version.created" && event.entity.kind === "project_document_version" ? event.entity.id : null;
|
|
33
33
|
}
|
|
34
34
|
export async function syncBindingEvents(store, deps, bindingId) {
|
|
35
35
|
const binding = getBindingById(store, bindingId);
|
|
@@ -40,9 +40,9 @@ export async function syncBindingEvents(store, deps, bindingId) {
|
|
|
40
40
|
createdHandoffs: [],
|
|
41
41
|
supersededHandoffs: [],
|
|
42
42
|
acknowledgedOwnOrigin: [],
|
|
43
|
-
highestSequence: binding.
|
|
43
|
+
highestSequence: binding.lastAppliedProjectSequence,
|
|
44
44
|
};
|
|
45
|
-
let after = binding.
|
|
45
|
+
let after = binding.lastAppliedProjectSequence;
|
|
46
46
|
for (;;) {
|
|
47
47
|
const events = await fetchEvents(deps.baseUrl, deps.accessToken, {
|
|
48
48
|
bindingId,
|
|
@@ -51,8 +51,8 @@ export async function syncBindingEvents(store, deps, bindingId) {
|
|
|
51
51
|
});
|
|
52
52
|
if (events.length === 0)
|
|
53
53
|
break;
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
cacheProjectEvents(store, bindingId, events.map((event) => ({
|
|
55
|
+
projectSequence: event.sequence,
|
|
56
56
|
eventType: event.type,
|
|
57
57
|
payloadJson: JSON.stringify({ entity: event.entity, payload: event.payload }),
|
|
58
58
|
})));
|
|
@@ -81,7 +81,7 @@ export async function syncBindingEvents(store, deps, bindingId) {
|
|
|
81
81
|
if (events.length < EVENTS_PAGE_LIMIT)
|
|
82
82
|
break;
|
|
83
83
|
}
|
|
84
|
-
|
|
84
|
+
pruneProjectEvents(store, bindingId);
|
|
85
85
|
return result;
|
|
86
86
|
}
|
|
87
87
|
function cachedHandoffId(eventType, payloadJson) {
|
|
@@ -100,9 +100,9 @@ export async function advanceContiguousCursor(store, deps, bindingId) {
|
|
|
100
100
|
if (!binding)
|
|
101
101
|
throw new Error(`No binding "${bindingId}" on this machine.`);
|
|
102
102
|
const acknowledgeCursor = deps.acknowledgeCursor ?? acknowledgeBindingCursor;
|
|
103
|
-
let cursor = binding.
|
|
104
|
-
for (const event of
|
|
105
|
-
if (event.
|
|
103
|
+
let cursor = binding.lastAppliedProjectSequence;
|
|
104
|
+
for (const event of listCachedProjectEvents(store, bindingId)) {
|
|
105
|
+
if (event.projectSequence <= cursor)
|
|
106
106
|
continue;
|
|
107
107
|
const handoffId = cachedHandoffId(event.eventType, event.payloadJson);
|
|
108
108
|
if (handoffId !== null) {
|
|
@@ -110,10 +110,10 @@ export async function advanceContiguousCursor(store, deps, bindingId) {
|
|
|
110
110
|
if (!application || PENDING_STATES.has(application.state))
|
|
111
111
|
break;
|
|
112
112
|
}
|
|
113
|
-
cursor = event.
|
|
113
|
+
cursor = event.projectSequence;
|
|
114
114
|
}
|
|
115
|
-
if (cursor > binding.
|
|
116
|
-
updateBindingCursor(store, bindingId, {
|
|
115
|
+
if (cursor > binding.lastAppliedProjectSequence) {
|
|
116
|
+
updateBindingCursor(store, bindingId, { lastAppliedProjectSequence: cursor });
|
|
117
117
|
try {
|
|
118
118
|
await acknowledgeCursor(deps.baseUrl, deps.accessToken, { bindingId, lastAppliedSequence: cursor });
|
|
119
119
|
}
|
|
@@ -7,7 +7,7 @@ export function renderCommitNotification(application, changedPaths) {
|
|
|
7
7
|
lines.push(` commit: ${application.generatedCommitId.slice(0, 12)}`);
|
|
8
8
|
if (changedPaths.length > 0)
|
|
9
9
|
lines.push(` changed: ${changedPaths.join(", ")}`);
|
|
10
|
-
lines.push(` View: nustack
|
|
10
|
+
lines.push(` View: nustack project sync pending Undo: nustack project sync undo ${application.handoffId}`);
|
|
11
11
|
if (application.recoveryBundleId) {
|
|
12
12
|
lines.push(` Recovery bundle ${application.recoveryBundleId} holds your pre-approval content until it expires.`);
|
|
13
13
|
}
|
|
@@ -43,11 +43,11 @@ function readBlob(repo, blobId) {
|
|
|
43
43
|
export function isCommitTreeReadable(repo, commitId) {
|
|
44
44
|
return runGit(repo.repositoryRoot, ["cat-file", "-e", `${commitId}^{tree}`], { allowFailure: true }).exitCode === 0;
|
|
45
45
|
}
|
|
46
|
-
export function extractCommitDelta(repo, commitId, whitelist,
|
|
46
|
+
export function extractCommitDelta(repo, commitId, whitelist, projectId, direction = "local_to_cloud") {
|
|
47
47
|
const parentCommitId = readRefCommit(repo, `${commitId}^1`);
|
|
48
|
-
return computeTreeDelta(repo, parentCommitId, commitId, whitelist,
|
|
48
|
+
return computeTreeDelta(repo, parentCommitId, commitId, whitelist, projectId, direction);
|
|
49
49
|
}
|
|
50
|
-
export function computeTreeDelta(repo, baseCommitId, targetCommitId, whitelist,
|
|
50
|
+
export function computeTreeDelta(repo, baseCommitId, targetCommitId, whitelist, projectId, direction = "local_to_cloud") {
|
|
51
51
|
const diffArgs = ["diff-tree", "-r", "-z", "--no-renames", "--no-commit-id"];
|
|
52
52
|
const args = baseCommitId === null ? [...diffArgs, "--root", targetCommitId] : [...diffArgs, baseCommitId, targetCommitId];
|
|
53
53
|
const { stdout } = runGit(repo.repositoryRoot, args);
|
|
@@ -81,7 +81,7 @@ export function computeTreeDelta(repo, baseCommitId, targetCommitId, whitelist,
|
|
|
81
81
|
bytes: blob.bytes,
|
|
82
82
|
byteSize: blob.byteSize,
|
|
83
83
|
entry: match.entry,
|
|
84
|
-
|
|
84
|
+
projectId,
|
|
85
85
|
});
|
|
86
86
|
if (!validation.ok)
|
|
87
87
|
return { ok: false, code: validation.code, path };
|