@amalgm/shell 0.1.28 → 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 +46 -22
- package/README.md +9 -2
- package/dist/cli.js +12 -7
- package/dist/cli.js.map +1 -1
- package/dist/content-wire-codec.d.ts +17 -0
- package/dist/content-wire-codec.js +47 -0
- package/dist/content-wire-codec.js.map +1 -0
- 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 +3 -0
- package/dist/index.js +2 -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 +236 -176
- 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,16 +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
|
+
import { decodeContentWireBytes, encodeContentWireBytes } from "./content-wire-codec.js";
|
|
10
11
|
import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, pathExists, pathWithin, referenceWorkspaceId, workspaceBindingDir, } from "./files-register-host.js";
|
|
11
|
-
import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile,
|
|
12
|
+
import { applyRepositoryFiles, captureRepository, inspectRepositoryTransportFile, hasGitMarker, } from "./git-repository-host.js";
|
|
12
13
|
import { inspectGitRegistration, } from "./git-registration-host.js";
|
|
13
14
|
import { projectMaterializedGraph } from "./materialized-graph.js";
|
|
15
|
+
import { NodeWatchHost, } from "./watching/index.js";
|
|
14
16
|
import { WireClient, WireRequestError } from "./wire-client.js";
|
|
15
17
|
const PORTABLE_FIELDS = [
|
|
16
18
|
"uuid", "type", "parentUUID", "name", "status", "version",
|
|
@@ -24,7 +26,7 @@ const FILE_BATCH_DOWNLOAD_CONCURRENCY = 2;
|
|
|
24
26
|
export class UserGroundHost {
|
|
25
27
|
options;
|
|
26
28
|
wire;
|
|
27
|
-
|
|
29
|
+
watchHost;
|
|
28
30
|
activeIdentity = null;
|
|
29
31
|
rescanTimer = null;
|
|
30
32
|
watchDirty = false;
|
|
@@ -40,6 +42,11 @@ export class UserGroundHost {
|
|
|
40
42
|
token: options.tunnelToken,
|
|
41
43
|
runtimeVersion: options.runtimeVersion,
|
|
42
44
|
});
|
|
45
|
+
this.watchHost = new NodeWatchHost({
|
|
46
|
+
open: options.openWatch,
|
|
47
|
+
onSignal: () => this.scheduleRescan(),
|
|
48
|
+
onHealth: options.onWatchHealth,
|
|
49
|
+
});
|
|
43
50
|
this.port = {
|
|
44
51
|
converge: async (identity) => {
|
|
45
52
|
this.converged = false;
|
|
@@ -72,18 +79,30 @@ export class UserGroundHost {
|
|
|
72
79
|
rows(identity) {
|
|
73
80
|
return readRows(this.databasePath(identity));
|
|
74
81
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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");
|
|
83
91
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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;
|
|
87
106
|
}
|
|
88
107
|
/** Host effects for Live-owned Files commands. The shell CLI receives these
|
|
89
108
|
* ports but never decides Files behavior itself. */
|
|
@@ -119,8 +138,8 @@ export class UserGroundHost {
|
|
|
119
138
|
readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact),
|
|
120
139
|
}),
|
|
121
140
|
coverWorkspace: async () => {
|
|
122
|
-
this.ensureWatchers(identity);
|
|
123
|
-
return { active: true, roots:
|
|
141
|
+
const health = this.ensureWatchers(identity);
|
|
142
|
+
return { active: true, roots: health.contentHandles };
|
|
124
143
|
},
|
|
125
144
|
};
|
|
126
145
|
return { register, add };
|
|
@@ -131,8 +150,7 @@ export class UserGroundHost {
|
|
|
131
150
|
if (!this.converged || !this.activeIdentity || !this.cloudState)
|
|
132
151
|
return;
|
|
133
152
|
const identity = this.activeIdentity;
|
|
134
|
-
|
|
135
|
-
watcher.suspicion.suspectAll();
|
|
153
|
+
this.watchHost.suspectAll();
|
|
136
154
|
this.watchDirty = true;
|
|
137
155
|
await this.syncNow();
|
|
138
156
|
if (!this.closing)
|
|
@@ -155,7 +173,7 @@ export class UserGroundHost {
|
|
|
155
173
|
async flushObservedChanges() {
|
|
156
174
|
while (true) {
|
|
157
175
|
const dirty = this.watchDirty
|
|
158
|
-
||
|
|
176
|
+
|| this.watchHost.hasPending;
|
|
159
177
|
this.watchDirty = false;
|
|
160
178
|
if (dirty) {
|
|
161
179
|
await this.syncNow();
|
|
@@ -183,25 +201,20 @@ export class UserGroundHost {
|
|
|
183
201
|
if (this.rescanTimer)
|
|
184
202
|
clearTimeout(this.rescanTimer);
|
|
185
203
|
this.rescanTimer = null;
|
|
186
|
-
|
|
187
|
-
watcher.handle.close();
|
|
188
|
-
this.watchers.clear();
|
|
204
|
+
this.watchHost.close({ catchUp: false });
|
|
189
205
|
await this.wire.close();
|
|
190
206
|
return;
|
|
191
207
|
}
|
|
192
208
|
// Command shutdown is the last catch-up boundary. A filesystem callback
|
|
193
209
|
// may still be queued, so watcher silence cannot be used as evidence yet.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this.watchDirty = this.watchers.size > 0 || this.watchDirty;
|
|
210
|
+
this.watchHost.suspectAll();
|
|
211
|
+
this.watchDirty = this.watchHost.health().logicalRoots > 0 || this.watchDirty;
|
|
197
212
|
this.closing = true;
|
|
198
213
|
if (this.rescanTimer)
|
|
199
214
|
clearTimeout(this.rescanTimer);
|
|
200
215
|
this.rescanTimer = null;
|
|
201
216
|
await this.flushObservedChanges();
|
|
202
|
-
|
|
203
|
-
watcher.handle.close();
|
|
204
|
-
this.watchers.clear();
|
|
217
|
+
this.watchHost.close();
|
|
205
218
|
if (this.rescanTimer)
|
|
206
219
|
clearTimeout(this.rescanTimer);
|
|
207
220
|
this.rescanTimer = null;
|
|
@@ -284,8 +297,8 @@ export class UserGroundHost {
|
|
|
284
297
|
return localValue(identity, userRoot, database);
|
|
285
298
|
},
|
|
286
299
|
watch: async () => {
|
|
287
|
-
this.ensureWatchers(identity);
|
|
288
|
-
return { active: true, roots:
|
|
300
|
+
const health = this.ensureWatchers(identity);
|
|
301
|
+
return { active: true, roots: health.contentHandles };
|
|
289
302
|
},
|
|
290
303
|
};
|
|
291
304
|
}
|
|
@@ -364,6 +377,7 @@ export class UserGroundHost {
|
|
|
364
377
|
}
|
|
365
378
|
return;
|
|
366
379
|
}
|
|
380
|
+
const encoded = await encodeContentWireBytes(bytes);
|
|
367
381
|
await retryContent(() => this.wire.requestBinary({
|
|
368
382
|
type: "private.entity-content.put-batch",
|
|
369
383
|
resource_id: resourceId,
|
|
@@ -376,7 +390,9 @@ export class UserGroundHost {
|
|
|
376
390
|
bytes: manifest.chunks[index]?.bytes,
|
|
377
391
|
})),
|
|
378
392
|
bytes: bytes.byteLength,
|
|
379
|
-
|
|
393
|
+
wire_bytes: encoded.bytes.byteLength,
|
|
394
|
+
...(encoded.encoding ? { content_encoding: encoded.encoding } : {}),
|
|
395
|
+
}, encoded.bytes, ["private.entity-content.stored-batch"]));
|
|
380
396
|
},
|
|
381
397
|
putManifest: async (_candidate, manifest) => {
|
|
382
398
|
if (manifestPresent)
|
|
@@ -466,7 +482,7 @@ export class UserGroundHost {
|
|
|
466
482
|
bytes: manifest.chunks[index]?.bytes,
|
|
467
483
|
})),
|
|
468
484
|
}, ["private.entity-content.data-batch"]), true);
|
|
469
|
-
return
|
|
485
|
+
return decodeContentWireBytes(binaryWireFrameBytes(frame), frame.content_encoding, Number(frame.bytes));
|
|
470
486
|
},
|
|
471
487
|
putChunk: async (_candidate, _manifest, index, bytes) => {
|
|
472
488
|
if (!cache.target)
|
|
@@ -493,64 +509,57 @@ export class UserGroundHost {
|
|
|
493
509
|
}
|
|
494
510
|
ensureWatchers(identity) {
|
|
495
511
|
if (this.closing)
|
|
496
|
-
return;
|
|
512
|
+
return this.watchHost.health();
|
|
497
513
|
const root = this.userRoot(identity);
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
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
|
+
}
|
|
501
542
|
for (const row of readRows(this.databasePath(identity))) {
|
|
502
543
|
if (row.parentUUID !== null || row.absolutePath === root)
|
|
503
544
|
continue;
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
for (const [directory, watcher] of this.watchers) {
|
|
513
|
-
if (!wanted.has(directory)) {
|
|
514
|
-
watcher.handle.close();
|
|
515
|
-
this.watchers.delete(directory);
|
|
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}`);
|
|
516
551
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
const suspicion = createSuspicionScope();
|
|
522
|
-
const handle = watch(directory, { recursive: true }, (_event, file) => {
|
|
523
|
-
const relativePath = String(file || "").split(sep).join("/");
|
|
524
|
-
if (relativePath.split("/").includes(".amalgm"))
|
|
525
|
-
return;
|
|
526
|
-
if (relativePath && !enrollment(relativePath))
|
|
527
|
-
return;
|
|
528
|
-
const segments = relativePath.split("/");
|
|
529
|
-
const git = segments.indexOf(".git");
|
|
530
|
-
if (!relativePath || relativePath === ".amalgmignore"
|
|
531
|
-
|| (git === 0 && segments[1] === "index")) {
|
|
532
|
-
suspicion.suspectAll();
|
|
533
|
-
}
|
|
534
|
-
else if (git > 0 && segments[git + 1] === "index") {
|
|
535
|
-
suspicion.ring(segments.slice(0, git).join("/"));
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
suspicion.ring(relativePath);
|
|
539
|
-
}
|
|
540
|
-
this.scheduleRescan();
|
|
541
|
-
});
|
|
542
|
-
handle.on("error", () => {
|
|
543
|
-
suspicion.suspectAll();
|
|
544
|
-
handle.close();
|
|
545
|
-
this.watchers.delete(directory);
|
|
546
|
-
this.scheduleRescan();
|
|
552
|
+
wanted.set(directory, {
|
|
553
|
+
rootId: row.rootUUID,
|
|
554
|
+
directory,
|
|
555
|
+
materialized: directoryExists(directory),
|
|
547
556
|
});
|
|
548
|
-
this.watchers.set(directory, { handle, suspicion });
|
|
549
|
-
// Coverage began after the preceding observation. One catch-up proves
|
|
550
|
-
// the unobserved interval before watcher silence can be trusted.
|
|
551
|
-
suspicion.suspectAll();
|
|
552
|
-
this.scheduleRescan();
|
|
553
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;
|
|
554
563
|
}
|
|
555
564
|
scheduleRescan() {
|
|
556
565
|
if (this.closing)
|
|
@@ -577,23 +586,18 @@ export class UserGroundHost {
|
|
|
577
586
|
const state = this.cloudState;
|
|
578
587
|
if (!state)
|
|
579
588
|
throw new Error("cloud state is unavailable for user-ground Watch");
|
|
580
|
-
const observations =
|
|
581
|
-
for (const [directory, watcher] of this.watchers) {
|
|
582
|
-
observations.set(directory, watcher.suspicion.snapshot());
|
|
583
|
-
}
|
|
589
|
+
const observations = this.watchHost.observations();
|
|
584
590
|
const localRecords = scanAndRegister({
|
|
585
591
|
identity,
|
|
586
592
|
userRoot: this.userRoot(identity),
|
|
587
593
|
database: this.databasePath(identity),
|
|
588
594
|
cacheDir: this.cacheDir(identity),
|
|
589
|
-
suspicions: new Map(
|
|
595
|
+
suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion.paths])),
|
|
590
596
|
});
|
|
591
597
|
const snapshot = snapshotFromRecords(travelingRecords(mergeMaterializedRoots(state.records, localRecords)));
|
|
592
598
|
const checksum = sha256Hex(stableJson(snapshot));
|
|
593
599
|
if (checksum === state.checksum) {
|
|
594
|
-
|
|
595
|
-
this.watchers.get(directory)?.suspicion.settle(snapshot);
|
|
596
|
-
}
|
|
600
|
+
this.watchHost.settle(observations);
|
|
597
601
|
return;
|
|
598
602
|
}
|
|
599
603
|
const database = initializeDatabase(this.databasePath(identity));
|
|
@@ -609,9 +613,7 @@ export class UserGroundHost {
|
|
|
609
613
|
database.close();
|
|
610
614
|
}
|
|
611
615
|
await this.drainOutbox(identity);
|
|
612
|
-
|
|
613
|
-
this.watchers.get(directory)?.suspicion.settle(snapshot);
|
|
614
|
-
}
|
|
616
|
+
this.watchHost.settle(observations);
|
|
615
617
|
}
|
|
616
618
|
async drainOutboxBeforeLookup(identity, resourceId) {
|
|
617
619
|
const pending = readOutbox(this.databasePath(identity));
|
|
@@ -682,6 +684,23 @@ function normalizedIdentity(identity) {
|
|
|
682
684
|
deviceId: String(identity.deviceId).trim(),
|
|
683
685
|
};
|
|
684
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
|
+
}
|
|
685
704
|
function immutableWrite(file, bytes) {
|
|
686
705
|
ensurePrivateDir(dirname(file));
|
|
687
706
|
try {
|
|
@@ -925,6 +944,14 @@ function fileType(bytes) {
|
|
|
925
944
|
function slashPath(path) {
|
|
926
945
|
return path.split(sep).join("/");
|
|
927
946
|
}
|
|
947
|
+
function directoryExists(path) {
|
|
948
|
+
try {
|
|
949
|
+
return statSync(path).isDirectory();
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
928
955
|
function scanAndRegister(input) {
|
|
929
956
|
const { identity, userRoot, database, cacheDir, suspicions } = input;
|
|
930
957
|
const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
|
|
@@ -980,7 +1007,7 @@ function scanAndRegister(input) {
|
|
|
980
1007
|
rootUUID: workspaceId,
|
|
981
1008
|
rootName: existingRoot?.name || basename(rootPath),
|
|
982
1009
|
rootPath,
|
|
983
|
-
rootType: classifyRegisteredRoot({ repository:
|
|
1010
|
+
rootType: classifyRegisteredRoot({ repository: hasGitMarker(rootPath) }),
|
|
984
1011
|
database,
|
|
985
1012
|
cacheDir,
|
|
986
1013
|
policy: () => true,
|
|
@@ -1070,7 +1097,15 @@ function scanRoot(input) {
|
|
|
1070
1097
|
? slashPath(relative(activeRepository.root, absolutePath))
|
|
1071
1098
|
: null;
|
|
1072
1099
|
if (observe && repositoryPath && activeRepository && !activeRepository.evidence) {
|
|
1073
|
-
|
|
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
|
+
}
|
|
1074
1109
|
}
|
|
1075
1110
|
const indexed = observe && repositoryPath
|
|
1076
1111
|
? activeRepository?.evidence?.cleanLeaves.get(repositoryPath)
|
|
@@ -1110,7 +1145,7 @@ function scanRoot(input) {
|
|
|
1110
1145
|
}
|
|
1111
1146
|
}
|
|
1112
1147
|
else if (stats.isDirectory()) {
|
|
1113
|
-
type = classifyDirectory({ repository:
|
|
1148
|
+
type = classifyDirectory({ repository: hasGitMarker(absolutePath) });
|
|
1114
1149
|
}
|
|
1115
1150
|
else if (stats.isFile()) {
|
|
1116
1151
|
if (indexed && indexed.type !== "link") {
|
|
@@ -1157,81 +1192,6 @@ function scanRoot(input) {
|
|
|
1157
1192
|
}
|
|
1158
1193
|
};
|
|
1159
1194
|
visit(rootPath, rootUUID, rootType);
|
|
1160
|
-
const repositories = entries.filter((entry) => entry.type === "repo.git");
|
|
1161
|
-
const properDescendantOf = (candidate, ancestor) => ancestor === "" ? candidate !== "" : candidate.startsWith(`${ancestor}/`);
|
|
1162
|
-
const repositoryOwner = (entry) => repositories
|
|
1163
|
-
.filter((repository) => repository.uuid !== entry.uuid
|
|
1164
|
-
&& properDescendantOf(entry.relativePath, repository.relativePath))
|
|
1165
|
-
.sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
|
|
1166
|
-
for (const repository of repositories) {
|
|
1167
|
-
const repoIdentity = entries
|
|
1168
|
-
.filter((entry) => repositoryOwner(entry)?.uuid === repository.uuid)
|
|
1169
|
-
.map((entry) => ({
|
|
1170
|
-
path: repository.relativePath
|
|
1171
|
-
? entry.relativePath.slice(repository.relativePath.length + 1)
|
|
1172
|
-
: entry.relativePath,
|
|
1173
|
-
uuid: entry.uuid,
|
|
1174
|
-
type: entry.type,
|
|
1175
|
-
payloadVersion: entry.payloadVersion,
|
|
1176
|
-
}))
|
|
1177
|
-
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
1178
|
-
const priorRow = existingByPath.get(repository.relativePath);
|
|
1179
|
-
let prior = null;
|
|
1180
|
-
if (priorRow?.type === "repo.git" && priorRow.payloadVersion && priorRow.transportVersion) {
|
|
1181
|
-
const priorPath = join(cacheDir, `${priorRow.transportVersion}.bin`);
|
|
1182
|
-
if (existsSync(priorPath)) {
|
|
1183
|
-
try {
|
|
1184
|
-
const layout = inspectRepositoryTransportFile(priorPath);
|
|
1185
|
-
if (layout.stateId === priorRow.payloadVersion) {
|
|
1186
|
-
prior = {
|
|
1187
|
-
stateId: priorRow.payloadVersion,
|
|
1188
|
-
transportVersion: priorRow.transportVersion,
|
|
1189
|
-
card: layout.card,
|
|
1190
|
-
identityHash: layout.identityHash,
|
|
1191
|
-
};
|
|
1192
|
-
}
|
|
1193
|
-
else {
|
|
1194
|
-
rmSync(priorPath, { force: true });
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
catch {
|
|
1198
|
-
// Content-addressed cache bytes are evidence, not ground. A file
|
|
1199
|
-
// that cannot prove the current envelope is a cache miss.
|
|
1200
|
-
rmSync(priorPath, { force: true });
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
const previousRepositories = [...existingByPath.values()]
|
|
1205
|
-
.filter((row) => row.type === "repo.git");
|
|
1206
|
-
const previousOwner = (row) => previousRepositories
|
|
1207
|
-
.filter((candidate) => candidate.uuid !== row.uuid
|
|
1208
|
-
&& properDescendantOf(row.relativePath, candidate.relativePath))
|
|
1209
|
-
.sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
|
|
1210
|
-
const previousIdentity = prior && priorRow
|
|
1211
|
-
? [...existingByPath.values()]
|
|
1212
|
-
.filter((row) => previousOwner(row)?.uuid === priorRow.uuid)
|
|
1213
|
-
.map((row) => ({
|
|
1214
|
-
path: priorRow.relativePath
|
|
1215
|
-
? row.relativePath.slice(priorRow.relativePath.length + 1)
|
|
1216
|
-
: row.relativePath,
|
|
1217
|
-
uuid: row.uuid,
|
|
1218
|
-
type: row.type,
|
|
1219
|
-
payloadVersion: row.payloadVersion,
|
|
1220
|
-
}))
|
|
1221
|
-
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
|
|
1222
|
-
: null;
|
|
1223
|
-
const captured = captureRepository(repository.absolutePath, repoIdentity, prior, previousIdentity);
|
|
1224
|
-
repository.payloadVersion = captured.stateId;
|
|
1225
|
-
repository.transportVersion = captured.transportVersion;
|
|
1226
|
-
if (captured.bytes) {
|
|
1227
|
-
immutableWriteArtifact(cacheDir, {
|
|
1228
|
-
entityId: repository.uuid,
|
|
1229
|
-
entityType: "repo.git",
|
|
1230
|
-
kind: "git",
|
|
1231
|
-
contentHash: captured.transportVersion,
|
|
1232
|
-
}, captured.bytes);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
1195
|
const children = new Map();
|
|
1236
1196
|
for (const entry of entries) {
|
|
1237
1197
|
if (entry.parentUUID === null)
|
|
@@ -1240,7 +1200,7 @@ function scanRoot(input) {
|
|
|
1240
1200
|
group.push({ uuid: entry.uuid, name: entry.name });
|
|
1241
1201
|
children.set(entry.parentUUID, group);
|
|
1242
1202
|
}
|
|
1243
|
-
const
|
|
1203
|
+
const rowsFromEntries = () => entries.map((entry) => {
|
|
1244
1204
|
const versionPayload = entry.type === "workspace" || entry.type === "folder"
|
|
1245
1205
|
? membershipHash(children.get(entry.uuid) ?? [], sha256Hex)
|
|
1246
1206
|
: entry.payloadVersion;
|
|
@@ -1268,7 +1228,100 @@ function scanRoot(input) {
|
|
|
1268
1228
|
inode: entry.inode,
|
|
1269
1229
|
};
|
|
1270
1230
|
});
|
|
1271
|
-
|
|
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();
|
|
1272
1325
|
return { records: rows.map((row) => row.record), rows, referenceWorkspaceIds: [...referenceWorkspaceIds] };
|
|
1273
1326
|
}
|
|
1274
1327
|
function portablePaths(records) {
|
|
@@ -1582,13 +1635,20 @@ function readyFromFrame(frame) {
|
|
|
1582
1635
|
* against their manifest, so the host must not copy or hash those bytes a
|
|
1583
1636
|
* second time before returning them to the portable operation. */
|
|
1584
1637
|
function binaryFrameBytes(frame) {
|
|
1638
|
+
const bytes = binaryWireFrameBytes(frame);
|
|
1639
|
+
if (frame.content_encoding !== undefined || bytes.length !== Number(frame.bytes)) {
|
|
1640
|
+
throw new Error("wire content byte count does not match its header");
|
|
1641
|
+
}
|
|
1642
|
+
return bytes;
|
|
1643
|
+
}
|
|
1644
|
+
function binaryWireFrameBytes(frame) {
|
|
1585
1645
|
if (!(frame.data_bin instanceof Uint8Array)) {
|
|
1586
1646
|
throw new Error("wire content response is not a binary frame");
|
|
1587
1647
|
}
|
|
1588
1648
|
const bytes = Buffer.isBuffer(frame.data_bin)
|
|
1589
1649
|
? frame.data_bin
|
|
1590
1650
|
: Buffer.from(frame.data_bin.buffer, frame.data_bin.byteOffset, frame.data_bin.byteLength);
|
|
1591
|
-
if (bytes.length !== Number(frame.bytes)) {
|
|
1651
|
+
if (bytes.length !== Number(frame.wire_bytes ?? frame.bytes)) {
|
|
1592
1652
|
throw new Error("wire content byte count does not match its header");
|
|
1593
1653
|
}
|
|
1594
1654
|
return bytes;
|