@amalgm/shell 0.1.33 → 0.1.35

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.
@@ -8,7 +8,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
- import { WORKSPACE_UUID as UUID, createFilesRegisterPorts, pathExists, pathWithin, referenceWorkspaceId, workspaceBindingDir, } from "./files-register-host.js";
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";
13
13
  import { inspectGitRegistration, } from "./git-registration-host.js";
14
14
  import { projectMaterializedGraph } from "./materialized-graph.js";
@@ -126,17 +126,24 @@ export class UserGroundHost {
126
126
  throw new Error("this user has no cloud entity registry");
127
127
  return cloud;
128
128
  },
129
- materializeWorkspace: async (input) => installCloudWorkspace({
130
- identity,
131
- database: this.databasePath(identity),
132
- cacheDir: this.cacheDir(identity),
133
- bindingDir,
134
- resourceId: privateEntityResourceId(identity.userId, sha256Hex),
135
- workspace: input.workspace,
136
- records: input.records,
137
- destinationParent: input.destinationParent,
138
- readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact),
139
- }),
129
+ materializeWorkspace: async (input) => {
130
+ const installed = await installCloudWorkspace({
131
+ identity,
132
+ userRoot,
133
+ database: this.databasePath(identity),
134
+ cacheDir: this.cacheDir(identity),
135
+ bindingDir,
136
+ resourceId: privateEntityResourceId(identity.userId, sha256Hex),
137
+ workspace: input.workspace,
138
+ reference: input.reference,
139
+ references: input.references,
140
+ records: input.records,
141
+ destinationParent: input.destinationParent,
142
+ readContent: (artifact) => this.downloadContent(privateEntityResourceId(identity.userId, sha256Hex), this.cacheDir(identity), artifact),
143
+ });
144
+ this.scheduleRescan();
145
+ return installed;
146
+ },
140
147
  coverWorkspace: async ({ workspaceId, baseline }) => {
141
148
  const health = this.ensureWatchers(identity, baseline === "authoritative-install" ? [workspaceId] : []);
142
149
  return { active: true, roots: health.contentHandles };
@@ -282,7 +289,10 @@ export class UserGroundHost {
282
289
  declaration: this.options.declaration ?? shippedUserHomeDeclaration,
283
290
  now: this.options.now?.() ?? new Date(),
284
291
  });
285
- scanAndRegister({ identity, userRoot, database, cacheDir });
292
+ await scanAndRegister({
293
+ identity, userRoot, database, cacheDir,
294
+ onScan: this.options.onDetectScan,
295
+ });
286
296
  return localValue(identity, userRoot, database);
287
297
  },
288
298
  install: async (_who, cloud) => {
@@ -575,11 +585,12 @@ export class UserGroundHost {
575
585
  if (!state)
576
586
  throw new Error("cloud state is unavailable for user-ground Watch");
577
587
  const observations = this.watchHost.observations();
578
- const localRecords = scanAndRegister({
588
+ const localRecords = await scanAndRegister({
579
589
  identity,
580
590
  userRoot: this.userRoot(identity),
581
591
  database: this.databasePath(identity),
582
592
  cacheDir: this.cacheDir(identity),
593
+ onScan: this.options.onDetectScan,
583
594
  suspicions: new Map(observations.map((observation) => [observation.directory, observation.suspicion.paths])),
584
595
  });
585
596
  const snapshot = snapshotFromRecords(travelingRecords(mergeMaterializedRoots(state.records, localRecords)));
@@ -744,6 +755,10 @@ function initializeDatabase(file) {
744
755
  absolute_path TEXT NOT NULL,
745
756
  device_number INTEGER NOT NULL,
746
757
  inode INTEGER NOT NULL,
758
+ byte_size INTEGER,
759
+ modified_time_ms REAL,
760
+ changed_time_ms REAL,
761
+ filesystem_mode INTEGER,
747
762
  UNIQUE(resource_id, root_uuid, relative_path)
748
763
  );
749
764
  CREATE TABLE IF NOT EXISTS cloud_outbox (
@@ -757,6 +772,18 @@ function initializeDatabase(file) {
757
772
  created_at TEXT NOT NULL
758
773
  );
759
774
  `);
775
+ const currentEntityColumns = new Set(database.prepare("PRAGMA table_info(entities)").all()
776
+ .map(({ name }) => name));
777
+ const fingerprintColumns = [
778
+ ["byte_size", "INTEGER"],
779
+ ["modified_time_ms", "REAL"],
780
+ ["changed_time_ms", "REAL"],
781
+ ["filesystem_mode", "INTEGER"],
782
+ ];
783
+ for (const [name, type] of fingerprintColumns) {
784
+ if (!currentEntityColumns.has(name))
785
+ database.exec(`ALTER TABLE entities ADD COLUMN ${name} ${type}`);
786
+ }
760
787
  return database;
761
788
  }
762
789
  function readRows(file) {
@@ -769,7 +796,9 @@ function readRows(file) {
769
796
  uuid, type, parent_uuid AS parentUUID, name, status, version,
770
797
  payload_version AS payloadVersion, transport_version AS transportVersion,
771
798
  relative_path AS relativePath, absolute_path AS absolutePath,
772
- device_number AS deviceNumber, inode
799
+ device_number AS deviceNumber, inode,
800
+ byte_size AS byteSize, modified_time_ms AS modifiedTimeMs,
801
+ changed_time_ms AS changedTimeMs, filesystem_mode AS filesystemMode
773
802
  FROM entities ORDER BY uuid
774
803
  `).all();
775
804
  }
@@ -823,7 +852,8 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
823
852
  INSERT INTO entities(
824
853
  uuid, resource_id, root_uuid, type, parent_uuid, name, status, version, payload_version,
825
854
  transport_version, relative_path, absolute_path, device_number, inode
826
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
855
+ , byte_size, modified_time_ms, changed_time_ms, filesystem_mode
856
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
827
857
  ON CONFLICT(uuid) DO UPDATE SET
828
858
  resource_id = excluded.resource_id,
829
859
  root_uuid = excluded.root_uuid,
@@ -837,7 +867,11 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
837
867
  relative_path = excluded.relative_path,
838
868
  absolute_path = excluded.absolute_path,
839
869
  device_number = excluded.device_number,
840
- inode = excluded.inode
870
+ inode = excluded.inode,
871
+ byte_size = excluded.byte_size,
872
+ modified_time_ms = excluded.modified_time_ms,
873
+ changed_time_ms = excluded.changed_time_ms,
874
+ filesystem_mode = excluded.filesystem_mode
841
875
  `);
842
876
  const remove = database.prepare("DELETE FROM entities WHERE uuid = ? AND resource_id = ? AND root_uuid = ?");
843
877
  const previousByUuid = new Map(previousRows.map((row) => [row.uuid, row]));
@@ -857,7 +891,11 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
857
891
  || old.relativePath !== row.relativePath
858
892
  || old.absolutePath !== row.absolutePath
859
893
  || old.deviceNumber !== row.deviceNumber
860
- || old.inode !== row.inode;
894
+ || old.inode !== row.inode
895
+ || old.byteSize !== row.byteSize
896
+ || old.modifiedTimeMs !== row.modifiedTimeMs
897
+ || old.changedTimeMs !== row.changedTimeMs
898
+ || old.filesystemMode !== row.filesystemMode;
861
899
  });
862
900
  const replace = database.transaction(() => {
863
901
  replaceIdentity.run(identity.userId, identity.userEmail, identity.deviceId);
@@ -866,7 +904,7 @@ function persistRows(file, identity, resourceId, rootUUID, rows, previousRows =
866
904
  remove.run(old.uuid, resourceId, rootUUID);
867
905
  }
868
906
  for (const row of changed)
869
- 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);
907
+ 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);
870
908
  });
871
909
  replace();
872
910
  }
@@ -943,6 +981,24 @@ function directoryExists(path) {
943
981
  return false;
944
982
  }
945
983
  }
984
+ function statFingerprint(stats) {
985
+ return {
986
+ deviceNumber: stats.dev,
987
+ inode: stats.ino,
988
+ byteSize: stats.size,
989
+ modifiedTimeMs: stats.mtimeMs,
990
+ changedTimeMs: stats.ctimeMs,
991
+ filesystemMode: stats.mode,
992
+ };
993
+ }
994
+ function sameStatFingerprint(row, stats) {
995
+ return row.deviceNumber === stats.dev
996
+ && row.inode === stats.ino
997
+ && row.byteSize === stats.size
998
+ && row.modifiedTimeMs === stats.mtimeMs
999
+ && row.changedTimeMs === stats.ctimeMs
1000
+ && row.filesystemMode === stats.mode;
1001
+ }
946
1002
  /** The binding is the durable machine-local declaration of an external root.
947
1003
  * Registration and Watch both derive their roots from this one inventory, so
948
1004
  * correctness never depends on receiving the reference-rendering OS event. */
@@ -966,8 +1022,8 @@ function materializedWorkspaceBindings(bindingDir) {
966
1022
  }
967
1023
  return bindings.sort((left, right) => left.workspaceId.localeCompare(right.workspaceId));
968
1024
  }
969
- function scanAndRegister(input) {
970
- const { identity, userRoot, database, cacheDir, suspicions } = input;
1025
+ async function scanAndRegister(input) {
1026
+ const { identity, userRoot, database, cacheDir, suspicions, onScan } = input;
971
1027
  const resourceId = privateEntityResourceId(identity.userId, sha256Hex);
972
1028
  const existing = readRows(database);
973
1029
  const ignoreFile = join(userRoot, ".amalgmignore");
@@ -989,7 +1045,31 @@ function scanAndRegister(input) {
989
1045
  const missingPortableReference = [...boundRoots.keys()]
990
1046
  .some((workspaceId) => !existingReferenceIds.has(workspaceId));
991
1047
  const deferredRootIds = new Set();
992
- const core = scanRoot({
1048
+ const scan = async (parameters) => {
1049
+ const counters = {
1050
+ entries: 0,
1051
+ metadataReused: 0,
1052
+ contentReads: 0,
1053
+ gitInspections: 0,
1054
+ repositoryCaptures: 0,
1055
+ };
1056
+ const started = performance.now();
1057
+ const result = scanRoot({ ...parameters, evidence: counters });
1058
+ onScan?.({
1059
+ rootId: parameters.rootUUID,
1060
+ directory: parameters.rootPath,
1061
+ scope: parameters.suspects === null || parameters.suspects === undefined
1062
+ ? "complete"
1063
+ : parameters.suspects.length === 0 ? "none" : "paths",
1064
+ ...counters,
1065
+ durationMs: performance.now() - started,
1066
+ });
1067
+ // Each root commits its local projection before Detect yields. Native
1068
+ // callbacks can therefore preserve new suspicion between large roots.
1069
+ await new Promise((resolve) => setImmediate(resolve));
1070
+ return result;
1071
+ };
1072
+ const core = await scan({
993
1073
  identity,
994
1074
  resourceId,
995
1075
  rootUUID: coreUUID,
@@ -1023,7 +1103,7 @@ function scanAndRegister(input) {
1023
1103
  }
1024
1104
  const existingRoot = existing.find((row) => row.uuid === workspaceId && row.parentUUID === null);
1025
1105
  const existingWorkspaceRows = existing.filter((row) => row.rootUUID === workspaceId);
1026
- const scanned = scanRoot({
1106
+ const scanned = await scan({
1027
1107
  identity,
1028
1108
  resourceId,
1029
1109
  rootUUID: workspaceId,
@@ -1045,7 +1125,7 @@ function scanAndRegister(input) {
1045
1125
  .map(portableRecord);
1046
1126
  }
1047
1127
  function scanRoot(input) {
1048
- const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, } = input;
1128
+ const { identity, resourceId, rootUUID, rootName, rootPath, rootType, database, cacheDir, policy, bindingDir, suspects = null, evidence, } = input;
1049
1129
  const existingRows = (input.existingRows ?? readRows(database))
1050
1130
  .filter((row) => row.resourceId === resourceId && row.rootUUID === rootUUID);
1051
1131
  const existingByPath = new Map(existingRows.map((row) => [row.relativePath, row]));
@@ -1063,6 +1143,10 @@ function scanRoot(input) {
1063
1143
  absolutePath: row.absolutePath,
1064
1144
  deviceNumber: row.deviceNumber,
1065
1145
  inode: row.inode,
1146
+ byteSize: row.byteSize,
1147
+ modifiedTimeMs: row.modifiedTimeMs,
1148
+ changedTimeMs: row.changedTimeMs,
1149
+ filesystemMode: row.filesystemMode,
1066
1150
  })),
1067
1151
  referenceWorkspaceIds: existingRows
1068
1152
  .filter((row) => row.type === "reference" && row.payloadVersion && UUID.test(row.payloadVersion))
@@ -1073,6 +1157,7 @@ function scanRoot(input) {
1073
1157
  const entries = [];
1074
1158
  const referenceWorkspaceIds = new Set();
1075
1159
  const rootStats = lstatSync(rootPath);
1160
+ evidence && (evidence.entries += 1);
1076
1161
  entries.push({
1077
1162
  uuid: rootUUID,
1078
1163
  type: rootType,
@@ -1082,8 +1167,7 @@ function scanRoot(input) {
1082
1167
  absolutePath: rootPath,
1083
1168
  payloadVersion: null,
1084
1169
  transportVersion: null,
1085
- deviceNumber: rootStats.dev,
1086
- inode: rootStats.ino,
1170
+ ...statFingerprint(rootStats),
1087
1171
  });
1088
1172
  const touchesSuspicion = (path) => suspects === null
1089
1173
  || pathIsSuspect(path, suspects)
@@ -1122,13 +1206,21 @@ function scanRoot(input) {
1122
1206
  continue;
1123
1207
  const absolutePath = join(directory, child.name);
1124
1208
  const stats = lstatSync(absolutePath);
1209
+ evidence && (evidence.entries += 1);
1125
1210
  const existing = existingByPath.get(relativePath);
1126
1211
  const uuid = existing?.uuid || randomUUID();
1127
- const observe = existing === undefined || touchesSuspicion(relativePath);
1212
+ const stableDuringCompleteCatchUp = suspects === null
1213
+ && existing !== undefined
1214
+ && sameStatFingerprint(existing, stats);
1215
+ const observe = existing === undefined
1216
+ || (suspects === null ? !stableDuringCompleteCatchUp : touchesSuspicion(relativePath));
1217
+ if (!observe && suspects === null)
1218
+ evidence && (evidence.metadataReused += 1);
1128
1219
  const repositoryPath = activeRepository
1129
1220
  ? slashPath(relative(activeRepository.root, absolutePath))
1130
1221
  : null;
1131
1222
  if (observe && repositoryPath && activeRepository && !activeRepository.evidence) {
1223
+ evidence && (evidence.gitInspections += 1);
1132
1224
  try {
1133
1225
  activeRepository.evidence = inspectGitRegistration(activeRepository.root, activeRepository.evidencePaths);
1134
1226
  }
@@ -1164,6 +1256,7 @@ function scanRoot(input) {
1164
1256
  payloadVersion = indexed.payloadVersion;
1165
1257
  }
1166
1258
  else {
1259
+ evidence && (evidence.contentReads += 1);
1167
1260
  const bytes = Buffer.from(readlinkSync(absolutePath), "utf8");
1168
1261
  payloadVersion = sha256Hex(bytes);
1169
1262
  if (!activeRepository)
@@ -1185,6 +1278,7 @@ function scanRoot(input) {
1185
1278
  payloadVersion = indexed.payloadVersion;
1186
1279
  }
1187
1280
  else {
1281
+ evidence && (evidence.contentReads += 1);
1188
1282
  const bytes = readFileSync(absolutePath);
1189
1283
  type = fileType(bytes);
1190
1284
  payloadVersion = sha256Hex(bytes);
@@ -1209,8 +1303,7 @@ function scanRoot(input) {
1209
1303
  absolutePath,
1210
1304
  payloadVersion,
1211
1305
  transportVersion: null,
1212
- deviceNumber: stats.dev,
1213
- inode: stats.ino,
1306
+ ...statFingerprint(stats),
1214
1307
  });
1215
1308
  if (stats.isDirectory() && !stats.isSymbolicLink()) {
1216
1309
  visit(absolutePath, uuid, type, relativePath, type === "repo.git"
@@ -1258,6 +1351,10 @@ function scanRoot(input) {
1258
1351
  absolutePath: entry.absolutePath,
1259
1352
  deviceNumber: entry.deviceNumber,
1260
1353
  inode: entry.inode,
1354
+ byteSize: entry.byteSize,
1355
+ modifiedTimeMs: entry.modifiedTimeMs,
1356
+ changedTimeMs: entry.changedTimeMs,
1357
+ filesystemMode: entry.filesystemMode,
1261
1358
  };
1262
1359
  });
1263
1360
  const persistCurrentRows = () => {
@@ -1333,6 +1430,7 @@ function scanRoot(input) {
1333
1430
  }))
1334
1431
  .sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0)
1335
1432
  : null;
1433
+ evidence && (evidence.repositoryCaptures += 1);
1336
1434
  const captured = captureRepository(repository.absolutePath, repoIdentity, prior, previousIdentity);
1337
1435
  repository.payloadVersion = captured.stateId;
1338
1436
  repository.transportVersion = captured.transportVersion;
@@ -1611,14 +1709,111 @@ async function materializeRecords(input) {
1611
1709
  record,
1612
1710
  relativePath,
1613
1711
  absolutePath,
1614
- deviceNumber: stats.dev,
1615
- inode: stats.ino,
1712
+ ...statFingerprint(stats),
1616
1713
  };
1617
1714
  });
1618
1715
  return { rows, repositories };
1619
1716
  }
1717
+ /** Refresh the always-materialized portable reference inventory selected by
1718
+ * Live. Paths and bindings are local; UUIDs, parents, names, and versions are
1719
+ * the same cloud entities on every machine. */
1720
+ function installCloudWorkspaceReferences(input) {
1721
+ const { identity, userRoot, database, bindingDir, resourceId, workspace, references, } = input;
1722
+ const localRows = readRows(database);
1723
+ const rendered = [];
1724
+ try {
1725
+ for (const reference of references) {
1726
+ if (reference.type !== "reference"
1727
+ || reference.status !== "active"
1728
+ || !reference.payloadVersion
1729
+ || reference.parentUUID === null) {
1730
+ throw new Error("cloud registry has an invalid durable reference");
1731
+ }
1732
+ const parent = localRows.find((row) => row.uuid === reference.parentUUID);
1733
+ if (!parent) {
1734
+ throw new Error(`cloud reference ${reference.uuid} parent is not materialized`);
1735
+ }
1736
+ const link = ensureWorkspaceReference({
1737
+ userRoot,
1738
+ bindingDir,
1739
+ workspaceId: reference.payloadVersion,
1740
+ workspaceName: reference.name,
1741
+ exactName: true,
1742
+ requireBinding: reference.payloadVersion === workspace.uuid,
1743
+ });
1744
+ const stats = lstatSync(link.referencePath);
1745
+ rendered.push({
1746
+ created: link.created,
1747
+ referencePath: link.referencePath,
1748
+ rootUUID: parent.rootUUID,
1749
+ row: {
1750
+ record: reference,
1751
+ relativePath: slashPath(relative(userRoot, link.referencePath)),
1752
+ absolutePath: link.referencePath,
1753
+ ...statFingerprint(stats),
1754
+ },
1755
+ });
1756
+ }
1757
+ const rootIds = new Set(rendered.map(({ rootUUID }) => rootUUID));
1758
+ for (const rootUUID of rootIds) {
1759
+ const priorReferences = localRows.filter((row) => row.rootUUID === rootUUID && row.type === "reference");
1760
+ const currentReferences = rendered.filter((entry) => entry.rootUUID === rootUUID);
1761
+ const parentIds = new Set([
1762
+ ...priorReferences.map((row) => row.parentUUID),
1763
+ ...currentReferences.map(({ row }) => row.record.parentUUID),
1764
+ ].filter((uuid) => uuid !== null));
1765
+ const priorParents = [...parentIds].map((parentId) => {
1766
+ const parent = localRows.find((row) => row.rootUUID === rootUUID && row.uuid === parentId);
1767
+ if (!parent || (parent.type !== "workspace" && parent.type !== "folder")) {
1768
+ throw new Error(`cloud reference parent ${parentId} is not a local container`);
1769
+ }
1770
+ return parent;
1771
+ });
1772
+ const parents = priorParents.map((parent) => {
1773
+ const parentId = parent.uuid;
1774
+ const stats = lstatSync(parent.absolutePath);
1775
+ const ordinaryChildren = localRows.filter((row) => row.rootUUID === rootUUID
1776
+ && row.parentUUID === parentId
1777
+ && row.type !== "reference"
1778
+ && row.status === "active");
1779
+ const referenceChildren = currentReferences
1780
+ .filter(({ row }) => row.record.parentUUID === parentId)
1781
+ .map(({ row }) => row.record);
1782
+ const versionPayload = membershipHash([
1783
+ ...ordinaryChildren,
1784
+ ...referenceChildren,
1785
+ ], sha256Hex);
1786
+ return {
1787
+ record: {
1788
+ ...portableRecord(parent),
1789
+ version: canonicalVersion({
1790
+ type: parent.type,
1791
+ parentUUID: parent.parentUUID,
1792
+ name: parent.name,
1793
+ status: parent.status,
1794
+ payloadVersion: versionPayload,
1795
+ }, sha256Hex),
1796
+ },
1797
+ relativePath: parent.relativePath,
1798
+ absolutePath: parent.absolutePath,
1799
+ ...statFingerprint(stats),
1800
+ };
1801
+ });
1802
+ persistRows(database, identity, resourceId, rootUUID, [...currentReferences.map(({ row }) => row), ...parents], [...priorReferences, ...priorParents]);
1803
+ }
1804
+ }
1805
+ catch (error) {
1806
+ for (const entry of rendered) {
1807
+ if (entry.created && pathExists(entry.referencePath)) {
1808
+ rmSync(entry.referencePath, { force: true });
1809
+ }
1810
+ }
1811
+ throw error;
1812
+ }
1813
+ return references;
1814
+ }
1620
1815
  async function installCloudWorkspace(input) {
1621
- const { identity, database, cacheDir, bindingDir, resourceId, workspace, records, destinationParent, readContent, } = input;
1816
+ const { identity, userRoot, database, cacheDir, bindingDir, resourceId, workspace, reference, references, records, destinationParent, readContent, } = input;
1622
1817
  const existing = readRows(database).filter((row) => row.rootUUID === workspace.uuid);
1623
1818
  if (existing.length > 0) {
1624
1819
  const root = existing.find((row) => row.uuid === workspace.uuid);
@@ -1629,10 +1824,21 @@ async function installCloudWorkspace(input) {
1629
1824
  if (!sameRecords(travelingRecords(localRecords), records)) {
1630
1825
  throw new Error(`local workspace ${workspace.uuid} differs from its cloud graph`);
1631
1826
  }
1827
+ const installedReferences = installCloudWorkspaceReferences({
1828
+ identity,
1829
+ userRoot,
1830
+ database,
1831
+ bindingDir,
1832
+ resourceId,
1833
+ workspace,
1834
+ references,
1835
+ });
1632
1836
  return {
1633
1837
  path: root.absolutePath,
1634
1838
  rows: existing.length,
1635
1839
  records: localRecords,
1840
+ reference,
1841
+ references: installedReferences,
1636
1842
  alreadyMaterialized: true,
1637
1843
  };
1638
1844
  }
@@ -1656,14 +1862,26 @@ async function installCloudWorkspace(input) {
1656
1862
  })), sha256Hex);
1657
1863
  symlinkSync(destination, binding, "dir");
1658
1864
  persistRows(database, identity, resourceId, workspace.uuid, rows);
1865
+ const installedReferences = installCloudWorkspaceReferences({
1866
+ identity,
1867
+ userRoot,
1868
+ database,
1869
+ bindingDir,
1870
+ resourceId,
1871
+ workspace,
1872
+ references,
1873
+ });
1659
1874
  return {
1660
1875
  path: destination,
1661
1876
  rows: rows.length,
1662
1877
  records: rows.map((row) => row.record),
1878
+ reference,
1879
+ references: installedReferences,
1663
1880
  alreadyMaterialized: false,
1664
1881
  };
1665
1882
  }
1666
1883
  catch (error) {
1884
+ deleteRootRows(database, resourceId, workspace.uuid);
1667
1885
  if (pathExists(binding))
1668
1886
  rmSync(binding, { force: true });
1669
1887
  if (pathExists(destination))