@amalgm/shell 0.1.29 → 0.1.31
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 +42 -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 +252 -177
- 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
|
-
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
for (const [directory, watcher] of this.watchers) {
|
|
517
|
-
if (!wanted.has(directory)) {
|
|
518
|
-
watcher.handle.close();
|
|
519
|
-
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}`);
|
|
520
551
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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();
|
|
545
|
-
});
|
|
546
|
-
handle.on("error", () => {
|
|
547
|
-
suspicion.suspectAll();
|
|
548
|
-
handle.close();
|
|
549
|
-
this.watchers.delete(directory);
|
|
550
|
-
this.scheduleRescan();
|
|
552
|
+
wanted.set(directory, {
|
|
553
|
+
rootId: row.rootUUID,
|
|
554
|
+
directory,
|
|
555
|
+
materialized: directoryExists(directory),
|
|
551
556
|
});
|
|
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);
|
|
@@ -943,6 +966,7 @@ function scanAndRegister(input) {
|
|
|
943
966
|
};
|
|
944
967
|
const existingCore = existing.find((row) => row.parentUUID === null && row.absolutePath === userRoot && row.type === "workspace");
|
|
945
968
|
const coreUUID = existingCore?.uuid || randomUUID();
|
|
969
|
+
const deferredRootIds = new Set();
|
|
946
970
|
const core = scanRoot({
|
|
947
971
|
identity,
|
|
948
972
|
resourceId,
|
|
@@ -957,6 +981,8 @@ function scanAndRegister(input) {
|
|
|
957
981
|
suspects: suspicionFor(userRoot),
|
|
958
982
|
existingRows: existing.filter((row) => row.rootUUID === coreUUID),
|
|
959
983
|
});
|
|
984
|
+
if (!core.transferable)
|
|
985
|
+
deferredRootIds.add(coreUUID);
|
|
960
986
|
const targetRoots = new Set(core.referenceWorkspaceIds);
|
|
961
987
|
for (const row of existing) {
|
|
962
988
|
if (row.parentUUID === null && row.uuid !== coreUUID)
|
|
@@ -978,13 +1004,13 @@ function scanAndRegister(input) {
|
|
|
978
1004
|
}
|
|
979
1005
|
const existingRoot = existing.find((row) => row.uuid === workspaceId && row.parentUUID === null);
|
|
980
1006
|
const existingWorkspaceRows = existing.filter((row) => row.rootUUID === workspaceId);
|
|
981
|
-
scanRoot({
|
|
1007
|
+
const scanned = scanRoot({
|
|
982
1008
|
identity,
|
|
983
1009
|
resourceId,
|
|
984
1010
|
rootUUID: workspaceId,
|
|
985
1011
|
rootName: existingRoot?.name || basename(rootPath),
|
|
986
1012
|
rootPath,
|
|
987
|
-
rootType: classifyRegisteredRoot({ repository:
|
|
1013
|
+
rootType: classifyRegisteredRoot({ repository: hasGitMarker(rootPath) }),
|
|
988
1014
|
database,
|
|
989
1015
|
cacheDir,
|
|
990
1016
|
policy: () => true,
|
|
@@ -992,15 +1018,24 @@ function scanAndRegister(input) {
|
|
|
992
1018
|
suspects: existingWorkspaceRows.length === 0 ? null : suspicionFor(rootPath),
|
|
993
1019
|
existingRows: existingWorkspaceRows,
|
|
994
1020
|
});
|
|
1021
|
+
if (!scanned.transferable)
|
|
1022
|
+
deferredRootIds.add(workspaceId);
|
|
995
1023
|
}
|
|
996
|
-
return
|
|
1024
|
+
return readRows(database)
|
|
1025
|
+
.filter((row) => !deferredRootIds.has(row.rootUUID))
|
|
1026
|
+
.map(portableRecord);
|
|
997
1027
|
}
|
|
998
1028
|
function scanRoot(input) {
|
|
999
1029
|
const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, } = input;
|
|
1000
1030
|
const existingRows = (input.existingRows ?? readRows(database))
|
|
1001
1031
|
.filter((row) => row.resourceId === resourceId && row.rootUUID === rootUUID);
|
|
1002
1032
|
const existingByPath = new Map(existingRows.map((row) => [row.relativePath, row]));
|
|
1003
|
-
|
|
1033
|
+
const completeRepositoryEvidence = existingRows.every((row) => row.type !== "repo.git"
|
|
1034
|
+
|| (Boolean(row.payloadVersion) && Boolean(row.transportVersion)));
|
|
1035
|
+
if (suspects !== null
|
|
1036
|
+
&& suspects.length === 0
|
|
1037
|
+
&& existingRows.length > 0
|
|
1038
|
+
&& completeRepositoryEvidence) {
|
|
1004
1039
|
return {
|
|
1005
1040
|
records: existingRows.map(portableRecord),
|
|
1006
1041
|
rows: existingRows.map((row) => ({
|
|
@@ -1013,6 +1048,7 @@ function scanRoot(input) {
|
|
|
1013
1048
|
referenceWorkspaceIds: existingRows
|
|
1014
1049
|
.filter((row) => row.type === "reference" && row.payloadVersion && UUID.test(row.payloadVersion))
|
|
1015
1050
|
.map((row) => row.payloadVersion),
|
|
1051
|
+
transferable: true,
|
|
1016
1052
|
};
|
|
1017
1053
|
}
|
|
1018
1054
|
const entries = [];
|
|
@@ -1074,7 +1110,15 @@ function scanRoot(input) {
|
|
|
1074
1110
|
? slashPath(relative(activeRepository.root, absolutePath))
|
|
1075
1111
|
: null;
|
|
1076
1112
|
if (observe && repositoryPath && activeRepository && !activeRepository.evidence) {
|
|
1077
|
-
|
|
1113
|
+
try {
|
|
1114
|
+
activeRepository.evidence = inspectGitRegistration(activeRepository.root, activeRepository.evidencePaths);
|
|
1115
|
+
}
|
|
1116
|
+
catch {
|
|
1117
|
+
// Classification already settled from the marker. If Git cannot
|
|
1118
|
+
// provide clean-index evidence, read current worktree bytes; Card +
|
|
1119
|
+
// Checkpoint capture below remains the transfer retry boundary.
|
|
1120
|
+
activeRepository.evidence = { cleanLeaves: new Map() };
|
|
1121
|
+
}
|
|
1078
1122
|
}
|
|
1079
1123
|
const indexed = observe && repositoryPath
|
|
1080
1124
|
? activeRepository?.evidence?.cleanLeaves.get(repositoryPath)
|
|
@@ -1114,7 +1158,7 @@ function scanRoot(input) {
|
|
|
1114
1158
|
}
|
|
1115
1159
|
}
|
|
1116
1160
|
else if (stats.isDirectory()) {
|
|
1117
|
-
type = classifyDirectory({ repository:
|
|
1161
|
+
type = classifyDirectory({ repository: hasGitMarker(absolutePath) });
|
|
1118
1162
|
}
|
|
1119
1163
|
else if (stats.isFile()) {
|
|
1120
1164
|
if (indexed && indexed.type !== "link") {
|
|
@@ -1161,81 +1205,6 @@ function scanRoot(input) {
|
|
|
1161
1205
|
}
|
|
1162
1206
|
};
|
|
1163
1207
|
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
1208
|
const children = new Map();
|
|
1240
1209
|
for (const entry of entries) {
|
|
1241
1210
|
if (entry.parentUUID === null)
|
|
@@ -1244,7 +1213,7 @@ function scanRoot(input) {
|
|
|
1244
1213
|
group.push({ uuid: entry.uuid, name: entry.name });
|
|
1245
1214
|
children.set(entry.parentUUID, group);
|
|
1246
1215
|
}
|
|
1247
|
-
const
|
|
1216
|
+
const rowsFromEntries = () => entries.map((entry) => {
|
|
1248
1217
|
const versionPayload = entry.type === "workspace" || entry.type === "folder"
|
|
1249
1218
|
? membershipHash(children.get(entry.uuid) ?? [], sha256Hex)
|
|
1250
1219
|
: entry.payloadVersion;
|
|
@@ -1272,8 +1241,114 @@ function scanRoot(input) {
|
|
|
1272
1241
|
inode: entry.inode,
|
|
1273
1242
|
};
|
|
1274
1243
|
});
|
|
1275
|
-
|
|
1276
|
-
|
|
1244
|
+
const persistCurrentRows = () => {
|
|
1245
|
+
const rows = rowsFromEntries();
|
|
1246
|
+
persistRows(database, identity, resourceId, rootUUID, rows, existingRows);
|
|
1247
|
+
return rows;
|
|
1248
|
+
};
|
|
1249
|
+
const repositories = entries.filter((entry) => entry.type === "repo.git");
|
|
1250
|
+
const properDescendantOf = (candidate, ancestor) => ancestor === "" ? candidate !== "" : candidate.startsWith(`${ancestor}/`);
|
|
1251
|
+
const repositoryOwner = (entry) => repositories
|
|
1252
|
+
.filter((repository) => repository.uuid !== entry.uuid
|
|
1253
|
+
&& properDescendantOf(entry.relativePath, repository.relativePath))
|
|
1254
|
+
.sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
|
|
1255
|
+
try {
|
|
1256
|
+
for (const repository of repositories) {
|
|
1257
|
+
const repoIdentity = entries
|
|
1258
|
+
.filter((entry) => repositoryOwner(entry)?.uuid === repository.uuid)
|
|
1259
|
+
.map((entry) => ({
|
|
1260
|
+
path: repository.relativePath
|
|
1261
|
+
? entry.relativePath.slice(repository.relativePath.length + 1)
|
|
1262
|
+
: entry.relativePath,
|
|
1263
|
+
uuid: entry.uuid,
|
|
1264
|
+
type: entry.type,
|
|
1265
|
+
payloadVersion: entry.payloadVersion,
|
|
1266
|
+
}))
|
|
1267
|
+
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
1268
|
+
const priorRow = existingByPath.get(repository.relativePath);
|
|
1269
|
+
if (priorRow?.type === "repo.git") {
|
|
1270
|
+
repository.payloadVersion = priorRow.payloadVersion;
|
|
1271
|
+
repository.transportVersion = priorRow.transportVersion;
|
|
1272
|
+
}
|
|
1273
|
+
let prior = null;
|
|
1274
|
+
if (priorRow?.type === "repo.git" && priorRow.payloadVersion && priorRow.transportVersion) {
|
|
1275
|
+
const priorPath = join(cacheDir, `${priorRow.transportVersion}.bin`);
|
|
1276
|
+
if (existsSync(priorPath)) {
|
|
1277
|
+
try {
|
|
1278
|
+
const layout = inspectRepositoryTransportFile(priorPath);
|
|
1279
|
+
if (layout.stateId === priorRow.payloadVersion) {
|
|
1280
|
+
prior = {
|
|
1281
|
+
stateId: priorRow.payloadVersion,
|
|
1282
|
+
transportVersion: priorRow.transportVersion,
|
|
1283
|
+
card: layout.card,
|
|
1284
|
+
identityHash: layout.identityHash,
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
else {
|
|
1288
|
+
rmSync(priorPath, { force: true });
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
catch {
|
|
1292
|
+
// Content-addressed cache bytes are evidence, not ground. A file
|
|
1293
|
+
// that cannot prove the current envelope is a cache miss.
|
|
1294
|
+
rmSync(priorPath, { force: true });
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const previousRepositories = [...existingByPath.values()]
|
|
1299
|
+
.filter((row) => row.type === "repo.git");
|
|
1300
|
+
const previousOwner = (row) => previousRepositories
|
|
1301
|
+
.filter((candidate) => candidate.uuid !== row.uuid
|
|
1302
|
+
&& properDescendantOf(row.relativePath, candidate.relativePath))
|
|
1303
|
+
.sort((left, right) => right.relativePath.length - left.relativePath.length)[0] ?? null;
|
|
1304
|
+
const previousIdentity = prior && priorRow
|
|
1305
|
+
? [...existingByPath.values()]
|
|
1306
|
+
.filter((row) => previousOwner(row)?.uuid === priorRow.uuid)
|
|
1307
|
+
.map((row) => ({
|
|
1308
|
+
path: priorRow.relativePath
|
|
1309
|
+
? row.relativePath.slice(priorRow.relativePath.length + 1)
|
|
1310
|
+
: row.relativePath,
|
|
1311
|
+
uuid: row.uuid,
|
|
1312
|
+
type: row.type,
|
|
1313
|
+
payloadVersion: row.payloadVersion,
|
|
1314
|
+
}))
|
|
1315
|
+
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
|
|
1316
|
+
: null;
|
|
1317
|
+
const captured = captureRepository(repository.absolutePath, repoIdentity, prior, previousIdentity);
|
|
1318
|
+
repository.payloadVersion = captured.stateId;
|
|
1319
|
+
repository.transportVersion = captured.transportVersion;
|
|
1320
|
+
if (captured.bytes) {
|
|
1321
|
+
immutableWriteArtifact(cacheDir, {
|
|
1322
|
+
entityId: repository.uuid,
|
|
1323
|
+
entityType: "repo.git",
|
|
1324
|
+
kind: "git",
|
|
1325
|
+
contentHash: captured.transportVersion,
|
|
1326
|
+
}, captured.bytes);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
catch {
|
|
1331
|
+
// Type and identity are filesystem facts, so they settle locally even
|
|
1332
|
+
// when the selected repository rail cannot yet capture its artifact.
|
|
1333
|
+
// Cloud publication remains commit-last because scanAndRegister omits the
|
|
1334
|
+
// complete materialized root until every repository parcel is capturable.
|
|
1335
|
+
// Git unavailability is local to this root; Watch and unrelated roots stay
|
|
1336
|
+
// live and the next suspicion or resume retries the same ordinary scan.
|
|
1337
|
+
const rows = persistCurrentRows();
|
|
1338
|
+
return {
|
|
1339
|
+
records: rows.map((row) => row.record),
|
|
1340
|
+
rows,
|
|
1341
|
+
referenceWorkspaceIds: [...referenceWorkspaceIds],
|
|
1342
|
+
transferable: false,
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
const rows = persistCurrentRows();
|
|
1346
|
+
return {
|
|
1347
|
+
records: rows.map((row) => row.record),
|
|
1348
|
+
rows,
|
|
1349
|
+
referenceWorkspaceIds: [...referenceWorkspaceIds],
|
|
1350
|
+
transferable: true,
|
|
1351
|
+
};
|
|
1277
1352
|
}
|
|
1278
1353
|
function portablePaths(records) {
|
|
1279
1354
|
const byUuid = new Map(records.map((record) => [record.uuid, record]));
|