@amalgm/shell 0.1.39 → 0.1.41
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/PURPOSE.md +33 -9
- package/dist/files-register-host.d.ts +10 -0
- package/dist/files-register-host.js +26 -15
- package/dist/files-register-host.js.map +1 -1
- package/dist/git-repository-host.js +29 -7
- package/dist/git-repository-host.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/machine-host.d.ts +9 -1
- package/dist/machine-host.js +3 -1
- package/dist/machine-host.js.map +1 -1
- package/dist/materialized-graph.js +1 -1
- package/dist/materialized-graph.js.map +1 -1
- package/dist/user-ground-host.d.ts +9 -0
- package/dist/user-ground-host.js +379 -80
- package/dist/user-ground-host.js.map +1 -1
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { open } from "node:fs/promises";
|
|
4
|
-
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { buildUserHomeManifest, buildUserManifest, liveMachineStateDir, scopedAmalgmDir, shippedUserHomeDeclaration, } from "@amalgm/core/identity";
|
|
6
6
|
import { CHUNK_BYTES, CONTENT_CONTRACT, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, convergeUserGround, createUserGroundEnrollmentPolicy, isRepositoryMetadataEntry, download as downloadArtifact, deriveUploadManifest, membershipHash, parseSnapshot, pathIsSuspect, privateEntityResourceId, sameRecords, snapshotFromRecords, stableJson, travelingRecords, upload as uploadArtifact, userGroundRecords, } from "@amalgm/live";
|
|
7
7
|
import Database from "better-sqlite3";
|
|
8
8
|
import { atomicCopy, atomicWrite, ensurePrivateDir } from "./filesystem.js";
|
|
9
9
|
import { ContentCacheDownload, captureContentFile, hashContentFile, } from "./content-cache-host.js";
|
|
10
10
|
import { decodeContentWireBytes, encodeContentWireBytes } from "./content-wire-codec.js";
|
|
11
|
-
import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, ensureWorkspaceReference, pathExists, pathWithin, referenceWorkspaceId, workspaceBindingDir, } from "./files-register-host.js";
|
|
11
|
+
import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, ensureWorkspaceBinding, ensureWorkspaceReference, pathExists, pathWithin, referenceWorkspaceId, workspaceBindingDir, } from "./files-register-host.js";
|
|
12
12
|
import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile, hasGitMarker, } from "./git-repository-host.js";
|
|
13
13
|
import { inspectGitRegistration, } from "./git-registration-host.js";
|
|
14
14
|
import { projectMaterializedGraph } from "./materialized-graph.js";
|
|
@@ -57,6 +57,11 @@ export class UserGroundHost {
|
|
|
57
57
|
cloud: this.cloudPort(this.activeIdentity),
|
|
58
58
|
local,
|
|
59
59
|
});
|
|
60
|
+
await this.resumeWorkspaceAdds(this.activeIdentity);
|
|
61
|
+
const watchHealth = this.watchHost.health();
|
|
62
|
+
if (watchHealth.state !== "healthy") {
|
|
63
|
+
throw new Error(`Watch coverage is degraded: ${watchHealth.reason ?? "unknown failure"}`);
|
|
64
|
+
}
|
|
60
65
|
const evidence = {
|
|
61
66
|
userId: result.userId,
|
|
62
67
|
userEmail: result.userEmail,
|
|
@@ -64,9 +69,9 @@ export class UserGroundHost {
|
|
|
64
69
|
localRoot: result.localRoot,
|
|
65
70
|
cloudHead: result.cloudHead,
|
|
66
71
|
mode: result.mode,
|
|
67
|
-
entityCount:
|
|
68
|
-
watcherRoots:
|
|
69
|
-
watching:
|
|
72
|
+
entityCount: readRows(this.databasePath(this.activeIdentity)).length,
|
|
73
|
+
watcherRoots: watchHealth.contentHandles,
|
|
74
|
+
watching: true,
|
|
70
75
|
};
|
|
71
76
|
this.converged = true;
|
|
72
77
|
return evidence;
|
|
@@ -140,18 +145,80 @@ export class UserGroundHost {
|
|
|
140
145
|
references: input.references,
|
|
141
146
|
records: input.records,
|
|
142
147
|
destinationParent: input.destinationParent,
|
|
148
|
+
cloudHead: input.cloudHead,
|
|
143
149
|
readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact),
|
|
150
|
+
onStage: (stage) => this.options.onWorkspaceAddStage?.({
|
|
151
|
+
workspaceId: input.workspace.uuid,
|
|
152
|
+
stage,
|
|
153
|
+
}),
|
|
144
154
|
});
|
|
145
|
-
this.scheduleRescan();
|
|
146
155
|
return installed;
|
|
147
156
|
},
|
|
148
157
|
coverWorkspace: async ({ workspaceId, baseline }) => {
|
|
149
|
-
const
|
|
158
|
+
const pending = readWorkspaceAddIntent(this.databasePath(identity), workspaceId);
|
|
159
|
+
const health = this.ensureWatchers(identity, pending || baseline === "authoritative-install" ? [workspaceId] : []);
|
|
160
|
+
assertHealthyWatch(this.watchHost.evidence(), workspaceId);
|
|
161
|
+
if (pending) {
|
|
162
|
+
await this.options.onWorkspaceAddStage?.({ workspaceId, stage: "watching" });
|
|
163
|
+
deleteWorkspaceAddIntent(this.databasePath(identity), workspaceId);
|
|
164
|
+
}
|
|
150
165
|
return { active: true, roots: health.contentHandles };
|
|
151
166
|
},
|
|
152
167
|
};
|
|
153
168
|
return { register, add };
|
|
154
169
|
}
|
|
170
|
+
/** Resume the same host effect used by the SDK command before readiness.
|
|
171
|
+
* Staged ground has no binding, so recovery cannot race Watch or Detect. */
|
|
172
|
+
async resumeWorkspaceAdds(identity) {
|
|
173
|
+
const database = this.databasePath(identity);
|
|
174
|
+
const userRoot = this.userRoot(identity);
|
|
175
|
+
const bindingDir = workspaceBindingDir(userRoot, identity.deviceId);
|
|
176
|
+
for (const intent of readWorkspaceAddIntents(database)) {
|
|
177
|
+
const selection = workspaceAddSelection(intent);
|
|
178
|
+
const visibleAcrossBlindInterval = pathExists(intent.destinationPath);
|
|
179
|
+
const stagedAcrossBlindInterval = pathExists(intent.stagingPath);
|
|
180
|
+
if (stagedAcrossBlindInterval && readRows(database).some((row) => row.resourceId === intent.resourceId && row.rootUUID === intent.workspaceId)) {
|
|
181
|
+
// Rows prove that staging was verified before the crash, not that its
|
|
182
|
+
// bytes remained unchanged while no watcher existed. Return this
|
|
183
|
+
// private install to the intent-only stage and reproduce it from the
|
|
184
|
+
// verified immutable cache before making it visible.
|
|
185
|
+
deleteRootRows(database, intent.resourceId, intent.workspaceId);
|
|
186
|
+
}
|
|
187
|
+
await installCloudWorkspace({
|
|
188
|
+
identity,
|
|
189
|
+
userRoot,
|
|
190
|
+
database,
|
|
191
|
+
cacheDir: this.cacheDir(identity),
|
|
192
|
+
bindingDir,
|
|
193
|
+
resourceId: intent.resourceId,
|
|
194
|
+
workspace: selection.workspace,
|
|
195
|
+
reference: selection.reference,
|
|
196
|
+
references: intent.references,
|
|
197
|
+
records: intent.records,
|
|
198
|
+
destinationParent: dirname(intent.destinationPath),
|
|
199
|
+
cloudHead: intent.cloudHead,
|
|
200
|
+
readContent: (artifact) => this.downloadContent(intent.resourceId, this.cacheDir(identity), artifact),
|
|
201
|
+
onStage: (stage) => this.options.onWorkspaceAddStage?.({
|
|
202
|
+
workspaceId: intent.workspaceId,
|
|
203
|
+
stage,
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
this.ensureWatchers(identity, visibleAcrossBlindInterval ? [] : [intent.workspaceId]);
|
|
207
|
+
assertHealthyWatch(this.watchHost.evidence(), intent.workspaceId);
|
|
208
|
+
if (visibleAcrossBlindInterval) {
|
|
209
|
+
// Ground that was visible while the runtime was absent may have been
|
|
210
|
+
// edited after its verified reveal. It is an ordinary startup blind
|
|
211
|
+
// interval, so Watch must retain complete suspicion and Detect must
|
|
212
|
+
// settle it before the Add responsibility can be cleared.
|
|
213
|
+
await this.flushObservedChanges();
|
|
214
|
+
}
|
|
215
|
+
await this.options.onWorkspaceAddStage?.({
|
|
216
|
+
workspaceId: intent.workspaceId,
|
|
217
|
+
stage: "watching",
|
|
218
|
+
});
|
|
219
|
+
deleteWorkspaceAddIntent(database, intent.workspaceId);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
155
222
|
/** Finish the user-visible register journey inside the one persistent Files
|
|
156
223
|
* owner. The declaration remains durable if any later stage fails, and a
|
|
157
224
|
* retry reuses its UUID through filesCommandPorts().register. */
|
|
@@ -179,7 +246,9 @@ export class UserGroundHost {
|
|
|
179
246
|
watch,
|
|
180
247
|
};
|
|
181
248
|
}
|
|
182
|
-
this.
|
|
249
|
+
// Reconcile already made this newly declared root suspicious. Consume
|
|
250
|
+
// that evidence as-is: registration of one workspace must never widen
|
|
251
|
+
// uncertainty to every other root on the machine.
|
|
183
252
|
this.watchDirty = false;
|
|
184
253
|
try {
|
|
185
254
|
await this.syncNow();
|
|
@@ -188,7 +257,6 @@ export class UserGroundHost {
|
|
|
188
257
|
// declaration. Preserve the declared root as suspicion and give it
|
|
189
258
|
// one scan of its own. Completion is evidence about this workspace,
|
|
190
259
|
// never a demand that unrelated live Watch traffic become silent.
|
|
191
|
-
this.watchHost.suspectAll();
|
|
192
260
|
this.watchDirty = false;
|
|
193
261
|
await this.syncNow();
|
|
194
262
|
}
|
|
@@ -606,22 +674,6 @@ export class UserGroundHost {
|
|
|
606
674
|
materialized: true,
|
|
607
675
|
});
|
|
608
676
|
}
|
|
609
|
-
for (const row of readRows(this.databasePath(identity))) {
|
|
610
|
-
if (row.parentUUID !== null || row.absolutePath === root)
|
|
611
|
-
continue;
|
|
612
|
-
const directory = directoryExists(row.absolutePath)
|
|
613
|
-
? realpathSync(row.absolutePath)
|
|
614
|
-
: resolve(row.absolutePath);
|
|
615
|
-
const prior = wanted.get(directory);
|
|
616
|
-
if (prior && prior.rootId !== row.rootUUID) {
|
|
617
|
-
throw new Error(`two logical roots claim the same Watch ground: ${directory}`);
|
|
618
|
-
}
|
|
619
|
-
wanted.set(directory, {
|
|
620
|
-
rootId: row.rootUUID,
|
|
621
|
-
directory,
|
|
622
|
-
materialized: directoryExists(directory),
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
677
|
const health = this.watchHost.reconcile([...wanted.values()], {
|
|
626
678
|
authoritativeBaselineRootIds,
|
|
627
679
|
});
|
|
@@ -895,6 +947,11 @@ function initializeDatabase(file) {
|
|
|
895
947
|
snapshot_json TEXT NOT NULL,
|
|
896
948
|
created_at TEXT NOT NULL
|
|
897
949
|
);
|
|
950
|
+
CREATE TABLE IF NOT EXISTS workspace_add_intents (
|
|
951
|
+
workspace_uuid TEXT PRIMARY KEY,
|
|
952
|
+
destination_path TEXT NOT NULL UNIQUE,
|
|
953
|
+
intent_json TEXT NOT NULL
|
|
954
|
+
);
|
|
898
955
|
CREATE INDEX IF NOT EXISTS entities_by_absolute_path
|
|
899
956
|
ON entities(absolute_path);
|
|
900
957
|
CREATE INDEX IF NOT EXISTS entities_by_physical_identity
|
|
@@ -936,6 +993,112 @@ function readRows(file) {
|
|
|
936
993
|
database.close();
|
|
937
994
|
}
|
|
938
995
|
}
|
|
996
|
+
function workspaceAddStagingPath(destinationPath, workspaceId) {
|
|
997
|
+
return join(dirname(destinationPath), `.amalgm-${workspaceId}.adding`);
|
|
998
|
+
}
|
|
999
|
+
function workspaceAddSelection(intent) {
|
|
1000
|
+
const workspace = intent.records.find((record) => record.uuid === intent.workspaceId);
|
|
1001
|
+
if (!workspace || !["workspace", "folder", "repo.git"].includes(workspace.type)) {
|
|
1002
|
+
throw new Error(`workspace Add intent ${intent.workspaceId} has no materialization boundary`);
|
|
1003
|
+
}
|
|
1004
|
+
const references = intent.references.filter((record) => record.type === "reference"
|
|
1005
|
+
&& record.status === "active"
|
|
1006
|
+
&& record.payloadVersion === intent.workspaceId);
|
|
1007
|
+
if (references.length !== 1) {
|
|
1008
|
+
throw new Error(`workspace Add intent ${intent.workspaceId} has no unique durable reference`);
|
|
1009
|
+
}
|
|
1010
|
+
return { workspace, reference: references[0] };
|
|
1011
|
+
}
|
|
1012
|
+
function parseWorkspaceAddIntent(json) {
|
|
1013
|
+
const value = JSON.parse(json);
|
|
1014
|
+
const workspaceId = String(value.workspaceId || "").toLowerCase();
|
|
1015
|
+
const resourceId = String(value.resourceId || "");
|
|
1016
|
+
const cloudHead = String(value.cloudHead || "");
|
|
1017
|
+
const destinationPath = String(value.destinationPath || "");
|
|
1018
|
+
const stagingPath = String(value.stagingPath || "");
|
|
1019
|
+
if (!UUID.test(workspaceId) || !resourceId || !/^[0-9a-f]{64}$/i.test(cloudHead)) {
|
|
1020
|
+
throw new Error("workspace Add intent identity is invalid");
|
|
1021
|
+
}
|
|
1022
|
+
if (!isAbsolute(destinationPath)
|
|
1023
|
+
|| stagingPath !== workspaceAddStagingPath(destinationPath, workspaceId)) {
|
|
1024
|
+
throw new Error(`workspace Add intent ${workspaceId} has invalid placement`);
|
|
1025
|
+
}
|
|
1026
|
+
const intent = {
|
|
1027
|
+
workspaceId,
|
|
1028
|
+
resourceId,
|
|
1029
|
+
cloudHead: cloudHead.toLowerCase(),
|
|
1030
|
+
destinationPath,
|
|
1031
|
+
stagingPath,
|
|
1032
|
+
records: snapshotFromRecords(Array.isArray(value.records) ? value.records : []).records,
|
|
1033
|
+
references: snapshotFromRecords(Array.isArray(value.references) ? value.references : []).records,
|
|
1034
|
+
};
|
|
1035
|
+
const { workspace } = workspaceAddSelection(intent);
|
|
1036
|
+
if (basename(destinationPath) !== workspace.name) {
|
|
1037
|
+
throw new Error(`workspace Add intent ${workspaceId} destination differs from its graph`);
|
|
1038
|
+
}
|
|
1039
|
+
return intent;
|
|
1040
|
+
}
|
|
1041
|
+
function readWorkspaceAddIntent(file, workspaceId) {
|
|
1042
|
+
if (!existsSync(file))
|
|
1043
|
+
return null;
|
|
1044
|
+
const database = initializeDatabase(file);
|
|
1045
|
+
try {
|
|
1046
|
+
const row = database.prepare("SELECT intent_json AS intentJson FROM workspace_add_intents WHERE workspace_uuid = ?").get(workspaceId);
|
|
1047
|
+
return row ? parseWorkspaceAddIntent(row.intentJson) : null;
|
|
1048
|
+
}
|
|
1049
|
+
finally {
|
|
1050
|
+
database.close();
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
function readWorkspaceAddIntents(file) {
|
|
1054
|
+
if (!existsSync(file))
|
|
1055
|
+
return [];
|
|
1056
|
+
const database = initializeDatabase(file);
|
|
1057
|
+
try {
|
|
1058
|
+
return database.prepare("SELECT intent_json AS intentJson FROM workspace_add_intents ORDER BY workspace_uuid").all().map(({ intentJson }) => parseWorkspaceAddIntent(intentJson));
|
|
1059
|
+
}
|
|
1060
|
+
finally {
|
|
1061
|
+
database.close();
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function persistWorkspaceAddIntent(file, proposed) {
|
|
1065
|
+
const database = initializeDatabase(file);
|
|
1066
|
+
try {
|
|
1067
|
+
let inserted;
|
|
1068
|
+
try {
|
|
1069
|
+
inserted = database.prepare(`
|
|
1070
|
+
INSERT INTO workspace_add_intents(workspace_uuid, destination_path, intent_json)
|
|
1071
|
+
VALUES (?, ?, ?)
|
|
1072
|
+
ON CONFLICT(workspace_uuid) DO NOTHING
|
|
1073
|
+
`).run(proposed.workspaceId, proposed.destinationPath, stableJson(proposed));
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
const destinationOwner = database.prepare("SELECT workspace_uuid AS workspaceId FROM workspace_add_intents WHERE destination_path = ?").get(proposed.destinationPath);
|
|
1077
|
+
if (destinationOwner) {
|
|
1078
|
+
throw new Error(`workspace Add destination belongs to pending workspace ${destinationOwner.workspaceId}`);
|
|
1079
|
+
}
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
if (inserted.changes === 1)
|
|
1083
|
+
return { intent: proposed, created: true };
|
|
1084
|
+
const existing = database.prepare("SELECT intent_json AS intentJson FROM workspace_add_intents WHERE workspace_uuid = ?").get(proposed.workspaceId);
|
|
1085
|
+
if (!existing)
|
|
1086
|
+
throw new Error(`workspace Add intent ${proposed.workspaceId} was not persisted`);
|
|
1087
|
+
return { intent: parseWorkspaceAddIntent(existing.intentJson), created: false };
|
|
1088
|
+
}
|
|
1089
|
+
finally {
|
|
1090
|
+
database.close();
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function deleteWorkspaceAddIntent(file, workspaceId) {
|
|
1094
|
+
const database = initializeDatabase(file);
|
|
1095
|
+
try {
|
|
1096
|
+
database.prepare("DELETE FROM workspace_add_intents WHERE workspace_uuid = ?").run(workspaceId);
|
|
1097
|
+
}
|
|
1098
|
+
finally {
|
|
1099
|
+
database.close();
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
939
1102
|
/** Register identity lookup is indexed and bounded: the command never walks
|
|
940
1103
|
* the complete entity table merely to discover that a directory is known. */
|
|
941
1104
|
function findKnownEntityId(file, absolutePath, deviceNumber, inode) {
|
|
@@ -1063,6 +1226,27 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
|
|
|
1063
1226
|
database.close();
|
|
1064
1227
|
}
|
|
1065
1228
|
}
|
|
1229
|
+
/** A same-filesystem rename preserves identity and bytes but may advance the
|
|
1230
|
+
* root directory's ctime. Refresh that one derived fingerprint before the
|
|
1231
|
+
* binding makes the committed graph observable. */
|
|
1232
|
+
function refreshRevealedRootEvidence(file, workspaceId, absolutePath) {
|
|
1233
|
+
const stats = statFingerprint(lstatSync(absolutePath));
|
|
1234
|
+
const database = initializeDatabase(file);
|
|
1235
|
+
try {
|
|
1236
|
+
const result = database.prepare(`
|
|
1237
|
+
UPDATE entities SET
|
|
1238
|
+
device_number = ?, inode = ?, byte_size = ?, modified_time_ms = ?,
|
|
1239
|
+
changed_time_ms = ?, filesystem_mode = ?
|
|
1240
|
+
WHERE uuid = ? AND absolute_path = ?
|
|
1241
|
+
`).run(stats.deviceNumber, stats.inode, stats.byteSize, stats.modifiedTimeMs, stats.changedTimeMs, stats.filesystemMode, workspaceId, absolutePath);
|
|
1242
|
+
if (result.changes !== 1) {
|
|
1243
|
+
throw new Error(`revealed workspace ${workspaceId} has no committed root row`);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
finally {
|
|
1247
|
+
database.close();
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1066
1250
|
function assertDatabaseIdentity(file, identity) {
|
|
1067
1251
|
const database = new Database(file, { readonly: true });
|
|
1068
1252
|
try {
|
|
@@ -1751,6 +1935,7 @@ function installCloudGround(input) {
|
|
|
1751
1935
|
async function materializeRecords(input) {
|
|
1752
1936
|
const { records, rootPath, cacheDir, bindingDir, readContent } = input;
|
|
1753
1937
|
const repositories = [];
|
|
1938
|
+
const repositoriesByUuid = new Map();
|
|
1754
1939
|
const fileArtifacts = new Map();
|
|
1755
1940
|
for (const record of records) {
|
|
1756
1941
|
if (record.type !== "file.text" && record.type !== "file.binary" && record.type !== "link")
|
|
@@ -1795,9 +1980,42 @@ async function materializeRecords(input) {
|
|
|
1795
1980
|
throw new Error(`cloud repository ${record.uuid} transport does not name its declared state`);
|
|
1796
1981
|
}
|
|
1797
1982
|
const applied = await applyRepositoryFiles(destination, chain);
|
|
1798
|
-
|
|
1983
|
+
const repository = { record, state: applied };
|
|
1984
|
+
repositories.push(repository);
|
|
1985
|
+
repositoriesByUuid.set(record.uuid, repository);
|
|
1799
1986
|
};
|
|
1800
1987
|
const paths = portablePaths(records, input.boundaryUUID ? new Set([input.boundaryUUID]) : new Set());
|
|
1988
|
+
const recordsByUuid = new Map(records.map((record) => [record.uuid, record]));
|
|
1989
|
+
const repositoryOwner = (record) => {
|
|
1990
|
+
let parentUUID = record.parentUUID;
|
|
1991
|
+
while (parentUUID !== null) {
|
|
1992
|
+
const repository = repositoriesByUuid.get(parentUUID);
|
|
1993
|
+
if (repository)
|
|
1994
|
+
return repository;
|
|
1995
|
+
const parent = recordsByUuid.get(parentUUID);
|
|
1996
|
+
if (!parent)
|
|
1997
|
+
throw new Error(`cloud entity ${record.uuid} has no parent`);
|
|
1998
|
+
parentUUID = parent.parentUUID;
|
|
1999
|
+
}
|
|
2000
|
+
return null;
|
|
2001
|
+
};
|
|
2002
|
+
const assertRepositoryBoundaryPath = (record, relativePath, destination) => {
|
|
2003
|
+
const owner = repositoryOwner(record);
|
|
2004
|
+
if (!owner)
|
|
2005
|
+
throw new Error(`cloud materialization conflicts with ${relativePath}`);
|
|
2006
|
+
const ownerPath = paths.get(owner.record.uuid);
|
|
2007
|
+
const repositoryPath = ownerPath
|
|
2008
|
+
? relativePath.slice(ownerPath.length + 1)
|
|
2009
|
+
: relativePath;
|
|
2010
|
+
const identity = owner.state.identity.find((entry) => entry.path === repositoryPath);
|
|
2011
|
+
if (!identity || identity.uuid !== record.uuid || identity.type !== record.type) {
|
|
2012
|
+
throw new Error(`repository boundary spine conflicts with ${relativePath}`);
|
|
2013
|
+
}
|
|
2014
|
+
const stats = lstatSync(destination);
|
|
2015
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
2016
|
+
throw new Error(`repository boundary path is not a directory: ${relativePath}`);
|
|
2017
|
+
}
|
|
2018
|
+
};
|
|
1801
2019
|
const ordered = [...records].sort((left, right) => {
|
|
1802
2020
|
const leftPath = paths.get(left.uuid);
|
|
1803
2021
|
const rightPath = paths.get(right.uuid);
|
|
@@ -1817,13 +2035,17 @@ async function materializeRecords(input) {
|
|
|
1817
2035
|
ensurePrivateDir(destination);
|
|
1818
2036
|
}
|
|
1819
2037
|
else if (record.type === "folder") {
|
|
1820
|
-
if (pathExists(destination))
|
|
1821
|
-
|
|
1822
|
-
|
|
2038
|
+
if (pathExists(destination)) {
|
|
2039
|
+
assertRepositoryBoundaryPath(record, relativePath, destination);
|
|
2040
|
+
}
|
|
2041
|
+
else {
|
|
2042
|
+
ensurePrivateDir(destination);
|
|
2043
|
+
}
|
|
1823
2044
|
}
|
|
1824
2045
|
else if (record.type === "repo.git") {
|
|
1825
|
-
if (pathExists(destination))
|
|
1826
|
-
|
|
2046
|
+
if (pathExists(destination)) {
|
|
2047
|
+
assertRepositoryBoundaryPath(record, relativePath, destination);
|
|
2048
|
+
}
|
|
1827
2049
|
await materializeRepository(record, destination);
|
|
1828
2050
|
}
|
|
1829
2051
|
else if (record.type === "reference") {
|
|
@@ -1973,94 +2195,171 @@ function installCloudWorkspaceReferences(input) {
|
|
|
1973
2195
|
return references;
|
|
1974
2196
|
}
|
|
1975
2197
|
async function installCloudWorkspace(input) {
|
|
1976
|
-
const { identity, userRoot, database, cacheDir, bindingDir, resourceId, workspace, reference, references, records, destinationParent, readContent, } = input;
|
|
2198
|
+
const { identity, userRoot, database, cacheDir, bindingDir, resourceId, workspace, reference, references, records, destinationParent, cloudHead, readContent, onStage, } = input;
|
|
1977
2199
|
const allLocalRows = readRows(database);
|
|
1978
2200
|
const existingBoundary = allLocalRows.find((row) => row.uuid === workspace.uuid);
|
|
1979
2201
|
const existing = existingBoundary
|
|
1980
2202
|
? descendantRows(allLocalRows, workspace.uuid)
|
|
1981
2203
|
: [];
|
|
2204
|
+
const pending = readWorkspaceAddIntent(database, workspace.uuid);
|
|
1982
2205
|
if (existing.length > 0) {
|
|
1983
2206
|
const root = existing.find((row) => row.uuid === workspace.uuid);
|
|
1984
|
-
if (!root
|
|
2207
|
+
if (!root) {
|
|
1985
2208
|
throw new Error(`local workspace ${workspace.uuid} has unresolved placement`);
|
|
1986
2209
|
}
|
|
2210
|
+
const expectedRecords = pending?.records ?? records;
|
|
1987
2211
|
const localRecords = existing.map((row) => Object.fromEntries(PORTABLE_FIELDS.map((field) => [field, row[field]])));
|
|
1988
|
-
if (!sameRecords(travelingRecords(localRecords),
|
|
2212
|
+
if (!sameRecords(travelingRecords(localRecords), expectedRecords)) {
|
|
1989
2213
|
throw new Error(`local workspace ${workspace.uuid} differs from its cloud graph`);
|
|
1990
2214
|
}
|
|
2215
|
+
if (pending && root.absolutePath !== pending.destinationPath) {
|
|
2216
|
+
throw new Error(`workspace ${workspace.uuid} rows differ from its pending destination`);
|
|
2217
|
+
}
|
|
2218
|
+
let revealed = false;
|
|
2219
|
+
if (!pathExists(root.absolutePath)) {
|
|
2220
|
+
if (!pending || !pathExists(pending.stagingPath)) {
|
|
2221
|
+
throw new Error(`local workspace ${workspace.uuid} has unresolved placement`);
|
|
2222
|
+
}
|
|
2223
|
+
renameSync(pending.stagingPath, pending.destinationPath);
|
|
2224
|
+
revealed = true;
|
|
2225
|
+
}
|
|
2226
|
+
else if (pending && pathExists(pending.stagingPath)) {
|
|
2227
|
+
throw new Error(`workspace ${workspace.uuid} has both staged and revealed ground`);
|
|
2228
|
+
}
|
|
2229
|
+
if (pending && pathExists(root.absolutePath)) {
|
|
2230
|
+
refreshRevealedRootEvidence(database, workspace.uuid, root.absolutePath);
|
|
2231
|
+
}
|
|
2232
|
+
if (revealed)
|
|
2233
|
+
await onStage?.("ground-revealed");
|
|
2234
|
+
ensureWorkspaceBinding({
|
|
2235
|
+
bindingDir,
|
|
2236
|
+
workspaceId: workspace.uuid,
|
|
2237
|
+
absolutePath: root.absolutePath,
|
|
2238
|
+
});
|
|
2239
|
+
if (pending)
|
|
2240
|
+
await onStage?.("binding-created");
|
|
2241
|
+
const activeReferences = pending?.references ?? references;
|
|
2242
|
+
const activeSelection = pending ? workspaceAddSelection(pending) : { workspace, reference };
|
|
1991
2243
|
const installedReferences = installCloudWorkspaceReferences({
|
|
1992
2244
|
identity,
|
|
1993
2245
|
userRoot,
|
|
1994
2246
|
database,
|
|
1995
2247
|
bindingDir,
|
|
1996
2248
|
resourceId,
|
|
1997
|
-
workspace,
|
|
1998
|
-
references,
|
|
2249
|
+
workspace: activeSelection.workspace,
|
|
2250
|
+
references: activeReferences,
|
|
1999
2251
|
});
|
|
2000
2252
|
return {
|
|
2001
2253
|
path: root.absolutePath,
|
|
2002
2254
|
rows: existing.length,
|
|
2003
2255
|
records: localRecords,
|
|
2004
|
-
reference,
|
|
2256
|
+
reference: activeSelection.reference,
|
|
2005
2257
|
references: installedReferences,
|
|
2006
|
-
alreadyMaterialized:
|
|
2258
|
+
alreadyMaterialized: pending === null,
|
|
2007
2259
|
};
|
|
2008
2260
|
}
|
|
2009
2261
|
const parent = realpathSync(resolve(destinationParent));
|
|
2010
2262
|
if (!statSync(parent).isDirectory())
|
|
2011
2263
|
throw new Error("destination must be a directory");
|
|
2012
2264
|
const destination = join(parent, workspace.name);
|
|
2265
|
+
if (dirname(destination) !== parent || basename(destination) !== workspace.name) {
|
|
2266
|
+
throw new Error("workspace name must be one path segment");
|
|
2267
|
+
}
|
|
2013
2268
|
const canonicalUserRoot = realpathSync(userRoot);
|
|
2014
2269
|
if (pathWithin(canonicalUserRoot, destination) || pathWithin(destination, canonicalUserRoot)) {
|
|
2015
2270
|
throw new Error("files add destination must be outside the Amalgm user ground");
|
|
2016
2271
|
}
|
|
2017
|
-
if (pathExists(destination))
|
|
2018
|
-
throw new Error(`workspace destination already exists: ${destination}`);
|
|
2019
2272
|
ensurePrivateDir(bindingDir);
|
|
2020
2273
|
const binding = join(bindingDir, workspace.uuid);
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
persistRows(database, identity, resourceId, workspace.uuid, rows);
|
|
2038
|
-
const installedReferences = installCloudWorkspaceReferences({
|
|
2039
|
-
identity,
|
|
2040
|
-
userRoot,
|
|
2041
|
-
database,
|
|
2042
|
-
bindingDir,
|
|
2274
|
+
let intent = pending;
|
|
2275
|
+
if (!intent) {
|
|
2276
|
+
const stagingPath = workspaceAddStagingPath(destination, workspace.uuid);
|
|
2277
|
+
if (pathExists(destination))
|
|
2278
|
+
throw new Error(`workspace destination already exists: ${destination}`);
|
|
2279
|
+
if (pathExists(stagingPath))
|
|
2280
|
+
throw new Error(`workspace staging path already exists: ${stagingPath}`);
|
|
2281
|
+
if (pathExists(binding))
|
|
2282
|
+
throw new Error(`workspace ${workspace.uuid} already has a local binding`);
|
|
2283
|
+
for (const bound of materializedWorkspaceBindings(bindingDir)) {
|
|
2284
|
+
if (pathWithin(bound.directory, destination) || pathWithin(destination, bound.directory)) {
|
|
2285
|
+
throw new Error(`files add destination overlaps materialized workspace ${bound.workspaceId}`);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
const written = persistWorkspaceAddIntent(database, {
|
|
2289
|
+
workspaceId: workspace.uuid,
|
|
2043
2290
|
resourceId,
|
|
2044
|
-
|
|
2045
|
-
|
|
2291
|
+
cloudHead,
|
|
2292
|
+
destinationPath: destination,
|
|
2293
|
+
stagingPath,
|
|
2294
|
+
records: snapshotFromRecords(records).records,
|
|
2295
|
+
references: snapshotFromRecords(references).records,
|
|
2046
2296
|
});
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
records: rows.map((row) => row.record),
|
|
2051
|
-
reference,
|
|
2052
|
-
references: installedReferences,
|
|
2053
|
-
alreadyMaterialized: false,
|
|
2054
|
-
};
|
|
2297
|
+
intent = written.intent;
|
|
2298
|
+
if (written.created)
|
|
2299
|
+
await onStage?.("intent-recorded");
|
|
2055
2300
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
if (pathExists(binding))
|
|
2059
|
-
rmSync(binding, { force: true });
|
|
2060
|
-
if (pathExists(destination))
|
|
2061
|
-
rmSync(destination, { recursive: true, force: true });
|
|
2062
|
-
throw error;
|
|
2301
|
+
if (intent.destinationPath !== destination) {
|
|
2302
|
+
throw new Error(`workspace ${workspace.uuid} already has a pending Add at ${intent.destinationPath}`);
|
|
2063
2303
|
}
|
|
2304
|
+
if (intent.resourceId !== resourceId) {
|
|
2305
|
+
throw new Error(`workspace ${workspace.uuid} Add intent belongs to a different registry`);
|
|
2306
|
+
}
|
|
2307
|
+
if (pathExists(intent.destinationPath)) {
|
|
2308
|
+
throw new Error(`workspace ${workspace.uuid} is visible without committed entity rows`);
|
|
2309
|
+
}
|
|
2310
|
+
if (pathExists(binding)) {
|
|
2311
|
+
throw new Error(`workspace ${workspace.uuid} has a binding without committed entity rows`);
|
|
2312
|
+
}
|
|
2313
|
+
if (pathExists(intent.stagingPath)) {
|
|
2314
|
+
rmSync(intent.stagingPath, { recursive: true, force: true });
|
|
2315
|
+
}
|
|
2316
|
+
const selection = workspaceAddSelection(intent);
|
|
2317
|
+
const materialized = await materializeRecords({
|
|
2318
|
+
records: intent.records,
|
|
2319
|
+
rootPath: intent.stagingPath,
|
|
2320
|
+
boundaryUUID: intent.workspaceId,
|
|
2321
|
+
cacheDir,
|
|
2322
|
+
bindingDir,
|
|
2323
|
+
readContent,
|
|
2324
|
+
});
|
|
2325
|
+
const rows = projectMaterializedGraph(materialized.rows, materialized.repositories.map((repository) => ({
|
|
2326
|
+
record: repository.record,
|
|
2327
|
+
identity: repository.state.identity,
|
|
2328
|
+
})), sha256Hex).map((row) => ({
|
|
2329
|
+
...row,
|
|
2330
|
+
absolutePath: row.relativePath
|
|
2331
|
+
? join(intent.destinationPath, ...row.relativePath.split("/"))
|
|
2332
|
+
: intent.destinationPath,
|
|
2333
|
+
}));
|
|
2334
|
+
await onStage?.("materialization-verified");
|
|
2335
|
+
persistRows(database, identity, intent.resourceId, intent.workspaceId, rows);
|
|
2336
|
+
await onStage?.("rows-committed");
|
|
2337
|
+
renameSync(intent.stagingPath, intent.destinationPath);
|
|
2338
|
+
refreshRevealedRootEvidence(database, intent.workspaceId, intent.destinationPath);
|
|
2339
|
+
await onStage?.("ground-revealed");
|
|
2340
|
+
ensureWorkspaceBinding({
|
|
2341
|
+
bindingDir,
|
|
2342
|
+
workspaceId: intent.workspaceId,
|
|
2343
|
+
absolutePath: intent.destinationPath,
|
|
2344
|
+
});
|
|
2345
|
+
await onStage?.("binding-created");
|
|
2346
|
+
const installedReferences = installCloudWorkspaceReferences({
|
|
2347
|
+
identity,
|
|
2348
|
+
userRoot,
|
|
2349
|
+
database,
|
|
2350
|
+
bindingDir,
|
|
2351
|
+
resourceId: intent.resourceId,
|
|
2352
|
+
workspace: selection.workspace,
|
|
2353
|
+
references: intent.references,
|
|
2354
|
+
});
|
|
2355
|
+
return {
|
|
2356
|
+
path: intent.destinationPath,
|
|
2357
|
+
rows: rows.length,
|
|
2358
|
+
records: rows.map((row) => row.record),
|
|
2359
|
+
reference: selection.reference,
|
|
2360
|
+
references: installedReferences,
|
|
2361
|
+
alreadyMaterialized: false,
|
|
2362
|
+
};
|
|
2064
2363
|
}
|
|
2065
2364
|
function readyFromFrame(frame) {
|
|
2066
2365
|
const bytes = Buffer.from(String(frame.snapshot_b64 || ""), "base64");
|