@amalgm/shell 0.1.29 → 0.1.30
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 +41 -22
- package/README.md +9 -2
- package/dist/cli.js +12 -7
- package/dist/cli.js.map +1 -1
- package/dist/git-repository-host.d.ts +3 -4
- package/dist/git-repository-host.js +11 -13
- package/dist/git-repository-host.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/machine-host.d.ts +11 -4
- package/dist/machine-host.js +75 -39
- package/dist/machine-host.js.map +1 -1
- package/dist/runtime-command-host.d.ts +16 -0
- package/dist/runtime-command-host.js +86 -0
- package/dist/runtime-command-host.js.map +1 -0
- package/dist/user-ground-host.d.ts +14 -8
- package/dist/user-ground-host.js +222 -173
- package/dist/user-ground-host.js.map +1 -1
- package/dist/watching/host.d.ts +99 -0
- package/dist/watching/host.js +543 -0
- package/dist/watching/host.js.map +1 -0
- package/dist/watching/index.d.ts +2 -0
- package/dist/watching/index.js +2 -0
- package/dist/watching/index.js.map +1 -0
- package/package.json +2 -2
package/dist/user-ground-host.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync,
|
|
2
|
+
import { existsSync, lstatSync, realpathSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { open } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, extname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { buildUserHomeManifest, buildUserManifest, liveMachineStateDir, scopedAmalgmDir, shippedUserHomeDeclaration, } from "@amalgm/core/identity";
|
|
6
|
-
import { CHUNK_BYTES, CONTENT_CONTRACT, ENTITY_CLOUD_CONTRACT, ENTITY_CLOUD_SCHEMA_VERSION, artifactForRecord, canonicalVersion, checkContentManifest, classifyDirectory, classifyFile, classifyRegisteredRoot, convergeUserGround,
|
|
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, hashContentFile } from "./content-cache-host.js";
|
|
10
10
|
import { decodeContentWireBytes, encodeContentWireBytes } from "./content-wire-codec.js";
|
|
11
11
|
import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, pathExists, pathWithin, referenceWorkspaceId, workspaceBindingDir, } from "./files-register-host.js";
|
|
12
|
-
import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile,
|
|
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";
|
|
15
|
+
import { NodeWatchHost, } from "./watching/index.js";
|
|
15
16
|
import { WireClient, WireRequestError } from "./wire-client.js";
|
|
16
17
|
const PORTABLE_FIELDS = [
|
|
17
18
|
"uuid", "type", "parentUUID", "name", "status", "version",
|
|
@@ -25,7 +26,7 @@ const FILE_BATCH_DOWNLOAD_CONCURRENCY = 2;
|
|
|
25
26
|
export class UserGroundHost {
|
|
26
27
|
options;
|
|
27
28
|
wire;
|
|
28
|
-
|
|
29
|
+
watchHost;
|
|
29
30
|
activeIdentity = null;
|
|
30
31
|
rescanTimer = null;
|
|
31
32
|
watchDirty = false;
|
|
@@ -41,6 +42,11 @@ export class UserGroundHost {
|
|
|
41
42
|
token: options.tunnelToken,
|
|
42
43
|
runtimeVersion: options.runtimeVersion,
|
|
43
44
|
});
|
|
45
|
+
this.watchHost = new NodeWatchHost({
|
|
46
|
+
open: options.openWatch,
|
|
47
|
+
onSignal: () => this.scheduleRescan(),
|
|
48
|
+
onHealth: options.onWatchHealth,
|
|
49
|
+
});
|
|
44
50
|
this.port = {
|
|
45
51
|
converge: async (identity) => {
|
|
46
52
|
this.converged = false;
|
|
@@ -73,18 +79,30 @@ export class UserGroundHost {
|
|
|
73
79
|
rows(identity) {
|
|
74
80
|
return readRows(this.databasePath(identity));
|
|
75
81
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
watchHealth() {
|
|
83
|
+
return this.watchHost.health();
|
|
84
|
+
}
|
|
85
|
+
watchEvidence() {
|
|
86
|
+
return this.watchHost.evidence();
|
|
87
|
+
}
|
|
88
|
+
coverWorkspaceEvidence(workspaceId) {
|
|
89
|
+
if (!this.activeIdentity || !this.converged) {
|
|
90
|
+
throw new Error("Watch coverage requires a running authenticated runtime");
|
|
84
91
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
const evidence = this.ensureWatchers(this.activeIdentity);
|
|
93
|
+
const watch = this.watchHost.evidence();
|
|
94
|
+
if (evidence.state !== "healthy") {
|
|
95
|
+
throw new Error(`Watch coverage is degraded: ${evidence.reason ?? "unknown failure"}`);
|
|
96
|
+
}
|
|
97
|
+
assertHealthyWatch(watch, workspaceId);
|
|
98
|
+
return watch;
|
|
99
|
+
}
|
|
100
|
+
/** Add already projects authoritative rows; this proves that the same
|
|
101
|
+
* long-lived runtime now owns their healthy ongoing coverage. */
|
|
102
|
+
addedWorkspaceEvidence(workspaceId) {
|
|
103
|
+
const watch = this.watchHost.evidence();
|
|
104
|
+
assertHealthyWatch(watch, workspaceId);
|
|
105
|
+
return watch;
|
|
88
106
|
}
|
|
89
107
|
/** Host effects for Live-owned Files commands. The shell CLI receives these
|
|
90
108
|
* ports but never decides Files behavior itself. */
|
|
@@ -120,8 +138,8 @@ export class UserGroundHost {
|
|
|
120
138
|
readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact),
|
|
121
139
|
}),
|
|
122
140
|
coverWorkspace: async () => {
|
|
123
|
-
this.ensureWatchers(identity);
|
|
124
|
-
return { active: true, roots:
|
|
141
|
+
const health = this.ensureWatchers(identity);
|
|
142
|
+
return { active: true, roots: health.contentHandles };
|
|
125
143
|
},
|
|
126
144
|
};
|
|
127
145
|
return { register, add };
|
|
@@ -132,8 +150,7 @@ export class UserGroundHost {
|
|
|
132
150
|
if (!this.converged || !this.activeIdentity || !this.cloudState)
|
|
133
151
|
return;
|
|
134
152
|
const identity = this.activeIdentity;
|
|
135
|
-
|
|
136
|
-
watcher.suspicion.suspectAll();
|
|
153
|
+
this.watchHost.suspectAll();
|
|
137
154
|
this.watchDirty = true;
|
|
138
155
|
await this.syncNow();
|
|
139
156
|
if (!this.closing)
|
|
@@ -156,7 +173,7 @@ export class UserGroundHost {
|
|
|
156
173
|
async flushObservedChanges() {
|
|
157
174
|
while (true) {
|
|
158
175
|
const dirty = this.watchDirty
|
|
159
|
-
||
|
|
176
|
+
|| this.watchHost.hasPending;
|
|
160
177
|
this.watchDirty = false;
|
|
161
178
|
if (dirty) {
|
|
162
179
|
await this.syncNow();
|
|
@@ -184,25 +201,20 @@ export class UserGroundHost {
|
|
|
184
201
|
if (this.rescanTimer)
|
|
185
202
|
clearTimeout(this.rescanTimer);
|
|
186
203
|
this.rescanTimer = null;
|
|
187
|
-
|
|
188
|
-
watcher.handle.close();
|
|
189
|
-
this.watchers.clear();
|
|
204
|
+
this.watchHost.close({ catchUp: false });
|
|
190
205
|
await this.wire.close();
|
|
191
206
|
return;
|
|
192
207
|
}
|
|
193
208
|
// Command shutdown is the last catch-up boundary. A filesystem callback
|
|
194
209
|
// may still be queued, so watcher silence cannot be used as evidence yet.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
this.watchDirty = this.watchers.size > 0 || this.watchDirty;
|
|
210
|
+
this.watchHost.suspectAll();
|
|
211
|
+
this.watchDirty = this.watchHost.health().logicalRoots > 0 || this.watchDirty;
|
|
198
212
|
this.closing = true;
|
|
199
213
|
if (this.rescanTimer)
|
|
200
214
|
clearTimeout(this.rescanTimer);
|
|
201
215
|
this.rescanTimer = null;
|
|
202
216
|
await this.flushObservedChanges();
|
|
203
|
-
|
|
204
|
-
watcher.handle.close();
|
|
205
|
-
this.watchers.clear();
|
|
217
|
+
this.watchHost.close();
|
|
206
218
|
if (this.rescanTimer)
|
|
207
219
|
clearTimeout(this.rescanTimer);
|
|
208
220
|
this.rescanTimer = null;
|
|
@@ -285,8 +297,8 @@ export class UserGroundHost {
|
|
|
285
297
|
return localValue(identity, userRoot, database);
|
|
286
298
|
},
|
|
287
299
|
watch: async () => {
|
|
288
|
-
this.ensureWatchers(identity);
|
|
289
|
-
return { active: true, roots:
|
|
300
|
+
const health = this.ensureWatchers(identity);
|
|
301
|
+
return { active: true, roots: health.contentHandles };
|
|
290
302
|
},
|
|
291
303
|
};
|
|
292
304
|
}
|
|
@@ -497,64 +509,57 @@ export class UserGroundHost {
|
|
|
497
509
|
}
|
|
498
510
|
ensureWatchers(identity) {
|
|
499
511
|
if (this.closing)
|
|
500
|
-
return;
|
|
512
|
+
return this.watchHost.health();
|
|
501
513
|
const root = this.userRoot(identity);
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
514
|
+
const wanted = new Map([[root, {
|
|
515
|
+
rootId: `user:${identity.userId}`,
|
|
516
|
+
directory: root,
|
|
517
|
+
materialized: directoryExists(root),
|
|
518
|
+
excludedSubtrees: [join(root, ".amalgm")],
|
|
519
|
+
}]]);
|
|
520
|
+
const bindings = workspaceBindingDir(root, identity.deviceId);
|
|
521
|
+
if (existsSync(bindings)) {
|
|
522
|
+
for (const entry of readdirSync(bindings, { withFileTypes: true })) {
|
|
523
|
+
if (!entry.isSymbolicLink() || !UUID.test(entry.name))
|
|
524
|
+
continue;
|
|
525
|
+
const binding = join(bindings, entry.name);
|
|
526
|
+
try {
|
|
527
|
+
const directory = realpathSync(binding);
|
|
528
|
+
if (!directoryExists(directory))
|
|
529
|
+
continue;
|
|
530
|
+
wanted.set(directory, {
|
|
531
|
+
rootId: entry.name.toLowerCase(),
|
|
532
|
+
directory,
|
|
533
|
+
materialized: true,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// A missing external target has no trustworthy address until Detect
|
|
538
|
+
// rescues or rebinds it; its prior entity row remains below.
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
505
542
|
for (const row of readRows(this.databasePath(identity))) {
|
|
506
543
|
if (row.parentUUID !== null || row.absolutePath === root)
|
|
507
544
|
continue;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
545
|
+
const directory = directoryExists(row.absolutePath)
|
|
546
|
+
? realpathSync(row.absolutePath)
|
|
547
|
+
: resolve(row.absolutePath);
|
|
548
|
+
const prior = wanted.get(directory);
|
|
549
|
+
if (prior && prior.rootId !== row.rootUUID) {
|
|
550
|
+
throw new Error(`two logical roots claim the same Watch ground: ${directory}`);
|
|
514
551
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
this.watchers.delete(directory);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
for (const [directory, enrollment] of wanted) {
|
|
523
|
-
if (this.watchers.has(directory))
|
|
524
|
-
continue;
|
|
525
|
-
const suspicion = createSuspicionScope();
|
|
526
|
-
const handle = watch(directory, { recursive: true }, (_event, file) => {
|
|
527
|
-
const relativePath = String(file || "").split(sep).join("/");
|
|
528
|
-
if (relativePath.split("/").includes(".amalgm"))
|
|
529
|
-
return;
|
|
530
|
-
if (relativePath && !enrollment(relativePath))
|
|
531
|
-
return;
|
|
532
|
-
const segments = relativePath.split("/");
|
|
533
|
-
const git = segments.indexOf(".git");
|
|
534
|
-
if (!relativePath || relativePath === ".amalgmignore"
|
|
535
|
-
|| (git === 0 && segments[1] === "index")) {
|
|
536
|
-
suspicion.suspectAll();
|
|
537
|
-
}
|
|
538
|
-
else if (git > 0 && segments[git + 1] === "index") {
|
|
539
|
-
suspicion.ring(segments.slice(0, git).join("/"));
|
|
540
|
-
}
|
|
541
|
-
else {
|
|
542
|
-
suspicion.ring(relativePath);
|
|
543
|
-
}
|
|
544
|
-
this.scheduleRescan();
|
|
552
|
+
wanted.set(directory, {
|
|
553
|
+
rootId: row.rootUUID,
|
|
554
|
+
directory,
|
|
555
|
+
materialized: directoryExists(directory),
|
|
545
556
|
});
|
|
546
|
-
handle.on("error", () => {
|
|
547
|
-
suspicion.suspectAll();
|
|
548
|
-
handle.close();
|
|
549
|
-
this.watchers.delete(directory);
|
|
550
|
-
this.scheduleRescan();
|
|
551
|
-
});
|
|
552
|
-
this.watchers.set(directory, { handle, suspicion });
|
|
553
|
-
// Coverage began after the preceding observation. One catch-up proves
|
|
554
|
-
// the unobserved interval before watcher silence can be trusted.
|
|
555
|
-
suspicion.suspectAll();
|
|
556
|
-
this.scheduleRescan();
|
|
557
557
|
}
|
|
558
|
+
const health = this.watchHost.reconcile([...wanted.values()]);
|
|
559
|
+
if (health.state !== "healthy" || health.contentHandles < 1) {
|
|
560
|
+
throw new Error(`Watch coverage is degraded: ${health.reason ?? "no content coverage"}`);
|
|
561
|
+
}
|
|
562
|
+
return health;
|
|
558
563
|
}
|
|
559
564
|
scheduleRescan() {
|
|
560
565
|
if (this.closing)
|
|
@@ -581,23 +586,18 @@ export class UserGroundHost {
|
|
|
581
586
|
const state = this.cloudState;
|
|
582
587
|
if (!state)
|
|
583
588
|
throw new Error("cloud state is unavailable for user-ground Watch");
|
|
584
|
-
const observations =
|
|
585
|
-
for (const [directory, watcher] of this.watchers) {
|
|
586
|
-
observations.set(directory, watcher.suspicion.snapshot());
|
|
587
|
-
}
|
|
589
|
+
const observations = this.watchHost.observations();
|
|
588
590
|
const localRecords = scanAndRegister({
|
|
589
591
|
identity,
|
|
590
592
|
userRoot: this.userRoot(identity),
|
|
591
593
|
database: this.databasePath(identity),
|
|
592
594
|
cacheDir: this.cacheDir(identity),
|
|
593
|
-
suspicions: new Map(
|
|
595
|
+
suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion.paths])),
|
|
594
596
|
});
|
|
595
597
|
const snapshot = snapshotFromRecords(travelingRecords(mergeMaterializedRoots(state.records, localRecords)));
|
|
596
598
|
const checksum = sha256Hex(stableJson(snapshot));
|
|
597
599
|
if (checksum === state.checksum) {
|
|
598
|
-
|
|
599
|
-
this.watchers.get(directory)?.suspicion.settle(snapshot);
|
|
600
|
-
}
|
|
600
|
+
this.watchHost.settle(observations);
|
|
601
601
|
return;
|
|
602
602
|
}
|
|
603
603
|
const database = initializeDatabase(this.databasePath(identity));
|
|
@@ -613,9 +613,7 @@ export class UserGroundHost {
|
|
|
613
613
|
database.close();
|
|
614
614
|
}
|
|
615
615
|
await this.drainOutbox(identity);
|
|
616
|
-
|
|
617
|
-
this.watchers.get(directory)?.suspicion.settle(snapshot);
|
|
618
|
-
}
|
|
616
|
+
this.watchHost.settle(observations);
|
|
619
617
|
}
|
|
620
618
|
async drainOutboxBeforeLookup(identity, resourceId) {
|
|
621
619
|
const pending = readOutbox(this.databasePath(identity));
|
|
@@ -686,6 +684,23 @@ function normalizedIdentity(identity) {
|
|
|
686
684
|
deviceId: String(identity.deviceId).trim(),
|
|
687
685
|
};
|
|
688
686
|
}
|
|
687
|
+
function assertHealthyWatch(evidence, rootId) {
|
|
688
|
+
const root = evidence.roots.find((candidate) => candidate.rootId === rootId);
|
|
689
|
+
const content = root?.contentOwner
|
|
690
|
+
? evidence.content.find((candidate) => candidate.directory === root.contentOwner)
|
|
691
|
+
: undefined;
|
|
692
|
+
const address = root?.contentOwner
|
|
693
|
+
? evidence.addresses.find((candidate) => candidate.rootIds.includes(rootId))
|
|
694
|
+
: undefined;
|
|
695
|
+
if (evidence.health.state !== "healthy"
|
|
696
|
+
|| evidence.health.contentHandles !== evidence.content.length
|
|
697
|
+
|| evidence.health.addressHandles !== evidence.addresses.length
|
|
698
|
+
|| !root?.materialized
|
|
699
|
+
|| !content?.rootIds.includes(rootId)
|
|
700
|
+
|| !address) {
|
|
701
|
+
throw new Error(`Watch coverage is not healthy for workspace ${rootId}: ${evidence.health.reason ?? "incomplete handle evidence"}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
689
704
|
function immutableWrite(file, bytes) {
|
|
690
705
|
ensurePrivateDir(dirname(file));
|
|
691
706
|
try {
|
|
@@ -929,6 +944,14 @@ function fileType(bytes) {
|
|
|
929
944
|
function slashPath(path) {
|
|
930
945
|
return path.split(sep).join("/");
|
|
931
946
|
}
|
|
947
|
+
function directoryExists(path) {
|
|
948
|
+
try {
|
|
949
|
+
return statSync(path).isDirectory();
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
932
955
|
function scanAndRegister(input) {
|
|
933
956
|
const { identity, userRoot, database, cacheDir, suspicions } = input;
|
|
934
957
|
const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
|
|
@@ -984,7 +1007,7 @@ function scanAndRegister(input) {
|
|
|
984
1007
|
rootUUID: workspaceId,
|
|
985
1008
|
rootName: existingRoot?.name || basename(rootPath),
|
|
986
1009
|
rootPath,
|
|
987
|
-
rootType: classifyRegisteredRoot({ repository:
|
|
1010
|
+
rootType: classifyRegisteredRoot({ repository: hasGitMarker(rootPath) }),
|
|
988
1011
|
database,
|
|
989
1012
|
cacheDir,
|
|
990
1013
|
policy: () => true,
|
|
@@ -1074,7 +1097,15 @@ function scanRoot(input) {
|
|
|
1074
1097
|
? slashPath(relative(activeRepository.root, absolutePath))
|
|
1075
1098
|
: null;
|
|
1076
1099
|
if (observe && repositoryPath && activeRepository && !activeRepository.evidence) {
|
|
1077
|
-
|
|
1100
|
+
try {
|
|
1101
|
+
activeRepository.evidence = inspectGitRegistration(activeRepository.root, activeRepository.evidencePaths);
|
|
1102
|
+
}
|
|
1103
|
+
catch {
|
|
1104
|
+
// Classification already settled from the marker. If Git cannot
|
|
1105
|
+
// provide clean-index evidence, read current worktree bytes; Card +
|
|
1106
|
+
// Checkpoint capture below remains the transfer retry boundary.
|
|
1107
|
+
activeRepository.evidence = { cleanLeaves: new Map() };
|
|
1108
|
+
}
|
|
1078
1109
|
}
|
|
1079
1110
|
const indexed = observe && repositoryPath
|
|
1080
1111
|
? activeRepository?.evidence?.cleanLeaves.get(repositoryPath)
|
|
@@ -1114,7 +1145,7 @@ function scanRoot(input) {
|
|
|
1114
1145
|
}
|
|
1115
1146
|
}
|
|
1116
1147
|
else if (stats.isDirectory()) {
|
|
1117
|
-
type = classifyDirectory({ repository:
|
|
1148
|
+
type = classifyDirectory({ repository: hasGitMarker(absolutePath) });
|
|
1118
1149
|
}
|
|
1119
1150
|
else if (stats.isFile()) {
|
|
1120
1151
|
if (indexed && indexed.type !== "link") {
|
|
@@ -1161,81 +1192,6 @@ function scanRoot(input) {
|
|
|
1161
1192
|
}
|
|
1162
1193
|
};
|
|
1163
1194
|
visit(rootPath, rootUUID, rootType);
|
|
1164
|
-
const repositories = entries.filter((entry) => entry.type === "repo.git");
|
|
1165
|
-
const properDescendantOf = (candidate, ancestor) => ancestor === "" ? candidate !== "" : candidate.startsWith(`${ancestor}/`);
|
|
1166
|
-
const repositoryOwner = (entry) => repositories
|
|
1167
|
-
.filter((repository) => repository.uuid !== entry.uuid
|
|
1168
|
-
&& properDescendantOf(entry.relativePath, repository.relativePath))
|
|
1169
|
-
.sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
|
|
1170
|
-
for (const repository of repositories) {
|
|
1171
|
-
const repoIdentity = entries
|
|
1172
|
-
.filter((entry) => repositoryOwner(entry)?.uuid === repository.uuid)
|
|
1173
|
-
.map((entry) => ({
|
|
1174
|
-
path: repository.relativePath
|
|
1175
|
-
? entry.relativePath.slice(repository.relativePath.length + 1)
|
|
1176
|
-
: entry.relativePath,
|
|
1177
|
-
uuid: entry.uuid,
|
|
1178
|
-
type: entry.type,
|
|
1179
|
-
payloadVersion: entry.payloadVersion,
|
|
1180
|
-
}))
|
|
1181
|
-
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
1182
|
-
const priorRow = existingByPath.get(repository.relativePath);
|
|
1183
|
-
let prior = null;
|
|
1184
|
-
if (priorRow?.type === "repo.git" && priorRow.payloadVersion && priorRow.transportVersion) {
|
|
1185
|
-
const priorPath = join(cacheDir, `${priorRow.transportVersion}.bin`);
|
|
1186
|
-
if (existsSync(priorPath)) {
|
|
1187
|
-
try {
|
|
1188
|
-
const layout = inspectRepositoryTransportFile(priorPath);
|
|
1189
|
-
if (layout.stateId === priorRow.payloadVersion) {
|
|
1190
|
-
prior = {
|
|
1191
|
-
stateId: priorRow.payloadVersion,
|
|
1192
|
-
transportVersion: priorRow.transportVersion,
|
|
1193
|
-
card: layout.card,
|
|
1194
|
-
identityHash: layout.identityHash,
|
|
1195
|
-
};
|
|
1196
|
-
}
|
|
1197
|
-
else {
|
|
1198
|
-
rmSync(priorPath, { force: true });
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
catch {
|
|
1202
|
-
// Content-addressed cache bytes are evidence, not ground. A file
|
|
1203
|
-
// that cannot prove the current envelope is a cache miss.
|
|
1204
|
-
rmSync(priorPath, { force: true });
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
const previousRepositories = [...existingByPath.values()]
|
|
1209
|
-
.filter((row) => row.type === "repo.git");
|
|
1210
|
-
const previousOwner = (row) => previousRepositories
|
|
1211
|
-
.filter((candidate) => candidate.uuid !== row.uuid
|
|
1212
|
-
&& properDescendantOf(row.relativePath, candidate.relativePath))
|
|
1213
|
-
.sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
|
|
1214
|
-
const previousIdentity = prior && priorRow
|
|
1215
|
-
? [...existingByPath.values()]
|
|
1216
|
-
.filter((row) => previousOwner(row)?.uuid === priorRow.uuid)
|
|
1217
|
-
.map((row) => ({
|
|
1218
|
-
path: priorRow.relativePath
|
|
1219
|
-
? row.relativePath.slice(priorRow.relativePath.length + 1)
|
|
1220
|
-
: row.relativePath,
|
|
1221
|
-
uuid: row.uuid,
|
|
1222
|
-
type: row.type,
|
|
1223
|
-
payloadVersion: row.payloadVersion,
|
|
1224
|
-
}))
|
|
1225
|
-
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
|
|
1226
|
-
: null;
|
|
1227
|
-
const captured = captureRepository(repository.absolutePath, repoIdentity, prior, previousIdentity);
|
|
1228
|
-
repository.payloadVersion = captured.stateId;
|
|
1229
|
-
repository.transportVersion = captured.transportVersion;
|
|
1230
|
-
if (captured.bytes) {
|
|
1231
|
-
immutableWriteArtifact(cacheDir, {
|
|
1232
|
-
entityId: repository.uuid,
|
|
1233
|
-
entityType: "repo.git",
|
|
1234
|
-
kind: "git",
|
|
1235
|
-
contentHash: captured.transportVersion,
|
|
1236
|
-
}, captured.bytes);
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
1195
|
const children = new Map();
|
|
1240
1196
|
for (const entry of entries) {
|
|
1241
1197
|
if (entry.parentUUID === null)
|
|
@@ -1244,7 +1200,7 @@ function scanRoot(input) {
|
|
|
1244
1200
|
group.push({ uuid: entry.uuid, name: entry.name });
|
|
1245
1201
|
children.set(entry.parentUUID, group);
|
|
1246
1202
|
}
|
|
1247
|
-
const
|
|
1203
|
+
const rowsFromEntries = () => entries.map((entry) => {
|
|
1248
1204
|
const versionPayload = entry.type === "workspace" || entry.type === "folder"
|
|
1249
1205
|
? membershipHash(children.get(entry.uuid) ?? [], sha256Hex)
|
|
1250
1206
|
: entry.payloadVersion;
|
|
@@ -1272,7 +1228,100 @@ function scanRoot(input) {
|
|
|
1272
1228
|
inode: entry.inode,
|
|
1273
1229
|
};
|
|
1274
1230
|
});
|
|
1275
|
-
|
|
1231
|
+
const persistCurrentRows = () => {
|
|
1232
|
+
const rows = rowsFromEntries();
|
|
1233
|
+
persistRows(database, identity, resourceId, rootUUID, rows, existingRows);
|
|
1234
|
+
return rows;
|
|
1235
|
+
};
|
|
1236
|
+
const repositories = entries.filter((entry) => entry.type === "repo.git");
|
|
1237
|
+
const properDescendantOf = (candidate, ancestor) => ancestor === "" ? candidate !== "" : candidate.startsWith(`${ancestor}/`);
|
|
1238
|
+
const repositoryOwner = (entry) => repositories
|
|
1239
|
+
.filter((repository) => repository.uuid !== entry.uuid
|
|
1240
|
+
&& properDescendantOf(entry.relativePath, repository.relativePath))
|
|
1241
|
+
.sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
|
|
1242
|
+
try {
|
|
1243
|
+
for (const repository of repositories) {
|
|
1244
|
+
const repoIdentity = entries
|
|
1245
|
+
.filter((entry) => repositoryOwner(entry)?.uuid === repository.uuid)
|
|
1246
|
+
.map((entry) => ({
|
|
1247
|
+
path: repository.relativePath
|
|
1248
|
+
? entry.relativePath.slice(repository.relativePath.length + 1)
|
|
1249
|
+
: entry.relativePath,
|
|
1250
|
+
uuid: entry.uuid,
|
|
1251
|
+
type: entry.type,
|
|
1252
|
+
payloadVersion: entry.payloadVersion,
|
|
1253
|
+
}))
|
|
1254
|
+
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
1255
|
+
const priorRow = existingByPath.get(repository.relativePath);
|
|
1256
|
+
if (priorRow?.type === "repo.git") {
|
|
1257
|
+
repository.payloadVersion = priorRow.payloadVersion;
|
|
1258
|
+
repository.transportVersion = priorRow.transportVersion;
|
|
1259
|
+
}
|
|
1260
|
+
let prior = null;
|
|
1261
|
+
if (priorRow?.type === "repo.git" && priorRow.payloadVersion && priorRow.transportVersion) {
|
|
1262
|
+
const priorPath = join(cacheDir, `${priorRow.transportVersion}.bin`);
|
|
1263
|
+
if (existsSync(priorPath)) {
|
|
1264
|
+
try {
|
|
1265
|
+
const layout = inspectRepositoryTransportFile(priorPath);
|
|
1266
|
+
if (layout.stateId === priorRow.payloadVersion) {
|
|
1267
|
+
prior = {
|
|
1268
|
+
stateId: priorRow.payloadVersion,
|
|
1269
|
+
transportVersion: priorRow.transportVersion,
|
|
1270
|
+
card: layout.card,
|
|
1271
|
+
identityHash: layout.identityHash,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
rmSync(priorPath, { force: true });
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
catch {
|
|
1279
|
+
// Content-addressed cache bytes are evidence, not ground. A file
|
|
1280
|
+
// that cannot prove the current envelope is a cache miss.
|
|
1281
|
+
rmSync(priorPath, { force: true });
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
const previousRepositories = [...existingByPath.values()]
|
|
1286
|
+
.filter((row) => row.type === "repo.git");
|
|
1287
|
+
const previousOwner = (row) => previousRepositories
|
|
1288
|
+
.filter((candidate) => candidate.uuid !== row.uuid
|
|
1289
|
+
&& properDescendantOf(row.relativePath, candidate.relativePath))
|
|
1290
|
+
.sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
|
|
1291
|
+
const previousIdentity = prior && priorRow
|
|
1292
|
+
? [...existingByPath.values()]
|
|
1293
|
+
.filter((row) => previousOwner(row)?.uuid === priorRow.uuid)
|
|
1294
|
+
.map((row) => ({
|
|
1295
|
+
path: priorRow.relativePath
|
|
1296
|
+
? row.relativePath.slice(priorRow.relativePath.length + 1)
|
|
1297
|
+
: row.relativePath,
|
|
1298
|
+
uuid: row.uuid,
|
|
1299
|
+
type: row.type,
|
|
1300
|
+
payloadVersion: row.payloadVersion,
|
|
1301
|
+
}))
|
|
1302
|
+
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
|
|
1303
|
+
: null;
|
|
1304
|
+
const captured = captureRepository(repository.absolutePath, repoIdentity, prior, previousIdentity);
|
|
1305
|
+
repository.payloadVersion = captured.stateId;
|
|
1306
|
+
repository.transportVersion = captured.transportVersion;
|
|
1307
|
+
if (captured.bytes) {
|
|
1308
|
+
immutableWriteArtifact(cacheDir, {
|
|
1309
|
+
entityId: repository.uuid,
|
|
1310
|
+
entityType: "repo.git",
|
|
1311
|
+
kind: "git",
|
|
1312
|
+
contentHash: captured.transportVersion,
|
|
1313
|
+
}, captured.bytes);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
catch (error) {
|
|
1318
|
+
// Type and identity are filesystem facts, so they settle locally even
|
|
1319
|
+
// when the selected repository rail cannot yet capture its artifact.
|
|
1320
|
+
// Cloud publication remains commit-last because this failure propagates.
|
|
1321
|
+
persistCurrentRows();
|
|
1322
|
+
throw error;
|
|
1323
|
+
}
|
|
1324
|
+
const rows = persistCurrentRows();
|
|
1276
1325
|
return { records: rows.map((row) => row.record), rows, referenceWorkspaceIds: [...referenceWorkspaceIds] };
|
|
1277
1326
|
}
|
|
1278
1327
|
function portablePaths(records) {
|