@nurix/nustack 0.10.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/dist/commands/add.js +130 -0
- package/dist/commands/agent.js +100 -0
- package/dist/commands/bootstrap.js +54 -0
- package/dist/commands/describe.js +34 -0
- package/dist/commands/device.js +159 -0
- package/dist/commands/docs.js +119 -0
- package/dist/commands/doctor.js +284 -0
- package/dist/commands/graph.js +34 -0
- package/dist/commands/handoff.js +108 -0
- package/dist/commands/init.js +280 -0
- package/dist/commands/lane.js +12 -0
- package/dist/commands/list.js +20 -0
- package/dist/commands/login.js +134 -0
- package/dist/commands/logout.js +44 -0
- package/dist/commands/open.js +54 -0
- package/dist/commands/organization.js +114 -0
- package/dist/commands/review.js +152 -0
- package/dist/commands/search.js +57 -0
- package/dist/commands/sessions.js +62 -0
- package/dist/commands/sync.js +85 -0
- package/dist/commands/telemetry.js +112 -0
- package/dist/commands/toolkit.js +167 -0
- package/dist/commands/validate.js +67 -0
- package/dist/commands/workspace-sync.js +645 -0
- package/dist/commands/workspace.js +479 -0
- package/dist/index.js +357 -0
- package/dist/lib/agent/claudeHooks.js +77 -0
- package/dist/lib/auth_grant.js +398 -0
- package/dist/lib/auth_recovery.js +49 -0
- package/dist/lib/browser_opener.js +24 -0
- package/dist/lib/device_identity.js +28 -0
- package/dist/lib/device_keychain.js +101 -0
- package/dist/lib/env_file.js +75 -0
- package/dist/lib/etna/manager.js +43 -0
- package/dist/lib/etna/profile.js +41 -0
- package/dist/lib/etna/reconcile.js +77 -0
- package/dist/lib/filebeat_installer.js +246 -0
- package/dist/lib/git-sync/core/commit_range.js +37 -0
- package/dist/lib/git-sync/core/engine.js +263 -0
- package/dist/lib/git-sync/core/git_runner.js +137 -0
- package/dist/lib/git-sync/core/operation_state.js +22 -0
- package/dist/lib/git-sync/core/path_normalizer.js +98 -0
- package/dist/lib/git-sync/core/reconciliation_lock.js +103 -0
- package/dist/lib/git-sync/core/repository.js +49 -0
- package/dist/lib/git-sync/core/watch_stream.js +86 -0
- package/dist/lib/git-sync/core/watcher.js +61 -0
- package/dist/lib/git-sync/inbound/applier.js +131 -0
- package/dist/lib/git-sync/inbound/apply_commit.js +225 -0
- package/dist/lib/git-sync/inbound/approval.js +117 -0
- package/dist/lib/git-sync/inbound/content_decision.js +148 -0
- package/dist/lib/git-sync/inbound/download.js +223 -0
- package/dist/lib/git-sync/inbound/event_sync.js +133 -0
- package/dist/lib/git-sync/inbound/index.js +10 -0
- package/dist/lib/git-sync/inbound/notification.js +39 -0
- package/dist/lib/git-sync/inbound/state_fingerprint.js +32 -0
- package/dist/lib/git-sync/inbound/technical_wait.js +39 -0
- package/dist/lib/git-sync/inbound/undo.js +198 -0
- package/dist/lib/git-sync/outbound/extraction.js +107 -0
- package/dist/lib/git-sync/outbound/history_decision.js +112 -0
- package/dist/lib/git-sync/outbound/index.js +26 -0
- package/dist/lib/git-sync/outbound/manifest_builder.js +58 -0
- package/dist/lib/git-sync/outbound/publisher.js +51 -0
- package/dist/lib/git-sync/outbound/queue.js +123 -0
- package/dist/lib/git-sync/outbound/state.js +12 -0
- package/dist/lib/git-sync/outbound/upload_client.js +141 -0
- package/dist/lib/git-sync/recovery/bundle.js +56 -0
- package/dist/lib/git-sync/recovery/crypto.js +114 -0
- package/dist/lib/git-sync/recovery/index.js +3 -0
- package/dist/lib/git-sync/recovery/store.js +55 -0
- package/dist/lib/git-sync/whitelist/index.js +4 -0
- package/dist/lib/git-sync/whitelist/matcher.js +62 -0
- package/dist/lib/git-sync/whitelist/store.js +99 -0
- package/dist/lib/git-sync/whitelist/types.js +46 -0
- package/dist/lib/git-sync/whitelist/validation.js +60 -0
- package/dist/lib/graph/answer.js +171 -0
- package/dist/lib/graph/freshness.js +29 -0
- package/dist/lib/graph/library.js +19 -0
- package/dist/lib/guards.js +106 -0
- package/dist/lib/interactive.js +12 -0
- package/dist/lib/json_output.js +8 -0
- package/dist/lib/lanes/claudeAdapter.js +135 -0
- package/dist/lib/lanes/frames.js +73 -0
- package/dist/lib/lanes/supervisor.js +247 -0
- package/dist/lib/lanes/worktree.js +33 -0
- package/dist/lib/local_store/accepted_uploads.js +49 -0
- package/dist/lib/local_store/bindings.js +163 -0
- package/dist/lib/local_store/commit_origins.js +25 -0
- package/dist/lib/local_store/database.js +71 -0
- package/dist/lib/local_store/device_state.js +72 -0
- package/dist/lib/local_store/filebeat_state.js +133 -0
- package/dist/lib/local_store/handoff_applications.js +166 -0
- package/dist/lib/local_store/history_decisions.js +48 -0
- package/dist/lib/local_store/index.js +14 -0
- package/dist/lib/local_store/migrations.js +240 -0
- package/dist/lib/local_store/paths.js +24 -0
- package/dist/lib/local_store/pending_uploads.js +76 -0
- package/dist/lib/local_store/recovery_bundles.js +66 -0
- package/dist/lib/local_store/sync_vocabulary.js +33 -0
- package/dist/lib/local_store/workspace_events_cache.js +54 -0
- package/dist/lib/loopback.js +78 -0
- package/dist/lib/manifest.js +83 -0
- package/dist/lib/nustack_client.js +547 -0
- package/dist/lib/nustack_store.js +17 -0
- package/dist/lib/onboarding/attach_existing_git.js +65 -0
- package/dist/lib/onboarding/binding.js +91 -0
- package/dist/lib/onboarding/folder_state.js +99 -0
- package/dist/lib/onboarding/git_init.js +10 -0
- package/dist/lib/onboarding/handoff_apply.js +68 -0
- package/dist/lib/onboarding/init_fresh_folder.js +100 -0
- package/dist/lib/onboarding/manifest_writer.js +161 -0
- package/dist/lib/onboarding/web_first_attach.js +133 -0
- package/dist/lib/package_manager.js +23 -0
- package/dist/lib/review/client.js +66 -0
- package/dist/lib/schema_validator.js +24 -0
- package/dist/lib/secure_file.js +10 -0
- package/dist/lib/session_telemetry.js +263 -0
- package/dist/lib/sessions/client.js +48 -0
- package/dist/lib/sessions/outsideSessions.js +54 -0
- package/dist/lib/sse_framing.js +48 -0
- package/dist/lib/telemetry/config_renderer.js +175 -0
- package/dist/lib/telemetry/health_report.js +173 -0
- package/dist/lib/telemetry/provider_paths.js +38 -0
- package/dist/lib/telemetry/redaction_policy.js +90 -0
- package/dist/lib/telemetry/reload.js +84 -0
- package/dist/lib/workspace_context.js +121 -0
- package/dist/spec_schema.json +422 -0
- package/dist/wire/auth.js +1 -0
- package/dist/wire/sync-paths.js +25 -0
- package/dist/wire/telemetry.js +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createBinding } from "../nustack_client.js";
|
|
3
|
+
import { getBindingById, getBindingByRepositoryRoot, insertBinding, updateBindingCursor, updateBindingStatus, } from "../local_store/bindings.js";
|
|
4
|
+
import { ensureBindingTelemetry } from "../session_telemetry.js";
|
|
5
|
+
import { readHeadCommit } from "../git-sync/core/repository.js";
|
|
6
|
+
import { writeWorkspaceManifests } from "./manifest_writer.js";
|
|
7
|
+
import { installToolkit } from "../etna/manager.js";
|
|
8
|
+
import { serializeProfileForBinding } from "../etna/profile.js";
|
|
9
|
+
import { SYNC_PROTOCOL_VERSION } from "../git-sync/whitelist/types.js";
|
|
10
|
+
export function computeRepositoryFingerprint(repository) {
|
|
11
|
+
return createHash("sha256").update(repository.commonDir).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
function bindingIdempotencyKey(workspaceId, deviceRef, fingerprint) {
|
|
14
|
+
return createHash("sha256").update(`binding:${workspaceId}:${deviceRef}:${fingerprint}`).digest("hex");
|
|
15
|
+
}
|
|
16
|
+
export async function establishBinding(input) {
|
|
17
|
+
const { store, repository } = input;
|
|
18
|
+
const fingerprint = computeRepositoryFingerprint(repository);
|
|
19
|
+
const existing = getBindingByRepositoryRoot(store, repository.repositoryRoot);
|
|
20
|
+
if (existing && existing.workspaceId === input.workspaceId) {
|
|
21
|
+
if (input.baselineCommitId && existing.lastProcessedCommitId === null) {
|
|
22
|
+
updateBindingCursor(store, existing.id, { lastProcessedCommitId: input.baselineCommitId });
|
|
23
|
+
}
|
|
24
|
+
if (existing.status === "creating")
|
|
25
|
+
updateBindingStatus(store, existing.id, "active");
|
|
26
|
+
await maybeRunTelemetry(input, existing);
|
|
27
|
+
return { binding: getBindingById(store, existing.id), created: false };
|
|
28
|
+
}
|
|
29
|
+
const cloudBinding = await createBinding(input.baseUrl, input.accessToken, input.workspaceId, { repositoryFingerprint: fingerprint, boundBranch: input.boundBranch, protocolVersion: SYNC_PROTOCOL_VERSION }, bindingIdempotencyKey(input.workspaceId, input.deviceRef, fingerprint));
|
|
30
|
+
const row = getBindingById(store, cloudBinding.id) ??
|
|
31
|
+
insertBinding(store, {
|
|
32
|
+
id: cloudBinding.id,
|
|
33
|
+
workspaceId: input.workspaceId,
|
|
34
|
+
organizationId: input.organizationId,
|
|
35
|
+
repositoryRoot: repository.repositoryRoot,
|
|
36
|
+
gitDir: repository.gitDir,
|
|
37
|
+
gitCommonDir: repository.commonDir,
|
|
38
|
+
repositoryFingerprint: fingerprint,
|
|
39
|
+
boundBranch: input.boundBranch,
|
|
40
|
+
status: "active",
|
|
41
|
+
lastProcessedCommitId: input.baselineCommitId ?? null,
|
|
42
|
+
etnaProfileJson: input.etnaProfileJson ?? null,
|
|
43
|
+
});
|
|
44
|
+
await maybeRunTelemetry(input, row);
|
|
45
|
+
return { binding: getBindingById(store, cloudBinding.id), created: true };
|
|
46
|
+
}
|
|
47
|
+
async function maybeRunTelemetry(input, binding) {
|
|
48
|
+
if (input.runTelemetry === false)
|
|
49
|
+
return;
|
|
50
|
+
await ensureBindingTelemetry({ baseUrl: input.baseUrl, store: input.store, binding }).catch(() => { });
|
|
51
|
+
}
|
|
52
|
+
export async function runRepositoryOnboarding(input) {
|
|
53
|
+
const baselineCommitId = input.baselineCommitId !== undefined ? input.baselineCommitId : readHeadCommit(input.repository);
|
|
54
|
+
const established = await establishBinding({
|
|
55
|
+
store: input.store,
|
|
56
|
+
baseUrl: input.baseUrl,
|
|
57
|
+
accessToken: input.accessToken,
|
|
58
|
+
workspaceId: input.workspaceId,
|
|
59
|
+
organizationId: input.organizationId,
|
|
60
|
+
repository: input.repository,
|
|
61
|
+
boundBranch: input.boundBranch,
|
|
62
|
+
deviceRef: input.deviceRef,
|
|
63
|
+
baselineCommitId,
|
|
64
|
+
etnaProfileJson: serializeProfileForBinding(input.profile),
|
|
65
|
+
runTelemetry: input.runTelemetry,
|
|
66
|
+
});
|
|
67
|
+
const install = await installToolkit(input.store, established.binding.id, input.profile, input.repository.repositoryRoot, input.toolkitDeps ?? {});
|
|
68
|
+
const workspaceManifest = await writeWorkspaceManifests(input.repository.repositoryRoot, {
|
|
69
|
+
workspaceId: input.workspaceId,
|
|
70
|
+
organizationId: input.organizationId,
|
|
71
|
+
toolkit: {
|
|
72
|
+
name: input.profile.name,
|
|
73
|
+
version: input.profile.version,
|
|
74
|
+
rendererVersion: input.profile.rendererVersion,
|
|
75
|
+
placementMapVersion: input.profile.placementMapVersion,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
flow: input.flow,
|
|
80
|
+
workspaceId: input.workspaceId,
|
|
81
|
+
organizationId: input.organizationId,
|
|
82
|
+
bindingId: established.binding.id,
|
|
83
|
+
boundBranch: input.boundBranch,
|
|
84
|
+
baselineCommitId,
|
|
85
|
+
toolkit: { name: input.profile.name, version: input.profile.version },
|
|
86
|
+
reused: !established.created,
|
|
87
|
+
install,
|
|
88
|
+
workspaceManifest,
|
|
89
|
+
noPush: true,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveRepository, readSymbolicBranch } from "../git-sync/core/repository.js";
|
|
4
|
+
import { readGitOperationState } from "../git-sync/core/operation_state.js";
|
|
5
|
+
import { NotARepositoryError } from "../git-sync/core/git_runner.js";
|
|
6
|
+
import { legacyAppJsonPath, workspaceJsonPath } from "../workspace_context.js";
|
|
7
|
+
const SAFE_OS_METADATA = new Set([".DS_Store", "Thumbs.db", "desktop.ini", ".localized"]);
|
|
8
|
+
function significantEntries(dir) {
|
|
9
|
+
let names;
|
|
10
|
+
try {
|
|
11
|
+
names = readdirSync(dir);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
return names.filter((name) => name !== ".git" && !SAFE_OS_METADATA.has(name));
|
|
17
|
+
}
|
|
18
|
+
function readMarkerWorkspaceId(root) {
|
|
19
|
+
const v2 = read(workspaceJsonPath(root));
|
|
20
|
+
if (v2 && v2.schemaVersion === 2) {
|
|
21
|
+
const id = trimmedField(v2.workspaceId);
|
|
22
|
+
if (id)
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
const v1 = read(legacyAppJsonPath(root));
|
|
26
|
+
return v1 ? trimmedField(v1.appId) : null;
|
|
27
|
+
}
|
|
28
|
+
function read(filePath) {
|
|
29
|
+
if (!existsSync(filePath))
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function trimmedField(value) {
|
|
39
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
|
|
40
|
+
}
|
|
41
|
+
function isNestedUnderWorkspace(root) {
|
|
42
|
+
let dir = path.dirname(root);
|
|
43
|
+
while (dir && dir !== path.dirname(dir)) {
|
|
44
|
+
const nustack = path.join(dir, ".nustack");
|
|
45
|
+
if (existsSync(path.join(nustack, "workspace.json")) || existsSync(path.join(nustack, "app.json")))
|
|
46
|
+
return true;
|
|
47
|
+
dir = path.dirname(dir);
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
export function classifyFolder(dir) {
|
|
52
|
+
const entries = significantEntries(dir);
|
|
53
|
+
const gitPath = path.join(dir, ".git");
|
|
54
|
+
const hasGit = existsSync(gitPath);
|
|
55
|
+
if (hasGit) {
|
|
56
|
+
let repository;
|
|
57
|
+
try {
|
|
58
|
+
repository = resolveRepository(dir);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (error instanceof NotARepositoryError) {
|
|
62
|
+
return { class: "populated_non_git", entries };
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
const symbolicBranch = readSymbolicBranch(repository);
|
|
67
|
+
const operation = readGitOperationState(repository);
|
|
68
|
+
const pointerFileGit = lstatSync(gitPath).isFile();
|
|
69
|
+
const markerWorkspaceId = readMarkerWorkspaceId(repository.repositoryRoot);
|
|
70
|
+
return {
|
|
71
|
+
class: "git_worktree",
|
|
72
|
+
entries,
|
|
73
|
+
repository,
|
|
74
|
+
git: {
|
|
75
|
+
clean: operation.kind === "normal" && symbolicBranch !== null,
|
|
76
|
+
detachedHead: symbolicBranch === null,
|
|
77
|
+
operationKind: operation.kind,
|
|
78
|
+
operationInProgress: operation.kind !== "normal",
|
|
79
|
+
pointerFileGit,
|
|
80
|
+
hasWorkspaceMarker: markerWorkspaceId !== null,
|
|
81
|
+
markerWorkspaceId,
|
|
82
|
+
nestedUnderWorkspace: isNestedUnderWorkspace(repository.repositoryRoot),
|
|
83
|
+
symbolicBranch,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (entries.length === 0)
|
|
88
|
+
return { class: "genuinely_empty", entries: [] };
|
|
89
|
+
return { class: "populated_non_git", entries };
|
|
90
|
+
}
|
|
91
|
+
export function attachRejection(git) {
|
|
92
|
+
if (git.nestedUnderWorkspace)
|
|
93
|
+
return "nested_workspace";
|
|
94
|
+
if (git.operationInProgress)
|
|
95
|
+
return "operation_in_progress";
|
|
96
|
+
if (git.detachedHead)
|
|
97
|
+
return "detached_head";
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { resolveRepository } from "../git-sync/core/repository.js";
|
|
3
|
+
export function initGitRepository(dir) {
|
|
4
|
+
const env = { ...process.env };
|
|
5
|
+
delete env.GIT_DIR;
|
|
6
|
+
delete env.GIT_WORK_TREE;
|
|
7
|
+
delete env.GIT_INDEX_FILE;
|
|
8
|
+
execFileSync("git", ["init", "-b", "main", dir], { env, stdio: "ignore" });
|
|
9
|
+
return resolveRepository(dir);
|
|
10
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { downloadTargetContent, fetchHandoffManifest } from "../nustack_client.js";
|
|
2
|
+
import { updateBindingCursor } from "../local_store/bindings.js";
|
|
3
|
+
import { recordInboundCommit } from "../local_store/commit_origins.js";
|
|
4
|
+
import { readHeadCommit } from "../git-sync/core/repository.js";
|
|
5
|
+
import { buildCommit, buildCommitMessage, casBranchRef, materializeTarget, NUSTACK_SYNC_IDENTITY, } from "../git-sync/inbound/apply_commit.js";
|
|
6
|
+
import { decideHandoffContent } from "../git-sync/inbound/content_decision.js";
|
|
7
|
+
export function toResolvedTarget(target) {
|
|
8
|
+
return {
|
|
9
|
+
path: target.path,
|
|
10
|
+
operation: target.operation,
|
|
11
|
+
targetSha256: target.targetSha256,
|
|
12
|
+
byteSize: target.byteSize,
|
|
13
|
+
base: target.baseAbsent ? { absent: true } : { sha256: target.baseSha256 ?? "" },
|
|
14
|
+
...(target.contentUrl ? { contentUrl: target.contentUrl } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export async function resolveHandoffTargets(baseUrl, accessToken, handoffId, bindingId) {
|
|
18
|
+
const manifest = await fetchHandoffManifest(baseUrl, accessToken, { handoffId, bindingId });
|
|
19
|
+
const bytesByPath = new Map();
|
|
20
|
+
for (const target of manifest.targets) {
|
|
21
|
+
if (target.operation === "upsert" && target.contentUrl) {
|
|
22
|
+
const bytes = await downloadTargetContent(baseUrl, accessToken, target.contentUrl);
|
|
23
|
+
bytesByPath.set(target.path, Buffer.from(bytes));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return { manifestTargets: manifest.targets, bytesByPath };
|
|
27
|
+
}
|
|
28
|
+
export function decideOnboardingApply(repo, targets) {
|
|
29
|
+
if (targets.length === 0)
|
|
30
|
+
return { kind: "no_op" };
|
|
31
|
+
const decision = decideHandoffContent(repo, targets);
|
|
32
|
+
if (decision.kind === "no_op")
|
|
33
|
+
return { kind: "no_op" };
|
|
34
|
+
if (decision.kind === "automatic")
|
|
35
|
+
return { kind: "apply" };
|
|
36
|
+
return { kind: "overwrite_required", divergent: decision.divergent };
|
|
37
|
+
}
|
|
38
|
+
export function composeOnboardingCommit(store, repo, binding, handoffId, workspaceId, entries) {
|
|
39
|
+
const oldHead = readHeadCommit(repo);
|
|
40
|
+
const targets = entries.map((entry) => ({
|
|
41
|
+
path: entry.path,
|
|
42
|
+
operation: entry.operation,
|
|
43
|
+
targetSha256: null,
|
|
44
|
+
byteSize: entry.bytes ? entry.bytes.byteLength : 0,
|
|
45
|
+
base: { absent: true },
|
|
46
|
+
}));
|
|
47
|
+
const bytesByPath = new Map();
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
if (entry.operation === "upsert" && entry.bytes)
|
|
50
|
+
bytesByPath.set(entry.path, entry.bytes);
|
|
51
|
+
}
|
|
52
|
+
const message = buildCommitMessage(handoffId, workspaceId, null);
|
|
53
|
+
const built = buildCommit(repo, oldHead, targets, bytesByPath, message, {
|
|
54
|
+
authorName: NUSTACK_SYNC_IDENTITY.name,
|
|
55
|
+
authorEmail: NUSTACK_SYNC_IDENTITY.email,
|
|
56
|
+
committerName: NUSTACK_SYNC_IDENTITY.name,
|
|
57
|
+
committerEmail: NUSTACK_SYNC_IDENTITY.email,
|
|
58
|
+
});
|
|
59
|
+
for (const target of targets) {
|
|
60
|
+
materializeTarget(repo, target, built.blobByPath.get(target.path) ?? null, bytesByPath.get(target.path) ?? null);
|
|
61
|
+
}
|
|
62
|
+
if (!casBranchRef(repo, binding.boundBranch, built.commitId, oldHead)) {
|
|
63
|
+
throw new Error("the bound branch moved while recording the onboarding commit — re-run to converge");
|
|
64
|
+
}
|
|
65
|
+
recordInboundCommit(store, { bindingId: binding.id, commitId: built.commitId, handoffId });
|
|
66
|
+
updateBindingCursor(store, binding.id, { lastProcessedCommitId: built.commitId });
|
|
67
|
+
return { commitId: built.commitId, changedPaths: targets.map((target) => target.path) };
|
|
68
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { acknowledgeBindingCursor, createWorkspaceViaAsk, getWorkspace, latestHandoff, } from "../nustack_client.js";
|
|
4
|
+
import { getBindingById } from "../local_store/bindings.js";
|
|
5
|
+
import { hasManifest, scaffoldManifest } from "../manifest.js";
|
|
6
|
+
import { fetchToolkitProfile, toolkitProfileFromManifest, ToolkitProfileUnavailableError } from "../etna/profile.js";
|
|
7
|
+
import { fetchHandoffManifest } from "../nustack_client.js";
|
|
8
|
+
import { classifyFolder } from "./folder_state.js";
|
|
9
|
+
import { initGitRepository } from "./git_init.js";
|
|
10
|
+
import { composeOnboardingCommit, resolveHandoffTargets, } from "./handoff_apply.js";
|
|
11
|
+
import { runRepositoryOnboarding, } from "./binding.js";
|
|
12
|
+
export const MANAGED_ONBOARDING_FILES = [".nustack/workspace.json", "nustack.service.json"];
|
|
13
|
+
export async function initFreshFolder(ctx, options) {
|
|
14
|
+
const state = classifyFolder(ctx.cwd);
|
|
15
|
+
if (state.class === "git_worktree") {
|
|
16
|
+
return { ok: false, code: "ALREADY_A_WORKTREE", message: "This folder is already a Git worktree — use `nustack workspace attach` instead of `nustack init`." };
|
|
17
|
+
}
|
|
18
|
+
if (state.class === "populated_non_git" && options.confirmPopulated !== true) {
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
code: "CONFIRMATION_REQUIRED",
|
|
22
|
+
message: `This folder has ${state.entries.length} existing file(s). Onboarding will create only ${MANAGED_ONBOARDING_FILES.join(", ")} and make one commit of just those — no pre-existing file is staged, and no remote or push is created. Re-run with confirmation to proceed.`,
|
|
23
|
+
managedFiles: MANAGED_ONBOARDING_FILES,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (!hasManifest(ctx.cwd)) {
|
|
27
|
+
await scaffoldManifest(ctx.cwd, options.slug);
|
|
28
|
+
}
|
|
29
|
+
const repository = initGitRepository(ctx.cwd);
|
|
30
|
+
const boundBranch = "main";
|
|
31
|
+
const workspaceId = options.workspaceId ?? (await createFreshWorkspace(ctx, state.class, options));
|
|
32
|
+
if (workspaceId === null) {
|
|
33
|
+
return { ok: false, code: "WORKSPACE_INITIALIZATION_FAILED", message: "The Workspace failed to initialize — check the Organization and try again." };
|
|
34
|
+
}
|
|
35
|
+
const profile = await resolveProfile(ctx, workspaceId);
|
|
36
|
+
if (!profile) {
|
|
37
|
+
return { ok: false, code: "HARNESS_PROFILE_UNAVAILABLE", message: "The Workspace did not project a Toolkit Profile pin — the owning server plan must expose it." };
|
|
38
|
+
}
|
|
39
|
+
const result = await runRepositoryOnboarding({
|
|
40
|
+
store: ctx.store,
|
|
41
|
+
baseUrl: ctx.baseUrl,
|
|
42
|
+
accessToken: ctx.accessToken,
|
|
43
|
+
deviceRef: ctx.deviceRef,
|
|
44
|
+
flow: "fresh_folder",
|
|
45
|
+
workspaceId,
|
|
46
|
+
organizationId: options.organizationId,
|
|
47
|
+
repository,
|
|
48
|
+
boundBranch,
|
|
49
|
+
profile,
|
|
50
|
+
baselineCommitId: null,
|
|
51
|
+
toolkitDeps: ctx.toolkitDeps,
|
|
52
|
+
runTelemetry: ctx.runTelemetry,
|
|
53
|
+
});
|
|
54
|
+
const seed = await latestHandoff(ctx.baseUrl, ctx.accessToken, workspaceId);
|
|
55
|
+
const entries = MANAGED_ONBOARDING_FILES.map((rel) => ({
|
|
56
|
+
path: rel,
|
|
57
|
+
operation: "upsert",
|
|
58
|
+
bytes: readFileSync(path.join(repository.repositoryRoot, rel)),
|
|
59
|
+
}));
|
|
60
|
+
if (seed) {
|
|
61
|
+
const { manifestTargets, bytesByPath } = await resolveHandoffTargets(ctx.baseUrl, ctx.accessToken, seed.id, result.bindingId);
|
|
62
|
+
for (const target of manifestTargets) {
|
|
63
|
+
if (!target.baseAbsent) {
|
|
64
|
+
return { ok: false, code: "SEED_TARGET_NOT_BASE_ABSENT", message: `Seed Handoff target ${target.path} is not baseAbsent — a fresh folder cannot host a non-clean target.` };
|
|
65
|
+
}
|
|
66
|
+
entries.push({ path: target.path, operation: target.operation, bytes: bytesByPath.get(target.path) ?? null });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const binding = getBindingById(ctx.store, result.bindingId);
|
|
70
|
+
const handoffId = seed?.id ?? `onboarding_seed:${workspaceId}`;
|
|
71
|
+
const commit = composeOnboardingCommit(ctx.store, repository, binding, handoffId, workspaceId, entries);
|
|
72
|
+
if (seed) {
|
|
73
|
+
await acknowledgeBindingCursor(ctx.baseUrl, ctx.accessToken, { bindingId: result.bindingId, lastAppliedSequence: 0 }).catch(() => { });
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
result: { ...result, baselineCommitId: commit.commitId, rootCommitId: commit.commitId, changedPaths: commit.changedPaths, handoffId: seed?.id ?? null },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function createFreshWorkspace(ctx, folderClass, options) {
|
|
81
|
+
void folderClass;
|
|
82
|
+
const outcome = await createWorkspaceViaAsk(ctx.baseUrl, ctx.accessToken, options.organizationId, `Create a workspace named "${options.name}".`);
|
|
83
|
+
const final = await getWorkspace(ctx.baseUrl, ctx.accessToken, outcome.workspaceId);
|
|
84
|
+
const failed = final.status === "initialization_failed" || final.initStatus === "failed";
|
|
85
|
+
return failed ? null : outcome.workspaceId;
|
|
86
|
+
}
|
|
87
|
+
async function resolveProfile(ctx, workspaceId) {
|
|
88
|
+
try {
|
|
89
|
+
return await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, workspaceId);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (!(error instanceof ToolkitProfileUnavailableError))
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
const seed = await latestHandoff(ctx.baseUrl, ctx.accessToken, workspaceId);
|
|
96
|
+
if (!seed)
|
|
97
|
+
return null;
|
|
98
|
+
const manifest = await fetchHandoffManifest(ctx.baseUrl, ctx.accessToken, { handoffId: seed.id, bindingId: "" }).catch(() => null);
|
|
99
|
+
return manifest ? toolkitProfileFromManifest(manifest) : null;
|
|
100
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { readFile, writeFile, rename, unlink, mkdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { legacyAppJsonPath, readBindingMarker, workspaceJsonPath } from "../workspace_context.js";
|
|
5
|
+
import { manifestPath } from "../manifest.js";
|
|
6
|
+
const CURRENT_BINDING_PROTOCOL_VERSION = 1;
|
|
7
|
+
const CURRENT_DOCUMENT_PROTOCOL_VERSION = 1;
|
|
8
|
+
function buildWorkspaceManifest(identity) {
|
|
9
|
+
return {
|
|
10
|
+
schemaVersion: 2,
|
|
11
|
+
workspaceId: identity.workspaceId,
|
|
12
|
+
organizationId: identity.organizationId,
|
|
13
|
+
bindingProtocolVersion: identity.bindingProtocolVersion ?? CURRENT_BINDING_PROTOCOL_VERSION,
|
|
14
|
+
documentProtocolVersion: identity.documentProtocolVersion ?? CURRENT_DOCUMENT_PROTOCOL_VERSION,
|
|
15
|
+
toolkit: {
|
|
16
|
+
name: identity.toolkit.name,
|
|
17
|
+
version: identity.toolkit.version,
|
|
18
|
+
rendererVersion: identity.toolkit.rendererVersion,
|
|
19
|
+
placementMapVersion: identity.toolkit.placementMapVersion,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async function atomicWrite(target, content) {
|
|
24
|
+
const prior = existsSync(target) ? await readFile(target, "utf8") : null;
|
|
25
|
+
const temp = `${target}.nustack-tmp-${process.pid}`;
|
|
26
|
+
await writeFile(temp, content, "utf8");
|
|
27
|
+
await rename(temp, target);
|
|
28
|
+
return prior;
|
|
29
|
+
}
|
|
30
|
+
async function restore(target, prior) {
|
|
31
|
+
if (prior === null) {
|
|
32
|
+
await unlink(target).catch(() => { });
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
await writeFile(target, prior, "utf8");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function writeWorkspaceManifests(root, identity, hooks = {}) {
|
|
39
|
+
const servicePath = manifestPath(root);
|
|
40
|
+
if (!existsSync(servicePath)) {
|
|
41
|
+
throw new Error("nustack.service.json is required before writing the Workspace binding — scaffold it first.");
|
|
42
|
+
}
|
|
43
|
+
const serviceRaw = await readFile(servicePath, "utf8");
|
|
44
|
+
let serviceManifest;
|
|
45
|
+
try {
|
|
46
|
+
serviceManifest = JSON.parse(serviceRaw);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error("nustack.service.json is not valid JSON — refusing to overwrite it.");
|
|
50
|
+
}
|
|
51
|
+
const nextService = {
|
|
52
|
+
...serviceManifest,
|
|
53
|
+
nustackWorkspace: { v: 2, workspaceId: identity.workspaceId, organizationId: identity.organizationId },
|
|
54
|
+
toolkit: { name: identity.toolkit.name, version: identity.toolkit.version },
|
|
55
|
+
};
|
|
56
|
+
delete nextService.nustackApp;
|
|
57
|
+
const workspaceManifest = buildWorkspaceManifest(identity);
|
|
58
|
+
const workspaceDir = path.dirname(workspaceJsonPath(root));
|
|
59
|
+
await mkdir(workspaceDir, { recursive: true });
|
|
60
|
+
const workspacePrior = await atomicWrite(workspaceJsonPath(root), JSON.stringify(workspaceManifest, null, 2) + "\n");
|
|
61
|
+
try {
|
|
62
|
+
if (hooks.beforeServiceWrite)
|
|
63
|
+
await hooks.beforeServiceWrite();
|
|
64
|
+
await atomicWrite(servicePath, JSON.stringify(nextService, null, 4) + "\n");
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
await restore(workspaceJsonPath(root), workspacePrior);
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
return workspaceManifest;
|
|
71
|
+
}
|
|
72
|
+
function toolkitEqual(a, b) {
|
|
73
|
+
return a !== null && b !== null && a.name === b.name && a.version === b.version;
|
|
74
|
+
}
|
|
75
|
+
export async function readWorkspaceManifests(root) {
|
|
76
|
+
const workspaceManifest = await readWorkspaceJson(root);
|
|
77
|
+
const service = await readServiceBlocks(root);
|
|
78
|
+
let localDiagnosis;
|
|
79
|
+
if (!workspaceManifest || !service.toolkit) {
|
|
80
|
+
localDiagnosis = "missing";
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
localDiagnosis = toolkitEqual(workspaceManifest.toolkit, service.toolkit) ? "aligned" : "divergent";
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
workspaceManifest,
|
|
87
|
+
serviceWorkspaceBinding: service.binding,
|
|
88
|
+
serviceToolkit: service.toolkit,
|
|
89
|
+
localDiagnosis,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function diagnoseManifests(cloud, workspaceJson, serviceJson) {
|
|
93
|
+
if (!workspaceJson || !serviceJson)
|
|
94
|
+
return "missing";
|
|
95
|
+
if (!cloud) {
|
|
96
|
+
return toolkitEqual(workspaceJson, serviceJson) ? "aligned" : "divergent";
|
|
97
|
+
}
|
|
98
|
+
const workspaceOk = toolkitEqual(cloud, workspaceJson);
|
|
99
|
+
const serviceOk = toolkitEqual(cloud, serviceJson);
|
|
100
|
+
if (workspaceOk && serviceOk)
|
|
101
|
+
return "aligned";
|
|
102
|
+
if (!workspaceOk && serviceOk)
|
|
103
|
+
return "workspace_json_stale";
|
|
104
|
+
if (workspaceOk && !serviceOk)
|
|
105
|
+
return "service_json_stale";
|
|
106
|
+
return "divergent";
|
|
107
|
+
}
|
|
108
|
+
async function readWorkspaceJson(root) {
|
|
109
|
+
const marker = await readBindingMarker(root);
|
|
110
|
+
if (!marker)
|
|
111
|
+
return null;
|
|
112
|
+
const filePath = marker.version === 2 ? workspaceJsonPath(root) : legacyAppJsonPath(root);
|
|
113
|
+
if (!existsSync(filePath))
|
|
114
|
+
return null;
|
|
115
|
+
try {
|
|
116
|
+
const raw = JSON.parse(await readFile(filePath, "utf8"));
|
|
117
|
+
if (marker.organizationId === null)
|
|
118
|
+
return null;
|
|
119
|
+
const h = raw.toolkit;
|
|
120
|
+
if (!h || typeof h.name !== "string" || typeof h.version !== "string")
|
|
121
|
+
return null;
|
|
122
|
+
return {
|
|
123
|
+
schemaVersion: 2,
|
|
124
|
+
workspaceId: marker.workspaceId,
|
|
125
|
+
organizationId: marker.organizationId,
|
|
126
|
+
bindingProtocolVersion: typeof raw.bindingProtocolVersion === "number" ? raw.bindingProtocolVersion : 1,
|
|
127
|
+
documentProtocolVersion: typeof raw.documentProtocolVersion === "number" ? raw.documentProtocolVersion : 1,
|
|
128
|
+
toolkit: {
|
|
129
|
+
name: h.name,
|
|
130
|
+
version: h.version,
|
|
131
|
+
rendererVersion: typeof h.rendererVersion === "string" ? h.rendererVersion : "",
|
|
132
|
+
placementMapVersion: typeof h.placementMapVersion === "string" ? h.placementMapVersion : "",
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function readServiceBlocks(root) {
|
|
141
|
+
const filePath = manifestPath(root);
|
|
142
|
+
if (!existsSync(filePath))
|
|
143
|
+
return { binding: null, toolkit: null };
|
|
144
|
+
try {
|
|
145
|
+
const manifest = JSON.parse(await readFile(filePath, "utf8"));
|
|
146
|
+
const v2 = manifest.nustackWorkspace;
|
|
147
|
+
const v1 = manifest.nustackApp;
|
|
148
|
+
const binding = typeof v2?.workspaceId === "string" && typeof v2?.organizationId === "string"
|
|
149
|
+
? { workspaceId: v2.workspaceId, organizationId: v2.organizationId }
|
|
150
|
+
: typeof v1?.appId === "string" && typeof v1?.workspaceId === "string"
|
|
151
|
+
? { workspaceId: v1.appId, organizationId: v1.workspaceId }
|
|
152
|
+
: null;
|
|
153
|
+
const toolkit = typeof manifest.toolkit?.name === "string" && typeof manifest.toolkit?.version === "string"
|
|
154
|
+
? { name: manifest.toolkit.name, version: manifest.toolkit.version }
|
|
155
|
+
: null;
|
|
156
|
+
return { binding, toolkit };
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return { binding: null, toolkit: null };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { acknowledgeBindingCursor, latestHandoff } from "../nustack_client.js";
|
|
4
|
+
import { fetchHandoffManifest } from "../nustack_client.js";
|
|
5
|
+
import { getBindingById } from "../local_store/bindings.js";
|
|
6
|
+
import { readHeadCommit, readSymbolicBranch } from "../git-sync/core/repository.js";
|
|
7
|
+
import { fetchToolkitProfile, toolkitProfileFromManifest, ToolkitProfileUnavailableError } from "../etna/profile.js";
|
|
8
|
+
import { hasManifest, scaffoldManifest } from "../manifest.js";
|
|
9
|
+
import { classifyFolder, attachRejection } from "./folder_state.js";
|
|
10
|
+
import { initGitRepository } from "./git_init.js";
|
|
11
|
+
import { MANAGED_ONBOARDING_FILES } from "./init_fresh_folder.js";
|
|
12
|
+
import { composeOnboardingCommit, decideOnboardingApply, resolveHandoffTargets, toResolvedTarget, } from "./handoff_apply.js";
|
|
13
|
+
import { runRepositoryOnboarding, } from "./binding.js";
|
|
14
|
+
export async function webFirstAttach(ctx, options) {
|
|
15
|
+
const state = classifyFolder(ctx.cwd);
|
|
16
|
+
let repository = state.repository ?? null;
|
|
17
|
+
let boundBranch;
|
|
18
|
+
let isFresh;
|
|
19
|
+
if (state.class === "git_worktree" && state.git && repository) {
|
|
20
|
+
const rejection = attachRejection(state.git);
|
|
21
|
+
if (rejection)
|
|
22
|
+
return { ok: false, code: rejection.toUpperCase(), message: rejectionMessage(rejection) };
|
|
23
|
+
const branch = readSymbolicBranch(repository);
|
|
24
|
+
if (!branch)
|
|
25
|
+
return { ok: false, code: "DETACHED_HEAD", message: "HEAD is detached — check out a branch before attaching." };
|
|
26
|
+
boundBranch = branch;
|
|
27
|
+
isFresh = false;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
if (state.class === "populated_non_git" && options.confirmPopulated !== true) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
code: "CONFIRMATION_REQUIRED",
|
|
34
|
+
message: `This folder has ${state.entries.length} existing file(s). Attaching will establish Git and create only ${MANAGED_ONBOARDING_FILES.join(", ")} — no pre-existing file is staged, and no remote or push is created. Re-run with confirmation to proceed.`,
|
|
35
|
+
managedFiles: MANAGED_ONBOARDING_FILES,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
repository = initGitRepository(ctx.cwd);
|
|
39
|
+
boundBranch = "main";
|
|
40
|
+
isFresh = true;
|
|
41
|
+
}
|
|
42
|
+
if (!hasManifest(repository.repositoryRoot)) {
|
|
43
|
+
await scaffoldManifest(repository.repositoryRoot, deriveScaffoldSlug(repository.repositoryRoot));
|
|
44
|
+
}
|
|
45
|
+
const profile = await resolveProfile(ctx, options.workspaceId);
|
|
46
|
+
if (!profile)
|
|
47
|
+
return { ok: false, code: "HARNESS_PROFILE_UNAVAILABLE", message: "The Workspace did not project a Toolkit Profile pin — the owning server plan must expose it." };
|
|
48
|
+
const baselineCommitId = isFresh ? null : readHeadCommit(repository);
|
|
49
|
+
const result = await runRepositoryOnboarding({
|
|
50
|
+
store: ctx.store,
|
|
51
|
+
baseUrl: ctx.baseUrl,
|
|
52
|
+
accessToken: ctx.accessToken,
|
|
53
|
+
deviceRef: ctx.deviceRef,
|
|
54
|
+
flow: "web_first",
|
|
55
|
+
workspaceId: options.workspaceId,
|
|
56
|
+
organizationId: options.organizationId,
|
|
57
|
+
repository,
|
|
58
|
+
boundBranch,
|
|
59
|
+
profile,
|
|
60
|
+
baselineCommitId,
|
|
61
|
+
toolkitDeps: ctx.toolkitDeps,
|
|
62
|
+
runTelemetry: ctx.runTelemetry,
|
|
63
|
+
});
|
|
64
|
+
const binding = getBindingById(ctx.store, result.bindingId);
|
|
65
|
+
const latest = await latestHandoff(ctx.baseUrl, ctx.accessToken, options.workspaceId).catch(() => null);
|
|
66
|
+
if (!latest) {
|
|
67
|
+
const applied = isFresh
|
|
68
|
+
? composeOnboardingCommit(ctx.store, repository, binding, `onboarding_seed:${options.workspaceId}`, options.workspaceId, managedEntries(repository.repositoryRoot))
|
|
69
|
+
: null;
|
|
70
|
+
return { ok: true, result: baseResult(result, applied, null, false, []) };
|
|
71
|
+
}
|
|
72
|
+
const { manifestTargets, bytesByPath } = await resolveHandoffTargets(ctx.baseUrl, ctx.accessToken, latest.id, result.bindingId);
|
|
73
|
+
const resolved = manifestTargets.map(toResolvedTarget);
|
|
74
|
+
const decision = decideOnboardingApply(repository, resolved);
|
|
75
|
+
if (decision.kind === "overwrite_required") {
|
|
76
|
+
return {
|
|
77
|
+
ok: true,
|
|
78
|
+
result: baseResult(result, null, latest.id, true, decision.divergent),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const entries = [];
|
|
82
|
+
if (isFresh)
|
|
83
|
+
entries.push(...managedEntries(repository.repositoryRoot));
|
|
84
|
+
if (decision.kind === "apply") {
|
|
85
|
+
for (const target of manifestTargets) {
|
|
86
|
+
entries.push({ path: target.path, operation: target.operation, bytes: bytesByPath.get(target.path) ?? null });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const applied = entries.length > 0 ? composeOnboardingCommit(ctx.store, repository, binding, latest.id, options.workspaceId, entries) : null;
|
|
90
|
+
await acknowledgeBindingCursor(ctx.baseUrl, ctx.accessToken, { bindingId: result.bindingId, lastAppliedSequence: 0 }).catch(() => { });
|
|
91
|
+
return { ok: true, result: baseResult(result, applied, latest.id, false, []) };
|
|
92
|
+
}
|
|
93
|
+
function managedEntries(root) {
|
|
94
|
+
return MANAGED_ONBOARDING_FILES.map((rel) => ({ path: rel, operation: "upsert", bytes: readFileSync(path.join(root, rel)) }));
|
|
95
|
+
}
|
|
96
|
+
function baseResult(result, applied, handoffId, overwriteRequired, divergent) {
|
|
97
|
+
return {
|
|
98
|
+
...result,
|
|
99
|
+
baselineCommitId: applied?.commitId ?? result.baselineCommitId,
|
|
100
|
+
appliedCommitId: applied?.commitId ?? null,
|
|
101
|
+
changedPaths: applied?.changedPaths ?? [],
|
|
102
|
+
handoffId,
|
|
103
|
+
overwriteRequired,
|
|
104
|
+
divergent,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function resolveProfile(ctx, workspaceId) {
|
|
108
|
+
try {
|
|
109
|
+
return await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, workspaceId);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (!(error instanceof ToolkitProfileUnavailableError))
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
const seed = await latestHandoff(ctx.baseUrl, ctx.accessToken, workspaceId).catch(() => null);
|
|
116
|
+
if (!seed)
|
|
117
|
+
return null;
|
|
118
|
+
const manifest = await fetchHandoffManifest(ctx.baseUrl, ctx.accessToken, { handoffId: seed.id, bindingId: "" }).catch(() => null);
|
|
119
|
+
return manifest ? toolkitProfileFromManifest(manifest) : null;
|
|
120
|
+
}
|
|
121
|
+
function deriveScaffoldSlug(root) {
|
|
122
|
+
return path.basename(root).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "workspace";
|
|
123
|
+
}
|
|
124
|
+
function rejectionMessage(rejection) {
|
|
125
|
+
switch (rejection) {
|
|
126
|
+
case "nested_workspace":
|
|
127
|
+
return "This repository is nested inside another NuStack Workspace — one Workspace has one logical repository root (F-014).";
|
|
128
|
+
case "operation_in_progress":
|
|
129
|
+
return "A merge, rebase, cherry-pick, or revert is in progress — finish or abort it, then re-attach.";
|
|
130
|
+
case "detached_head":
|
|
131
|
+
return "HEAD is detached — check out a branch before attaching.";
|
|
132
|
+
}
|
|
133
|
+
}
|