@amalgm/shell 0.1.36 → 0.1.38
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 +82 -31
- package/README.md +8 -1
- package/dist/boot.js +8 -0
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +4 -7
- package/dist/cli.js.map +1 -1
- package/dist/compose.js +9 -4
- package/dist/compose.js.map +1 -1
- package/dist/content-cache-host.d.ts +27 -1
- package/dist/content-cache-host.js +114 -5
- package/dist/content-cache-host.js.map +1 -1
- package/dist/external-proxy-host.js +1 -0
- package/dist/external-proxy-host.js.map +1 -1
- package/dist/files-register-host.d.ts +10 -2
- package/dist/files-register-host.js +11 -5
- package/dist/files-register-host.js.map +1 -1
- package/dist/git-repository-host.js +8 -2
- package/dist/git-repository-host.js.map +1 -1
- package/dist/machine-host.d.ts +5 -6
- package/dist/machine-host.js +17 -28
- package/dist/machine-host.js.map +1 -1
- package/dist/pty-host.js +33 -3
- package/dist/pty-host.js.map +1 -1
- package/dist/runtime-command-host.d.ts +1 -1
- package/dist/runtime-command-host.js +1 -0
- package/dist/runtime-command-host.js.map +1 -1
- package/dist/user-ground-host.d.ts +9 -0
- package/dist/user-ground-host.js +234 -95
- package/dist/user-ground-host.js.map +1 -1
- package/dist/watching/host.d.ts +3 -0
- package/dist/watching/host.js +158 -3
- package/dist/watching/host.js.map +1 -1
- package/dist/wire-client.d.ts +9 -0
- package/dist/wire-client.js +52 -3
- package/dist/wire-client.js.map +1 -1
- package/package.json +3 -3
|
@@ -13,4 +13,4 @@ export declare function publishRuntimeCommands(input: RuntimeCommandPublication)
|
|
|
13
13
|
/** Remove only the discovery document owned by this exact runtime token. */
|
|
14
14
|
export declare function unpublishRuntimeCommands(input: RuntimeCommandPublication): void;
|
|
15
15
|
/** Call the one running Files runtime. The CLI owns no Files effects. */
|
|
16
|
-
export declare function callRuntimeCommand(input: RuntimeCommandAddress, path: "/entities/
|
|
16
|
+
export declare function callRuntimeCommand(input: RuntimeCommandAddress, path: "/entities/register" | "/entities/add", body: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime-command-host.js","sourceRoot":"","sources":["../src/runtime-command-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEjC,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,UAAU,GAAG,YAAY,CAAC;AAyBhC,6EAA6E;AAC7E,MAAM,UAAU,sBAAsB,CAAC,KAAgC;IACrE,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,kBAAkB,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACrD,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC;IAC3E,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE;QACtB,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK,CAAC,SAAS;QAC3B,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;KAC/C,CAAC,CAAC;AACL,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,wBAAwB,CAAC,KAAgC;IACvE,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,QAAQ,CAAe,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,KAAK,CAAC,KAAK;QAAE,OAAO;IACvE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACvC,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAA4B,EAC5B,
|
|
1
|
+
{"version":3,"file":"runtime-command-host.js","sourceRoot":"","sources":["../src/runtime-command-host.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEjC,OAAO,EACL,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,UAAU,GAAG,YAAY,CAAC;AAyBhC,6EAA6E;AAC7E,MAAM,UAAU,sBAAsB,CAAC,KAAgC;IACrE,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,kBAAkB,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACrD,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC;IAC3E,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE;QACtB,aAAa,EAAE,KAAK;QACpB,UAAU,EAAE,KAAK,CAAC,SAAS;QAC3B,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE;KAC/C,CAAC,CAAC;AACL,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,wBAAwB,CAAC,KAAgC;IACvE,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAClC,IAAI,QAAQ,CAAe,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,KAAK,CAAC,KAAK;QAAE,OAAO;IACvE,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACvC,CAAC;AAED,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,KAA4B,EAC5B,IAA4C,EAC5C,IAA6B;IAE7B,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,KAAK,GAAG,QAAQ,CAAe,KAAK,CAAC,KAAK,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,kBAAkB,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IACrD,IAAI,CAAC,KAAK;WACL,KAAK,CAAC,aAAa,KAAK,KAAK;WAC7B,KAAK,CAAC,UAAU,KAAK,KAAK,CAAC,SAAS,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CAAC,qCAAqC,KAAK,CAAC,SAAS,uCAAuC,KAAK,CAAC,SAAS,KAAK,CAAC,CAAC;IACnI,CAAC;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAe,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,eAAe,CAAC,CAAC;IACpF,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC;IAClD,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CAAC,oBAAoB,IAAI,GAAG,IAAI,EAAE,EAAE;YACxD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,wBAAwB,EAAE,KAAK;gBAC/B,YAAY,EAAE,OAAO;aACtB;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,qCAAqC,KAAK,CAAC,SAAS,uCAAuC,KAAK,CAAC,SAAS,KAAK,CAAC,CAAC;IACnI,CAAC;IACD,IAAI,OAAO,GAAY,IAAI,CAAC;IAC5B,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,wDAAwD;IAC1D,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,OAAO,GAAG,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,IAAI,OAAO;YAC1E,CAAC,CAAC,MAAM,CAAE,OAA8B,CAAC,KAAK,CAAC;YAC/C,CAAC,CAAC,qBAAqB,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;IACD,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,OAAkC,CAAC;AAC5C,CAAC;AAED,SAAS,YAAY,CAAC,KAA4B;IAChD,MAAM,SAAS,GAAG,eAAe,CAC/B,KAAK,CAAC,QAAQ,EACd,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,WAAW,CAAC,EACrC,kBAAkB,CAAC,KAAK,CAAC,YAAY,CAAC,CACvC,CAAC;IACF,OAAO,EAAE,KAAK,EAAE,gBAAgB,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,gBAAgB,CAAC,SAAS,CAAC,EAAE,CAAC;AACpF,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC3B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,MAAM,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc,EAAE,KAAa;IAC7C,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3D,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,GAAG,KAAK,aAAa,CAAC,CAAC;IAClD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -82,6 +82,15 @@ export declare class UserGroundHost {
|
|
|
82
82
|
readonly register: FilesRegisterPorts;
|
|
83
83
|
readonly add: FilesAddPorts;
|
|
84
84
|
};
|
|
85
|
+
/** Finish the user-visible register journey inside the one persistent Files
|
|
86
|
+
* owner. The declaration remains durable if any later stage fails, and a
|
|
87
|
+
* retry reuses its UUID through filesCommandPorts().register. */
|
|
88
|
+
completeWorkspaceRegistration(workspaceId: string): Promise<{
|
|
89
|
+
readonly registered: true;
|
|
90
|
+
readonly entityCount: number;
|
|
91
|
+
readonly cloudHead: string;
|
|
92
|
+
readonly watch: WatchCoverageEvidence;
|
|
93
|
+
}>;
|
|
85
94
|
/** Make every observed filesystem change durable in SQLite, immutable
|
|
86
95
|
* content storage, and the official cloud head before returning. */
|
|
87
96
|
flush(): Promise<void>;
|
package/dist/user-ground-host.js
CHANGED
|
@@ -6,7 +6,7 @@ import { buildUserHomeManifest, buildUserManifest, liveMachineStateDir, scopedAm
|
|
|
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
|
-
import { ContentCacheDownload, hashContentFile } from "./content-cache-host.js";
|
|
9
|
+
import { ContentCacheDownload, captureContentFile, hashContentFile, } from "./content-cache-host.js";
|
|
10
10
|
import { decodeContentWireBytes, encodeContentWireBytes } from "./content-wire-codec.js";
|
|
11
11
|
import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, ensureWorkspaceReference, pathExists, pathWithin, referenceWorkspaceId, workspaceBindingDir, } from "./files-register-host.js";
|
|
12
12
|
import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile, hasGitMarker, } from "./git-repository-host.js";
|
|
@@ -117,6 +117,7 @@ export class UserGroundHost {
|
|
|
117
117
|
userRoot,
|
|
118
118
|
deviceId: identity.deviceId,
|
|
119
119
|
onReferenceDeclared: () => this.scheduleRescan(),
|
|
120
|
+
findKnownWorkspaceId: ({ absolutePath, deviceNumber, inode }) => findKnownEntityId(this.databasePath(identity), absolutePath, deviceNumber, inode),
|
|
120
121
|
});
|
|
121
122
|
const add = {
|
|
122
123
|
lookupRegistry: async () => {
|
|
@@ -151,6 +152,65 @@ export class UserGroundHost {
|
|
|
151
152
|
};
|
|
152
153
|
return { register, add };
|
|
153
154
|
}
|
|
155
|
+
/** Finish the user-visible register journey inside the one persistent Files
|
|
156
|
+
* owner. The declaration remains durable if any later stage fails, and a
|
|
157
|
+
* retry reuses its UUID through filesCommandPorts().register. */
|
|
158
|
+
async completeWorkspaceRegistration(workspaceId) {
|
|
159
|
+
const identity = this.activeIdentity;
|
|
160
|
+
if (!identity || !this.converged || !this.cloudState) {
|
|
161
|
+
throw new Error("Files register requires a converged authenticated machine");
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
this.ensureWatchers(identity);
|
|
165
|
+
assertHealthyWatch(this.watchHost.evidence(), workspaceId);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
throw stageError("Watch coverage", error);
|
|
169
|
+
}
|
|
170
|
+
this.watchHost.suspectAll();
|
|
171
|
+
this.watchDirty = false;
|
|
172
|
+
try {
|
|
173
|
+
await this.syncNow();
|
|
174
|
+
if (!this.cloudState.records.some((record) => record.uuid === workspaceId)) {
|
|
175
|
+
// A pre-existing scan may have taken its observation before this
|
|
176
|
+
// declaration. Preserve the declared root as suspicion and give it
|
|
177
|
+
// one scan of its own. Completion is evidence about this workspace,
|
|
178
|
+
// never a demand that unrelated live Watch traffic become silent.
|
|
179
|
+
this.watchHost.suspectAll();
|
|
180
|
+
this.watchDirty = false;
|
|
181
|
+
await this.syncNow();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const failedStage = filesPipelineStage(error);
|
|
186
|
+
if (failedStage) {
|
|
187
|
+
throw stageError(failedStage, error.cause ?? error);
|
|
188
|
+
}
|
|
189
|
+
throw stageError("entity registration or cloud publication", error);
|
|
190
|
+
}
|
|
191
|
+
let watch;
|
|
192
|
+
try {
|
|
193
|
+
this.ensureWatchers(identity);
|
|
194
|
+
watch = this.watchHost.evidence();
|
|
195
|
+
assertHealthyWatch(watch, workspaceId);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
throw stageError("Watch coverage", error);
|
|
199
|
+
}
|
|
200
|
+
const rows = readRows(this.databasePath(identity));
|
|
201
|
+
if (!rows.some((row) => row.uuid === workspaceId)) {
|
|
202
|
+
throw stageError("entity registration", new Error(`workspace ${workspaceId} was not recorded`));
|
|
203
|
+
}
|
|
204
|
+
if (!this.cloudState.records.some((record) => record.uuid === workspaceId)) {
|
|
205
|
+
throw stageError("cloud graph commit", new Error(`workspace ${workspaceId} is absent from the committed head`));
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
registered: true,
|
|
209
|
+
entityCount: descendantRows(rows, workspaceId).length,
|
|
210
|
+
cloudHead: this.cloudState.checksum,
|
|
211
|
+
watch,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
154
214
|
/** Make every observed filesystem change durable in SQLite, immutable
|
|
155
215
|
* content storage, and the official cloud head before returning. */
|
|
156
216
|
async flush() {
|
|
@@ -194,9 +254,9 @@ export class UserGroundHost {
|
|
|
194
254
|
}
|
|
195
255
|
}
|
|
196
256
|
async activateRuntimeTunnel(gatewayPort, runtimeToken) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
await this.wire.
|
|
257
|
+
// Files convergence and Watch/Detect own ground currency. Advertising an
|
|
258
|
+
// HTTP tunnel must never trigger or wait for a second filesystem pass.
|
|
259
|
+
await this.wire.advertiseRuntime({ gatewayPort, runtimeToken });
|
|
200
260
|
}
|
|
201
261
|
async stopRuntimeTunnel() {
|
|
202
262
|
this.wire.configureRuntime({});
|
|
@@ -220,16 +280,16 @@ export class UserGroundHost {
|
|
|
220
280
|
if (this.rescanTimer)
|
|
221
281
|
clearTimeout(this.rescanTimer);
|
|
222
282
|
this.rescanTimer = null;
|
|
223
|
-
|
|
283
|
+
// Full suspicion already contains every callback that could still be
|
|
284
|
+
// queued. Retire physical coverage before the final observation so native
|
|
285
|
+
// backends cannot continuously widen a finite shutdown pass. Changes
|
|
286
|
+
// after this cutoff belong to the next startup blind interval.
|
|
224
287
|
this.watchHost.close();
|
|
225
|
-
if (this.rescanTimer)
|
|
226
|
-
clearTimeout(this.rescanTimer);
|
|
227
|
-
this.rescanTimer = null;
|
|
228
288
|
await this.flushObservedChanges();
|
|
229
289
|
await this.wire.close();
|
|
230
290
|
}
|
|
231
291
|
userRoot(identity) {
|
|
232
|
-
return scopedAmalgmDir(this.options.amalgmRoot, identity.userEmail);
|
|
292
|
+
return resolve(scopedAmalgmDir(this.options.amalgmRoot, identity.userEmail));
|
|
233
293
|
}
|
|
234
294
|
cacheDir(identity) {
|
|
235
295
|
return join(liveMachineStateDir(this.userRoot(identity), identity.deviceId), "content");
|
|
@@ -585,14 +645,20 @@ export class UserGroundHost {
|
|
|
585
645
|
if (!state)
|
|
586
646
|
throw new Error("cloud state is unavailable for user-ground Watch");
|
|
587
647
|
const observations = this.watchHost.observations();
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
648
|
+
let localRecords;
|
|
649
|
+
try {
|
|
650
|
+
localRecords = await scanAndRegister({
|
|
651
|
+
identity,
|
|
652
|
+
userRoot: this.userRoot(identity),
|
|
653
|
+
database: this.databasePath(identity),
|
|
654
|
+
cacheDir: this.cacheDir(identity),
|
|
655
|
+
onScan: this.options.onDetectScan,
|
|
656
|
+
suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion.paths])),
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
catch (error) {
|
|
660
|
+
throw pipelineStageError("entity registration", error);
|
|
661
|
+
}
|
|
596
662
|
const snapshot = snapshotFromRecords(travelingRecords(mergeMaterializedRoots(state.records, localRecords)));
|
|
597
663
|
const checksum = sha256Hex(stableJson(snapshot));
|
|
598
664
|
if (checksum === state.checksum) {
|
|
@@ -634,24 +700,34 @@ export class UserGroundHost {
|
|
|
634
700
|
async drainOutbox(identity) {
|
|
635
701
|
for (const pending of readOutbox(this.databasePath(identity))) {
|
|
636
702
|
const snapshot = snapshotFromRecords(JSON.parse(pending.snapshotJson).records || []);
|
|
637
|
-
const
|
|
638
|
-
.
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
703
|
+
const locallyMaterializedIds = new Set(readRows(this.databasePath(identity))
|
|
704
|
+
.map((row) => row.uuid));
|
|
705
|
+
try {
|
|
706
|
+
await this.uploadSnapshotContent(identity, pending.resourceId, snapshot.records.filter((record) => locallyMaterializedIds.has(record.uuid)));
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
throw pipelineStageError("cloud upload", error);
|
|
710
|
+
}
|
|
711
|
+
let frame;
|
|
712
|
+
try {
|
|
713
|
+
frame = await this.wire.request({
|
|
714
|
+
type: "shared.mutation.submit",
|
|
715
|
+
envelope: {
|
|
716
|
+
resourceId: pending.resourceId,
|
|
717
|
+
authorityEpoch: pending.authorityEpoch,
|
|
718
|
+
mutationId: pending.mutationId,
|
|
719
|
+
deviceId: identity.deviceId,
|
|
720
|
+
baseVersion: pending.baseVersion,
|
|
721
|
+
contract: ENTITY_CLOUD_CONTRACT,
|
|
722
|
+
schemaVersion: ENTITY_CLOUD_SCHEMA_VERSION,
|
|
723
|
+
operationKind: "entity.snapshot.replace",
|
|
724
|
+
operation: { kind: "entity.snapshot.replace", snapshot },
|
|
725
|
+
},
|
|
726
|
+
}, ["shared.mutation.ack"]);
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
throw pipelineStageError("cloud graph commit", error);
|
|
730
|
+
}
|
|
655
731
|
this.cloudState = {
|
|
656
732
|
resourceId: pending.resourceId,
|
|
657
733
|
authorityEpoch: Number(frame.authority_epoch),
|
|
@@ -686,12 +762,48 @@ function normalizedIdentity(identity) {
|
|
|
686
762
|
deviceId: String(identity.deviceId).trim(),
|
|
687
763
|
};
|
|
688
764
|
}
|
|
765
|
+
function stageError(stage, error) {
|
|
766
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
767
|
+
return Object.assign(new Error(`Files register failed during ${stage}: ${message}`), {
|
|
768
|
+
code: `files_register_${stage.toLowerCase().replaceAll(/[^a-z0-9]+/g, "_")}`,
|
|
769
|
+
cause: error,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
function pipelineStageError(stage, error) {
|
|
773
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
774
|
+
return Object.assign(new Error(`Files pipeline failed during ${stage}: ${message}`), {
|
|
775
|
+
code: `files_pipeline_${stage.toLowerCase().replaceAll(/[^a-z0-9]+/g, "_")}`,
|
|
776
|
+
filesStage: stage,
|
|
777
|
+
cause: error,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
function filesPipelineStage(error) {
|
|
781
|
+
const stage = error instanceof Error
|
|
782
|
+
? error.filesStage
|
|
783
|
+
: null;
|
|
784
|
+
return typeof stage === "string" && stage ? stage : null;
|
|
785
|
+
}
|
|
786
|
+
function descendantRows(rows, rootUUID) {
|
|
787
|
+
const included = new Set([rootUUID]);
|
|
788
|
+
let changed = true;
|
|
789
|
+
while (changed) {
|
|
790
|
+
changed = false;
|
|
791
|
+
for (const row of rows) {
|
|
792
|
+
if (row.parentUUID !== null && included.has(row.parentUUID) && !included.has(row.uuid)) {
|
|
793
|
+
included.add(row.uuid);
|
|
794
|
+
changed = true;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return rows.filter((row) => included.has(row.uuid));
|
|
799
|
+
}
|
|
689
800
|
function assertHealthyWatch(evidence, rootId) {
|
|
690
801
|
const root = evidence.roots.find((candidate) => candidate.rootId === rootId);
|
|
691
802
|
const content = root?.contentOwner
|
|
692
803
|
? evidence.content.find((candidate) => candidate.directory === root.contentOwner)
|
|
693
804
|
: undefined;
|
|
694
|
-
const
|
|
805
|
+
const ownsContent = root?.contentOwner === root?.directory;
|
|
806
|
+
const address = ownsContent
|
|
695
807
|
? evidence.addresses.find((candidate) => candidate.rootIds.includes(rootId))
|
|
696
808
|
: undefined;
|
|
697
809
|
if (evidence.health.state !== "healthy"
|
|
@@ -699,7 +811,7 @@ function assertHealthyWatch(evidence, rootId) {
|
|
|
699
811
|
|| evidence.health.addressHandles !== evidence.addresses.length
|
|
700
812
|
|| !root?.materialized
|
|
701
813
|
|| !content?.rootIds.includes(rootId)
|
|
702
|
-
|| !address) {
|
|
814
|
+
|| (ownsContent && !address)) {
|
|
703
815
|
throw new Error(`Watch coverage is not healthy for workspace ${rootId}: ${evidence.health.reason ?? "incomplete handle evidence"}`);
|
|
704
816
|
}
|
|
705
817
|
}
|
|
@@ -772,6 +884,10 @@ function initializeDatabase(file) {
|
|
|
772
884
|
snapshot_json TEXT NOT NULL,
|
|
773
885
|
created_at TEXT NOT NULL
|
|
774
886
|
);
|
|
887
|
+
CREATE INDEX IF NOT EXISTS entities_by_absolute_path
|
|
888
|
+
ON entities(absolute_path);
|
|
889
|
+
CREATE INDEX IF NOT EXISTS entities_by_physical_identity
|
|
890
|
+
ON entities(device_number, inode);
|
|
775
891
|
`);
|
|
776
892
|
const currentEntityColumns = new Set(database.prepare("PRAGMA table_info(entities)").all()
|
|
777
893
|
.map(({ name }) => name));
|
|
@@ -809,6 +925,25 @@ function readRows(file) {
|
|
|
809
925
|
database.close();
|
|
810
926
|
}
|
|
811
927
|
}
|
|
928
|
+
/** Register identity lookup is indexed and bounded: the command never walks
|
|
929
|
+
* the complete entity table merely to discover that a directory is known. */
|
|
930
|
+
function findKnownEntityId(file, absolutePath, deviceNumber, inode) {
|
|
931
|
+
if (!existsSync(file))
|
|
932
|
+
return null;
|
|
933
|
+
const database = initializeDatabase(file);
|
|
934
|
+
try {
|
|
935
|
+
const exact = database.prepare("SELECT uuid FROM entities WHERE absolute_path = ? LIMIT 2").all(absolutePath);
|
|
936
|
+
const physical = exact.length > 0 ? exact : database.prepare("SELECT uuid FROM entities WHERE device_number = ? AND inode = ? LIMIT 2").all(deviceNumber, inode);
|
|
937
|
+
const ids = [...new Set(physical.map(({ uuid }) => uuid))];
|
|
938
|
+
if (ids.length > 1) {
|
|
939
|
+
throw new Error(`entity graph has ambiguous identity for ${absolutePath}`);
|
|
940
|
+
}
|
|
941
|
+
return ids[0] ?? null;
|
|
942
|
+
}
|
|
943
|
+
finally {
|
|
944
|
+
database.close();
|
|
945
|
+
}
|
|
946
|
+
}
|
|
812
947
|
function readOutbox(file) {
|
|
813
948
|
if (!existsSync(file))
|
|
814
949
|
return [];
|
|
@@ -906,7 +1041,7 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
|
|
|
906
1041
|
replaceIdentity.run(identity.userId, identity.userEmail, identity.deviceId);
|
|
907
1042
|
for (const old of previousRows) {
|
|
908
1043
|
if (!currentUuids.has(old.uuid))
|
|
909
|
-
remove.run(old.uuid, resourceId, rootUUID);
|
|
1044
|
+
remove.run(old.uuid, resourceId, old.rootUUID);
|
|
910
1045
|
}
|
|
911
1046
|
for (const row of changed)
|
|
912
1047
|
upsert.run(row.record.uuid, resourceId, rootUUID, row.record.type, row.record.parentUUID, row.record.name, row.record.status, row.record.version, row.record.payloadVersion, row.record.transportVersion, row.relativePath, row.absolutePath, row.deviceNumber, row.inode, row.byteSize, row.modifiedTimeMs, row.changedTimeMs, row.filesystemMode, row.contentVerifiedAtMs);
|
|
@@ -972,9 +1107,6 @@ function createDeclaredHome(input) {
|
|
|
972
1107
|
}
|
|
973
1108
|
}
|
|
974
1109
|
}
|
|
975
|
-
function fileType(bytes) {
|
|
976
|
-
return classifyFile({ binary: bytes.includes(0) });
|
|
977
|
-
}
|
|
978
1110
|
function slashPath(path) {
|
|
979
1111
|
return path.split(sep).join("/");
|
|
980
1112
|
}
|
|
@@ -1041,8 +1173,13 @@ async function scanAndRegister(input) {
|
|
|
1041
1173
|
const ignoreFile = join(userRoot, ".amalgmignore");
|
|
1042
1174
|
const policy = createUserGroundEnrollmentPolicy(existsSync(ignoreFile) ? readFileSync(ignoreFile, "utf8") : "");
|
|
1043
1175
|
const bindingDir = workspaceBindingDir(userRoot, identity.deviceId);
|
|
1044
|
-
const
|
|
1176
|
+
const boundRootEntries = materializedWorkspaceBindings(bindingDir);
|
|
1177
|
+
const boundRoots = new Map(boundRootEntries
|
|
1045
1178
|
.map(({ workspaceId, directory }) => [workspaceId, directory]));
|
|
1179
|
+
const outerBoundRoots = boundRootEntries.filter((candidate) => !boundRootEntries.some((other) => other.workspaceId !== candidate.workspaceId
|
|
1180
|
+
&& pathWithin(other.directory, candidate.directory)));
|
|
1181
|
+
const registeredBoundaries = new Map(boundRootEntries
|
|
1182
|
+
.map(({ workspaceId, directory }) => [resolve(directory), workspaceId]));
|
|
1046
1183
|
const suspicionFor = (root) => {
|
|
1047
1184
|
if (!suspicions || !suspicions.has(root))
|
|
1048
1185
|
return suspicions ? [] : null;
|
|
@@ -1066,7 +1203,7 @@ async function scanAndRegister(input) {
|
|
|
1066
1203
|
repositoryCaptures: 0,
|
|
1067
1204
|
};
|
|
1068
1205
|
const started = performance.now();
|
|
1069
|
-
const result = scanRoot({ ...parameters, evidence: counters });
|
|
1206
|
+
const result = await scanRoot({ ...parameters, evidence: counters });
|
|
1070
1207
|
onScan?.({
|
|
1071
1208
|
rootId: parameters.rootUUID,
|
|
1072
1209
|
directory: parameters.rootPath,
|
|
@@ -1099,35 +1236,26 @@ async function scanAndRegister(input) {
|
|
|
1099
1236
|
});
|
|
1100
1237
|
if (!core.transferable)
|
|
1101
1238
|
deferredRootIds.add(coreUUID);
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
targetRoots.add(row.uuid);
|
|
1106
|
-
}
|
|
1107
|
-
for (const workspaceId of boundRoots.keys())
|
|
1108
|
-
targetRoots.add(workspaceId);
|
|
1109
|
-
for (const workspaceId of targetRoots) {
|
|
1110
|
-
const rootPath = boundRoots.get(workspaceId);
|
|
1111
|
-
if (!rootPath) {
|
|
1112
|
-
// Missing external ground is undecided. Preserve its last witnessed rows
|
|
1113
|
-
// so a move can be rescued; absence is never permission to delete it.
|
|
1114
|
-
continue;
|
|
1115
|
-
}
|
|
1116
|
-
const existingRoot = existing.find((row) => row.uuid === workspaceId && row.parentUUID === null);
|
|
1117
|
-
const existingWorkspaceRows = existing.filter((row) => row.rootUUID === workspaceId);
|
|
1239
|
+
for (const { workspaceId, directory: rootPath } of outerBoundRoots) {
|
|
1240
|
+
const existingRoot = existing.find((row) => row.uuid === workspaceId);
|
|
1241
|
+
const existingWorkspaceRows = existing.filter((row) => row.resourceId === resourceId && pathWithin(rootPath, row.absolutePath));
|
|
1118
1242
|
const scanned = await scan({
|
|
1119
1243
|
identity,
|
|
1120
1244
|
resourceId,
|
|
1121
1245
|
rootUUID: workspaceId,
|
|
1122
1246
|
rootName: existingRoot?.name || basename(rootPath),
|
|
1123
1247
|
rootPath,
|
|
1124
|
-
rootType:
|
|
1248
|
+
rootType: existingRoot?.parentUUID
|
|
1249
|
+
? classifyDirectory({ repository: hasGitMarker(rootPath) })
|
|
1250
|
+
: classifyRegisteredRoot({ repository: hasGitMarker(rootPath) }),
|
|
1251
|
+
rootParentUUID: existingRoot?.parentUUID ?? null,
|
|
1125
1252
|
database,
|
|
1126
1253
|
cacheDir,
|
|
1127
1254
|
policy: () => true,
|
|
1128
1255
|
bindingDir,
|
|
1129
1256
|
suspects: existingWorkspaceRows.length === 0 ? null : suspicionFor(rootPath),
|
|
1130
1257
|
existingRows: existingWorkspaceRows,
|
|
1258
|
+
registeredBoundaries,
|
|
1131
1259
|
});
|
|
1132
1260
|
if (!scanned.transferable)
|
|
1133
1261
|
deferredRootIds.add(workspaceId);
|
|
@@ -1136,11 +1264,17 @@ async function scanAndRegister(input) {
|
|
|
1136
1264
|
.filter((row) => !deferredRootIds.has(row.rootUUID))
|
|
1137
1265
|
.map(portableRecord);
|
|
1138
1266
|
}
|
|
1139
|
-
function scanRoot(input) {
|
|
1267
|
+
async function scanRoot(input) {
|
|
1140
1268
|
const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, } = input;
|
|
1141
|
-
const existingRows =
|
|
1142
|
-
.filter((row) => row.resourceId === resourceId && row.rootUUID === rootUUID);
|
|
1143
|
-
const existingByPath = new Map(existingRows.
|
|
1269
|
+
const existingRows = input.existingRows
|
|
1270
|
+
?? readRows(database).filter((row) => row.resourceId === resourceId && row.rootUUID === rootUUID);
|
|
1271
|
+
const existingByPath = new Map(existingRows.flatMap((row) => {
|
|
1272
|
+
if (!pathWithin(rootPath, row.absolutePath))
|
|
1273
|
+
return [];
|
|
1274
|
+
const local = slashPath(relative(rootPath, row.absolutePath));
|
|
1275
|
+
return [[local, row]];
|
|
1276
|
+
}));
|
|
1277
|
+
const registeredBoundaries = input.registeredBoundaries ?? new Map();
|
|
1144
1278
|
const completeRepositoryEvidence = existingRows.every((row) => row.type !== "repo.git"
|
|
1145
1279
|
|| (Boolean(row.payloadVersion) && Boolean(row.transportVersion)));
|
|
1146
1280
|
if (suspects !== null
|
|
@@ -1174,7 +1308,7 @@ function scanRoot(input) {
|
|
|
1174
1308
|
entries.push({
|
|
1175
1309
|
uuid: rootUUID,
|
|
1176
1310
|
type: rootType,
|
|
1177
|
-
parentUUID: null,
|
|
1311
|
+
parentUUID: input.rootParentUUID ?? null,
|
|
1178
1312
|
name: rootName,
|
|
1179
1313
|
relativePath: "",
|
|
1180
1314
|
absolutePath: rootPath,
|
|
@@ -1207,7 +1341,7 @@ function scanRoot(input) {
|
|
|
1207
1341
|
}
|
|
1208
1342
|
return [...new Set(paths)].sort();
|
|
1209
1343
|
};
|
|
1210
|
-
const visit = (directory, parentUUID, parentType, base = "", activeRepository = parentType === "repo.git"
|
|
1344
|
+
const visit = async (directory, parentUUID, parentType, base = "", activeRepository = parentType === "repo.git"
|
|
1211
1345
|
? { root: directory, evidencePaths: repositoryEvidencePaths(directory), evidence: null }
|
|
1212
1346
|
: null) => {
|
|
1213
1347
|
const children = readdirSync(directory, { withFileTypes: true })
|
|
@@ -1222,7 +1356,13 @@ function scanRoot(input) {
|
|
|
1222
1356
|
const stats = lstatSync(absolutePath);
|
|
1223
1357
|
evidence && (evidence.entries += 1);
|
|
1224
1358
|
const existing = existingByPath.get(relativePath);
|
|
1225
|
-
const
|
|
1359
|
+
const registeredBoundaryId = stats.isDirectory()
|
|
1360
|
+
? registeredBoundaries.get(resolve(absolutePath))
|
|
1361
|
+
: undefined;
|
|
1362
|
+
if (registeredBoundaryId && existing && existing.uuid !== registeredBoundaryId) {
|
|
1363
|
+
throw new Error(`registered boundary ${absolutePath} conflicts with entity ${existing.uuid}`);
|
|
1364
|
+
}
|
|
1365
|
+
const uuid = registeredBoundaryId || existing?.uuid || randomUUID();
|
|
1226
1366
|
const stableDuringCompleteCatchUp = suspects === null
|
|
1227
1367
|
&& existing !== undefined
|
|
1228
1368
|
&& reusableDuringCompleteCatchUp(existing, stats);
|
|
@@ -1294,16 +1434,9 @@ function scanRoot(input) {
|
|
|
1294
1434
|
}
|
|
1295
1435
|
else {
|
|
1296
1436
|
evidence && (evidence.contentReads += 1);
|
|
1297
|
-
const
|
|
1298
|
-
type =
|
|
1299
|
-
payloadVersion =
|
|
1300
|
-
if (!activeRepository)
|
|
1301
|
-
immutableWriteArtifact(cacheDir, {
|
|
1302
|
-
entityId: uuid,
|
|
1303
|
-
entityType: type,
|
|
1304
|
-
kind: "file",
|
|
1305
|
-
contentHash: payloadVersion,
|
|
1306
|
-
}, bytes);
|
|
1437
|
+
const captured = await captureContentFile(absolutePath, stats, activeRepository ? null : cacheDir);
|
|
1438
|
+
type = classifyFile({ binary: captured.binary });
|
|
1439
|
+
payloadVersion = captured.contentHash;
|
|
1307
1440
|
}
|
|
1308
1441
|
}
|
|
1309
1442
|
else {
|
|
@@ -1324,7 +1457,7 @@ function scanRoot(input) {
|
|
|
1324
1457
|
...statFingerprint(stats),
|
|
1325
1458
|
});
|
|
1326
1459
|
if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
1327
|
-
visit(absolutePath, uuid, type, relativePath, type === "repo.git"
|
|
1460
|
+
await visit(absolutePath, uuid, type, relativePath, type === "repo.git"
|
|
1328
1461
|
? {
|
|
1329
1462
|
root: absolutePath,
|
|
1330
1463
|
evidencePaths: repositoryEvidencePaths(absolutePath),
|
|
@@ -1334,7 +1467,7 @@ function scanRoot(input) {
|
|
|
1334
1467
|
}
|
|
1335
1468
|
}
|
|
1336
1469
|
};
|
|
1337
|
-
visit(rootPath, rootUUID, rootType);
|
|
1470
|
+
await visit(rootPath, rootUUID, rootType);
|
|
1338
1471
|
const children = new Map();
|
|
1339
1472
|
for (const entry of entries) {
|
|
1340
1473
|
if (entry.parentUUID === null)
|
|
@@ -1486,7 +1619,7 @@ function scanRoot(input) {
|
|
|
1486
1619
|
transferable: true,
|
|
1487
1620
|
};
|
|
1488
1621
|
}
|
|
1489
|
-
function portablePaths(records) {
|
|
1622
|
+
function portablePaths(records, boundaryUUIDs = new Set()) {
|
|
1490
1623
|
const byUuid = new Map(records.map((record) => [record.uuid, record]));
|
|
1491
1624
|
const memo = new Map();
|
|
1492
1625
|
const resolving = new Set();
|
|
@@ -1498,7 +1631,7 @@ function portablePaths(records) {
|
|
|
1498
1631
|
throw new Error("cloud user-ground graph has a parent cycle");
|
|
1499
1632
|
resolving.add(record.uuid);
|
|
1500
1633
|
let path = "";
|
|
1501
|
-
if (record.parentUUID !== null) {
|
|
1634
|
+
if (!boundaryUUIDs.has(record.uuid) && record.parentUUID !== null) {
|
|
1502
1635
|
const parent = byUuid.get(record.parentUUID);
|
|
1503
1636
|
if (!parent)
|
|
1504
1637
|
throw new Error(`cloud entity ${record.uuid} has no parent`);
|
|
@@ -1536,22 +1669,15 @@ function recordRootResolver(records) {
|
|
|
1536
1669
|
};
|
|
1537
1670
|
return rootOf;
|
|
1538
1671
|
}
|
|
1539
|
-
/** Select the records whose root is authored by this machine. A full cloud
|
|
1540
|
-
* snapshot also carries untouched cloud-only sibling roots; preserving their
|
|
1541
|
-
* rows never means this machine owns or must re-upload their bytes. */
|
|
1542
|
-
function recordsWithinRoots(records, rootIds) {
|
|
1543
|
-
const rootOf = recordRootResolver(records);
|
|
1544
|
-
return records.filter((record) => {
|
|
1545
|
-
const root = rootOf(record);
|
|
1546
|
-
return root !== null && rootIds.has(root);
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
1672
|
function mergeMaterializedRoots(cloudRecords, localRecords) {
|
|
1550
1673
|
const localRoots = new Set(localRecords
|
|
1551
1674
|
.filter((record) => record.parentUUID === null)
|
|
1552
1675
|
.map((record) => record.uuid));
|
|
1676
|
+
const localIds = new Set(localRecords.map((record) => record.uuid));
|
|
1553
1677
|
const rootOf = recordRootResolver(cloudRecords);
|
|
1554
1678
|
const preserved = cloudRecords.filter((record) => {
|
|
1679
|
+
if (localIds.has(record.uuid))
|
|
1680
|
+
return false;
|
|
1555
1681
|
const root = rootOf(record);
|
|
1556
1682
|
return root === null || !localRoots.has(root);
|
|
1557
1683
|
});
|
|
@@ -1660,7 +1786,7 @@ async function materializeRecords(input) {
|
|
|
1660
1786
|
const applied = await applyRepositoryFiles(destination, chain);
|
|
1661
1787
|
repositories.push({ record, state: applied });
|
|
1662
1788
|
};
|
|
1663
|
-
const paths = portablePaths(records);
|
|
1789
|
+
const paths = portablePaths(records, input.boundaryUUID ? new Set([input.boundaryUUID]) : new Set());
|
|
1664
1790
|
const ordered = [...records].sort((left, right) => {
|
|
1665
1791
|
const leftPath = paths.get(left.uuid);
|
|
1666
1792
|
const rightPath = paths.get(right.uuid);
|
|
@@ -1673,7 +1799,7 @@ async function materializeRecords(input) {
|
|
|
1673
1799
|
if (!pathWithin(rootPath, destination)) {
|
|
1674
1800
|
throw new Error(`cloud entity path escapes its workspace: ${relativePath}`);
|
|
1675
1801
|
}
|
|
1676
|
-
if (
|
|
1802
|
+
if (relativePath === "") {
|
|
1677
1803
|
if (record.type === "repo.git")
|
|
1678
1804
|
await materializeRepository(record, destination);
|
|
1679
1805
|
else
|
|
@@ -1837,7 +1963,11 @@ function installCloudWorkspaceReferences(input) {
|
|
|
1837
1963
|
}
|
|
1838
1964
|
async function installCloudWorkspace(input) {
|
|
1839
1965
|
const { identity, userRoot, database, cacheDir, bindingDir, resourceId, workspace, reference, references, records, destinationParent, readContent, } = input;
|
|
1840
|
-
const
|
|
1966
|
+
const allLocalRows = readRows(database);
|
|
1967
|
+
const existingBoundary = allLocalRows.find((row) => row.uuid === workspace.uuid);
|
|
1968
|
+
const existing = existingBoundary
|
|
1969
|
+
? descendantRows(allLocalRows, workspace.uuid)
|
|
1970
|
+
: [];
|
|
1841
1971
|
if (existing.length > 0) {
|
|
1842
1972
|
const root = existing.find((row) => row.uuid === workspace.uuid);
|
|
1843
1973
|
if (!root || !pathExists(root.absolutePath)) {
|
|
@@ -1869,6 +1999,10 @@ async function installCloudWorkspace(input) {
|
|
|
1869
1999
|
if (!statSync(parent).isDirectory())
|
|
1870
2000
|
throw new Error("destination must be a directory");
|
|
1871
2001
|
const destination = join(parent, workspace.name);
|
|
2002
|
+
const canonicalUserRoot = realpathSync(userRoot);
|
|
2003
|
+
if (pathWithin(canonicalUserRoot, destination) || pathWithin(destination, canonicalUserRoot)) {
|
|
2004
|
+
throw new Error("files add destination must be outside the Amalgm user ground");
|
|
2005
|
+
}
|
|
1872
2006
|
if (pathExists(destination))
|
|
1873
2007
|
throw new Error(`workspace destination already exists: ${destination}`);
|
|
1874
2008
|
ensurePrivateDir(bindingDir);
|
|
@@ -1877,7 +2011,12 @@ async function installCloudWorkspace(input) {
|
|
|
1877
2011
|
throw new Error(`workspace ${workspace.uuid} already has a local binding`);
|
|
1878
2012
|
try {
|
|
1879
2013
|
const materialized = await materializeRecords({
|
|
1880
|
-
records,
|
|
2014
|
+
records,
|
|
2015
|
+
rootPath: destination,
|
|
2016
|
+
boundaryUUID: workspace.uuid,
|
|
2017
|
+
cacheDir,
|
|
2018
|
+
bindingDir,
|
|
2019
|
+
readContent,
|
|
1881
2020
|
});
|
|
1882
2021
|
const rows = projectMaterializedGraph(materialized.rows, materialized.repositories.map((repository) => ({
|
|
1883
2022
|
record: repository.record,
|