@tiphareth/dsh-hardssh 0.1.2 → 0.2.2

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.
Files changed (174) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +49 -25
  3. package/SKILLS.md +140 -0
  4. package/lib/base/index.js +95 -0
  5. package/lib/client.js +1171 -1321
  6. package/lib/fs.js +307 -416
  7. package/lib/index.js +5428 -2991
  8. package/lib/ledger-B-LXlftp.js +43 -0
  9. package/lib/ledger-D2ezq1iW.js +376 -0
  10. package/lib/model-Cp7f70Mb.js +5 -0
  11. package/lib/registry-CViuzKYI.js +421 -0
  12. package/lib/subprocess.js +92 -549
  13. package/lib/types/backend.d.ts +73 -78
  14. package/lib/types/base/capability.d.ts +4 -0
  15. package/lib/types/base/ledger-router.d.ts +42 -26
  16. package/lib/types/base/ledger.d.ts +30 -19
  17. package/lib/types/base/model.d.ts +17 -8
  18. package/lib/types/base/plugin.d.ts +11 -0
  19. package/lib/types/base/registry.d.ts +28 -31
  20. package/lib/types/base/router.d.ts +0 -7
  21. package/lib/types/client/api.d.ts +8 -8
  22. package/lib/types/client/connect-host.d.ts +13 -0
  23. package/lib/types/client/index.d.ts +6 -4
  24. package/lib/types/client/locales.d.ts +15 -1
  25. package/lib/types/client/session-connect-gate.d.ts +71 -0
  26. package/lib/types/client/ssh/api.d.ts +11 -9
  27. package/lib/types/client/ssh/apply.d.ts +14 -8
  28. package/lib/types/client/ssh/locales.d.ts +8 -1
  29. package/lib/types/client/ssh/ops-tab.d.ts +34 -0
  30. package/lib/types/client/ssh/panel/ClusterTab.d.ts +3 -1
  31. package/lib/types/client/ssh/panel/ConnectionErrorDialog.d.ts +8 -0
  32. package/lib/types/client/ssh/panel/SessionSecretDialog.d.ts +4 -1
  33. package/lib/types/client/ssh/panel/SshPanel.d.ts +8 -8
  34. package/lib/types/client/ssh/panel/TerminalTab.d.ts +3 -5
  35. package/lib/types/client/ssh/panel/TransferTab.d.ts +5 -1
  36. package/lib/types/client/ssh/panel/TunnelsTab.d.ts +3 -1
  37. package/lib/types/client/ssh/session-target.d.ts +17 -0
  38. package/lib/types/client/state.d.ts +4 -1
  39. package/lib/types/client/workspace-panel-entry.d.ts +30 -0
  40. package/lib/types/client/workspace-panel.d.ts +24 -0
  41. package/lib/types/client-http.d.ts +77 -6
  42. package/lib/types/core.d.ts +5 -16
  43. package/lib/types/fs.d.ts +18 -25
  44. package/lib/types/index.d.ts +138 -9
  45. package/lib/types/ledger.d.ts +16 -111
  46. package/lib/types/protocol.d.ts +0 -5
  47. package/lib/types/providers/index.d.ts +6 -3
  48. package/lib/types/providers/local/provider.d.ts +83 -23
  49. package/lib/types/providers/ssh/provider.d.ts +24 -57
  50. package/lib/types/remote/environment.d.ts +2 -0
  51. package/lib/types/remote/remote-fs.d.ts +12 -1
  52. package/lib/types/remote/remote-process.d.ts +22 -1
  53. package/lib/types/remote/remote-subprocess.d.ts +13 -1
  54. package/lib/types/remote/remote-terminal.d.ts +4 -0
  55. package/lib/types/remote-search.d.ts +3 -3
  56. package/lib/types/routes.d.ts +36 -8
  57. package/lib/types/runtime/dsh-capabilities.d.ts +18 -0
  58. package/lib/types/runtime/workspace-core.d.ts +73 -38
  59. package/lib/types/runtime/workspace-migration.d.ts +113 -0
  60. package/lib/types/ssh/connection/lease.d.ts +23 -0
  61. package/lib/types/ssh/connection/manager.d.ts +359 -0
  62. package/lib/types/ssh/connection/pool.d.ts +8 -1
  63. package/lib/types/ssh/engine.d.ts +97 -267
  64. package/lib/types/ssh/known-hosts.d.ts +22 -5
  65. package/lib/types/ssh/local-transfer-policy.d.ts +20 -0
  66. package/lib/types/ssh/plugin.d.ts +10 -15
  67. package/lib/types/ssh/protocol.d.ts +18 -0
  68. package/lib/types/ssh/routes.d.ts +15 -7
  69. package/lib/types/ssh/sftp/service.d.ts +216 -0
  70. package/lib/types/ssh/store.d.ts +20 -8
  71. package/lib/types/ssh/terminal/service.d.ts +49 -0
  72. package/lib/types/ssh/tunnel/service.d.ts +26 -0
  73. package/lib/types/ssh/vault.d.ts +51 -4
  74. package/lib/types/subprocess.d.ts +13 -21
  75. package/lib/types/switch/switch-fs.d.ts +52 -2
  76. package/lib/types/switch/switch-subprocess.d.ts +32 -3
  77. package/lib/types/tools.d.ts +7 -6
  78. package/lib/types/workspace-tool-ops.d.ts +33 -0
  79. package/lib/types/workspace.d.ts +18 -0
  80. package/lib/vault-3gpWct2Q.js +559 -0
  81. package/lib/workspace.js +2 -0
  82. package/package.json +58 -25
  83. package/scripts/export-legacy-workspaces.mjs +136 -0
  84. package/src/backend.ts +155 -483
  85. package/src/base/capability.ts +4 -0
  86. package/src/base/ledger-router.ts +214 -65
  87. package/src/base/ledger.ts +175 -48
  88. package/src/base/model.ts +18 -8
  89. package/src/base/plugin.ts +15 -4
  90. package/src/base/registry.ts +49 -41
  91. package/src/base/router.ts +0 -8
  92. package/src/client/api.ts +8 -36
  93. package/src/client/connect-host.ts +199 -0
  94. package/src/client/directory-flow.tsx +91 -36
  95. package/src/client/index.ts +56 -53
  96. package/src/client/locales.ts +14 -0
  97. package/src/client/session-connect-gate.ts +163 -0
  98. package/src/client/ssh/api.ts +245 -108
  99. package/src/client/ssh/apply.ts +23 -24
  100. package/src/client/ssh/locales.ts +16 -2
  101. package/src/client/ssh/ops-tab.tsx +79 -0
  102. package/src/client/ssh/panel/ClusterTab.tsx +9 -20
  103. package/src/client/ssh/panel/ConnectionErrorDialog.tsx +33 -0
  104. package/src/client/ssh/panel/SessionSecretDialog.tsx +5 -1
  105. package/src/client/ssh/panel/SshPanel.tsx +81 -77
  106. package/src/client/ssh/panel/TerminalTab.tsx +18 -37
  107. package/src/client/ssh/panel/TransferTab.tsx +43 -33
  108. package/src/client/ssh/panel/TunnelsTab.tsx +9 -26
  109. package/src/client/ssh/panel/panel.module.css +90 -127
  110. package/src/client/ssh/session-target.ts +69 -0
  111. package/src/client/state.ts +9 -1
  112. package/src/client/workspace-badges.ts +99 -96
  113. package/src/client/workspace-panel-entry.tsx +91 -0
  114. package/src/client/workspace-panel.tsx +227 -0
  115. package/src/client/workspace.module.css +76 -2
  116. package/src/client-http.ts +118 -18
  117. package/src/core.ts +37 -47
  118. package/src/fs.ts +137 -85
  119. package/src/index.ts +535 -244
  120. package/src/ledger.ts +62 -416
  121. package/src/protocol.ts +0 -6
  122. package/src/providers/index.ts +8 -4
  123. package/src/providers/local/provider.ts +259 -87
  124. package/src/providers/ssh/provider.ts +158 -155
  125. package/src/remote/environment.ts +22 -1
  126. package/src/remote/remote-fs.ts +123 -34
  127. package/src/remote/remote-process.ts +265 -203
  128. package/src/remote/remote-subprocess.ts +61 -18
  129. package/src/remote/remote-terminal.ts +106 -21
  130. package/src/remote-search.ts +122 -26
  131. package/src/routes.ts +416 -395
  132. package/src/runtime/dsh-capabilities.ts +19 -0
  133. package/src/runtime/workspace-core.ts +171 -88
  134. package/src/runtime/workspace-migration.ts +472 -0
  135. package/src/ssh/connection/lease.ts +35 -0
  136. package/src/ssh/connection/manager.ts +1083 -0
  137. package/src/ssh/connection/pool.ts +341 -275
  138. package/src/ssh/engine.ts +269 -1477
  139. package/src/ssh/known-hosts.ts +42 -18
  140. package/src/ssh/local-transfer-policy.ts +83 -0
  141. package/src/ssh/plugin.ts +20 -19
  142. package/src/ssh/protocol.ts +22 -1
  143. package/src/ssh/routes.ts +419 -187
  144. package/src/ssh/sftp/service.ts +967 -0
  145. package/src/ssh/store.ts +169 -72
  146. package/src/ssh/terminal/service.ts +177 -0
  147. package/src/ssh/tools.ts +42 -20
  148. package/src/ssh/tunnel/service.ts +217 -0
  149. package/src/ssh/vault.ts +245 -91
  150. package/src/subprocess.ts +88 -71
  151. package/src/switch/switch-fs.ts +141 -10
  152. package/src/switch/switch-subprocess.ts +80 -6
  153. package/src/tools.ts +217 -221
  154. package/src/workspace-tool-ops.ts +101 -0
  155. package/src/workspace.ts +52 -0
  156. package/lib/environment-BL1jddfB.js +0 -449
  157. package/lib/switch-fs-CAJpFY9C.js +0 -193
  158. package/lib/switch-fs-RrZtG2gv.js +0 -210
  159. package/lib/types/client/manager-button.d.ts +0 -32
  160. package/lib/types/client/ssh/mount.d.ts +0 -13
  161. package/lib/types/client/ssh/panel/HostsTab.d.ts +0 -10
  162. package/lib/types/client/ssh/panel/controller.d.ts +0 -23
  163. package/lib/types/client/ssh/sidebar-entry.d.ts +0 -25
  164. package/lib/types/client/workspace-gate.d.ts +0 -15
  165. package/lib/types/remote-runner.d.ts +0 -83
  166. package/lib/types/seam-state.d.ts +0 -69
  167. package/src/client/manager-button.tsx +0 -269
  168. package/src/client/ssh/mount.tsx +0 -83
  169. package/src/client/ssh/panel/HostsTab.tsx +0 -236
  170. package/src/client/ssh/panel/controller.ts +0 -48
  171. package/src/client/ssh/sidebar-entry.ts +0 -123
  172. package/src/client/workspace-gate.ts +0 -225
  173. package/src/remote-runner.ts +0 -201
  174. package/src/seam-state.ts +0 -185
@@ -0,0 +1,43 @@
1
+ import { join, posix } from "node:path";
2
+ import { homedir } from "node:os";
3
+ //#region src/ledger.ts
4
+ /**
5
+ * SSH-bound workspace path helpers.
6
+ *
7
+ * This module is the last remaining piece of the retired SSH ledger module:
8
+ * the `SshWorkspaceLedger` class was removed with the generic cutover, so what
9
+ * survives here are the anchor/path helpers the generic runtime still shares —
10
+ * the managed SSH anchor layout (`~/.dsh/ssh-workspaces/<id>`), the legacy
11
+ * ledger file location (the one-time import source), the client DTO title
12
+ * default, and the lexical anchor-containment helpers used by fs/subprocess
13
+ * fail-closed gating. Record persistence itself is owned by the generic
14
+ * `WorkspaceLedger` (`./base/ledger.ts`).
15
+ *
16
+ * @module dsh-hardssh/ledger
17
+ */
18
+ /** The frozen legacy SSH ledger location: ~/.dsh/dsh-hardssh-workspaces.json.
19
+ * Only read once by the startup import when no cutover marker exists. */
20
+ function ledgerPath() {
21
+ return join(homedir(), ".dsh", "dsh-hardssh-workspaces.json");
22
+ }
23
+ /**
24
+ * Normalize a remote POSIX root: keep '/' as '/', collapse repeated
25
+ * separators and dot segments, reject relative paths and NUL.
26
+ */
27
+ function normalizeRemoteRoot(raw) {
28
+ if (raw.includes("\0")) throw new Error(`remoteRoot must not contain NUL (got '${raw}')`);
29
+ const normalized = posix.normalize(raw.trim());
30
+ if (!normalized.startsWith("/")) throw new Error(`remoteRoot must be an absolute POSIX path (got '${raw}')`);
31
+ return normalized === "/" ? "/" : normalized.replace(/\/+$/, "");
32
+ }
33
+ /** Anchor roots: ~/.dsh/ssh-workspaces/<id>/ — visible in the sidebar as
34
+ * ordinary host workspaces (and thus selectable for sessions). */
35
+ function anchorRoot() {
36
+ return join(homedir(), ".dsh", "ssh-workspaces");
37
+ }
38
+ /** Default record title when the user gives none. */
39
+ function defaultTitle(remoteRoot, alias) {
40
+ return `${alias}:${remoteRoot.split("/").filter(Boolean).pop() ?? remoteRoot}`;
41
+ }
42
+ //#endregion
43
+ export { normalizeRemoteRoot as i, defaultTitle as n, ledgerPath as r, anchorRoot as t };
@@ -0,0 +1,376 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { copyFile, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
5
+ //#region src/base/ledger.ts
6
+ /**
7
+ * Provider-neutral workspace ledger with strict loading, atomic persistence,
8
+ * detached snapshots, and one serialized mutation queue.
9
+ */
10
+ /** Normalize an anchor for comparison (Windows case-insensitive; POSIX not). */
11
+ function normalizeAnchorPath(path) {
12
+ if (/^[a-zA-Z]:[\\/]/.test(path) || path.includes("\\")) {
13
+ const normalized = path.replace(/\//g, "\\");
14
+ return trimTrailing(normalized, /^[a-zA-Z]:\\/.test(normalized) ? 3 : 0).toLowerCase();
15
+ }
16
+ return trimTrailing(path, path.startsWith("/") ? 1 : 0);
17
+ }
18
+ /** True when `candidate` equals `anchor` or is one of its descendants (lexical). */
19
+ function isPathUnderAnchor(anchor, candidate) {
20
+ const normAnchor = normalizeAnchorPath(anchor);
21
+ const normCandidate = normalizeAnchorPath(candidate);
22
+ if (normCandidate === normAnchor) return true;
23
+ const sep = normAnchor.includes("\\") ? "\\" : "/";
24
+ const prefix = normAnchor.endsWith(sep) ? normAnchor : `${normAnchor}${sep}`;
25
+ return normCandidate.startsWith(prefix);
26
+ }
27
+ function trimTrailing(path, minimumLength) {
28
+ let end = path.length;
29
+ while (end > minimumLength && (path[end - 1] === "/" || path[end - 1] === "\\")) end -= 1;
30
+ return path.slice(0, end);
31
+ }
32
+ /** Generic ledger schema currently accepted by strict loading. */
33
+ const WORKSPACE_LEDGER_SCHEMA_VERSION = 1;
34
+ /**
35
+ * The generic ledger. The in-memory state changes only after an atomic save,
36
+ * and every public observation is detached from that state.
37
+ */
38
+ var WorkspaceLedger = class {
39
+ fileOverride;
40
+ anchorOverride;
41
+ records;
42
+ loadPromise;
43
+ anchorIndex = [];
44
+ mutationTail = Promise.resolve();
45
+ currentRevision = 0;
46
+ listeners = /* @__PURE__ */ new Set();
47
+ constructor(fileOverride, anchorOverride) {
48
+ this.fileOverride = fileOverride;
49
+ this.anchorOverride = anchorOverride;
50
+ }
51
+ file() {
52
+ return this.fileOverride ?? "";
53
+ }
54
+ anchors() {
55
+ return this.anchorOverride ?? "";
56
+ }
57
+ async ensureLoaded() {
58
+ if (this.records !== void 0) return;
59
+ if (this.loadPromise === void 0) this.loadPromise = this.readRecords();
60
+ await this.loadPromise;
61
+ }
62
+ async readRecords() {
63
+ let parsed;
64
+ try {
65
+ const text = await readFile(this.file(), "utf8");
66
+ parsed = JSON.parse(text);
67
+ } catch (error) {
68
+ if (isNodeErrorCode(error, "ENOENT")) {
69
+ this.records = [];
70
+ this.reindex();
71
+ return;
72
+ }
73
+ throw error;
74
+ }
75
+ if (!Array.isArray(parsed)) throw new Error("WorkspaceLedger: expected a JSON array");
76
+ assertValidRecords(parsed);
77
+ this.records = cloneRecords(parsed);
78
+ this.reindex();
79
+ }
80
+ reindex() {
81
+ this.anchorIndex = (this.records ?? []).filter((record) => record.anchor !== void 0).map((record) => ({
82
+ anchor: normalizeAnchorPath(record.anchor.path),
83
+ record
84
+ })).sort((a, b) => b.anchor.length - a.anchor.length);
85
+ }
86
+ async save(nextRecords) {
87
+ assertValidRecords(nextRecords);
88
+ const target = this.file();
89
+ if (target === "") throw new Error("WorkspaceLedger: no persistence file configured");
90
+ await mkdir(dirname(target), { recursive: true });
91
+ const temporary = `${target}.tmp-${process.pid}-${Date.now()}-${randomUUID()}`;
92
+ const lastGood = `${target}.last-good`;
93
+ try {
94
+ await writeFile(temporary, JSON.stringify(nextRecords, null, 2), "utf8");
95
+ const hadPrevious = await copyFile(target, lastGood).then(() => true, (error) => {
96
+ if (!isNodeErrorCode(error, "ENOENT")) console.warn(`[dsh-hardssh] could not refresh the ledger recovery copy: ${error instanceof Error ? error.message : String(error)}`);
97
+ return false;
98
+ });
99
+ await rename(temporary, target);
100
+ if (!hadPrevious) await copyFile(target, lastGood).catch(() => void 0);
101
+ } catch (error) {
102
+ await rm(temporary, { force: true }).catch(() => void 0);
103
+ throw error;
104
+ }
105
+ }
106
+ enqueueMutation(operation) {
107
+ const result = this.mutationTail.then(operation);
108
+ this.mutationTail = result.then(() => void 0, () => void 0);
109
+ return result;
110
+ }
111
+ commit(nextRecords, change) {
112
+ this.records = cloneRecords(nextRecords);
113
+ this.reindex();
114
+ this.currentRevision += 1;
115
+ this.emit({
116
+ ...change,
117
+ revision: this.currentRevision
118
+ });
119
+ }
120
+ emit(change) {
121
+ for (const listener of [...this.listeners]) try {
122
+ listener(cloneChange(change));
123
+ } catch (error) {
124
+ console.warn("[dsh-workspace] ledger subscriber failed:", error instanceof Error ? error.message : String(error));
125
+ }
126
+ }
127
+ async load() {
128
+ await this.ensureLoaded();
129
+ return cloneRecords(this.records ?? []);
130
+ }
131
+ async list() {
132
+ await this.ensureLoaded();
133
+ return cloneRecords(this.records ?? []);
134
+ }
135
+ async snapshot() {
136
+ await this.ensureLoaded();
137
+ return {
138
+ revision: this.currentRevision,
139
+ records: cloneRecords(this.records ?? [])
140
+ };
141
+ }
142
+ snapshotSync() {
143
+ return {
144
+ revision: this.currentRevision,
145
+ records: cloneRecords(this.records ?? [])
146
+ };
147
+ }
148
+ revision() {
149
+ return this.currentRevision;
150
+ }
151
+ subscribe(listener) {
152
+ this.listeners.add(listener);
153
+ let disposed = false;
154
+ return () => {
155
+ if (disposed) return;
156
+ disposed = true;
157
+ this.listeners.delete(listener);
158
+ };
159
+ }
160
+ async get(id) {
161
+ await this.ensureLoaded();
162
+ const record = this.records?.find((candidate) => candidate.id === id);
163
+ return record === void 0 ? void 0 : cloneRecord(record);
164
+ }
165
+ async findByAnchor(path) {
166
+ await this.ensureLoaded();
167
+ return this.findByAnchorSync(path);
168
+ }
169
+ findByAnchorSync(path) {
170
+ const canonical = safeRealpathSync(path);
171
+ if (canonical === void 0) return void 0;
172
+ for (const { anchor, record } of this.anchorIndex) if (isPathUnderAnchor(anchor, canonical)) return cloneRecord(record);
173
+ }
174
+ /** Create cannot preserve caller timestamps, so migration uses replaceAll instead. */
175
+ async create(input) {
176
+ return this.enqueueMutation(async () => {
177
+ await this.ensureLoaded();
178
+ const now = (/* @__PURE__ */ new Date()).toISOString();
179
+ const record = cloneRecord({
180
+ schemaVersion: input.schemaVersion,
181
+ id: input.id ?? randomUUID(),
182
+ title: input.title,
183
+ provider: input.provider,
184
+ location: input.location,
185
+ anchor: input.anchor,
186
+ createdAt: now,
187
+ updatedAt: now,
188
+ labels: input.labels,
189
+ extensions: input.extensions
190
+ });
191
+ const nextRecords = [...this.records ?? [], record];
192
+ assertValidRecords(nextRecords);
193
+ if (record.anchor !== void 0 && record.anchor.mode === "managed") await mkdir(record.anchor.path, { recursive: true });
194
+ await this.save(nextRecords);
195
+ this.commit(nextRecords, {
196
+ type: "created",
197
+ record
198
+ });
199
+ return cloneRecord(record);
200
+ });
201
+ }
202
+ async rename(id, title) {
203
+ const before = await this.get(id);
204
+ const record = await this.update(id, { title: title.trim() });
205
+ if (before === void 0 || record === void 0) return void 0;
206
+ return record;
207
+ }
208
+ /** WorkspaceLedger.rename cannot change provider bindings or extension data, so update applies a generic patch. */
209
+ async update(id, patch) {
210
+ return this.enqueueMutation(async () => {
211
+ await this.ensureLoaded();
212
+ const records = this.records ?? [];
213
+ const index = records.findIndex((record) => record.id === id);
214
+ if (index < 0) return void 0;
215
+ const before = cloneRecord(records[index]);
216
+ const record = cloneRecord({
217
+ ...before,
218
+ ...structuredClone(patch),
219
+ id: before.id,
220
+ schemaVersion: before.schemaVersion,
221
+ createdAt: before.createdAt,
222
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
223
+ });
224
+ const nextRecords = [...records];
225
+ nextRecords[index] = record;
226
+ assertValidRecords(nextRecords);
227
+ await this.save(nextRecords);
228
+ this.commit(nextRecords, {
229
+ type: "updated",
230
+ before,
231
+ record
232
+ });
233
+ return cloneRecord(record);
234
+ });
235
+ }
236
+ /** WorkspaceLedger.create persists one generated record, so it cannot atomically import a complete authoritative snapshot. */
237
+ async replaceAll(records, options = {}) {
238
+ return this.enqueueMutation(async () => {
239
+ await this.ensureLoaded();
240
+ const nextRecords = cloneRecords(records);
241
+ assertValidRecords(nextRecords);
242
+ const before = cloneRecords(this.records ?? []);
243
+ if (options.backupPath !== void 0) {
244
+ await mkdir(dirname(options.backupPath), { recursive: true });
245
+ try {
246
+ await copyFile(this.file(), options.backupPath);
247
+ } catch (error) {
248
+ if (!isNodeErrorCode(error, "ENOENT")) throw error;
249
+ }
250
+ }
251
+ await this.save(nextRecords);
252
+ this.commit(nextRecords, {
253
+ type: "replaced",
254
+ before,
255
+ records: nextRecords
256
+ });
257
+ });
258
+ }
259
+ /** Remove a record. The anchor directory is left in place (caller decides). */
260
+ async remove(id) {
261
+ return this.enqueueMutation(async () => {
262
+ await this.ensureLoaded();
263
+ const records = this.records ?? [];
264
+ const index = records.findIndex((record) => record.id === id);
265
+ if (index < 0) return false;
266
+ const record = records[index];
267
+ const nextRecords = [...records.slice(0, index), ...records.slice(index + 1)];
268
+ await this.save(nextRecords);
269
+ this.commit(nextRecords, {
270
+ type: "removed",
271
+ record
272
+ });
273
+ return true;
274
+ });
275
+ }
276
+ anchorsRoot() {
277
+ return this.anchors();
278
+ }
279
+ };
280
+ /** isRecord validates one shape only, so this validator also rejects duplicate ids and ambiguous anchors across records. */
281
+ function assertValidRecords(values) {
282
+ const ids = /* @__PURE__ */ new Set();
283
+ const anchors = [];
284
+ for (const value of values) {
285
+ if (!isRecord(value)) throw new Error("WorkspaceLedger: invalid workspace record");
286
+ if (ids.has(value.id)) throw new Error(`WorkspaceLedger: duplicate workspace id '${value.id}'`);
287
+ ids.add(value.id);
288
+ if (value.anchor !== void 0) anchors.push({
289
+ id: value.id,
290
+ path: normalizeAnchorPath(value.anchor.path)
291
+ });
292
+ }
293
+ for (let left = 0; left < anchors.length; left += 1) for (let right = left + 1; right < anchors.length; right += 1) {
294
+ const a = anchors[left];
295
+ const b = anchors[right];
296
+ if (isPathUnderAnchor(a.path, b.path) || isPathUnderAnchor(b.path, a.path)) throw new Error(`WorkspaceLedger: duplicate or overlapping anchors for '${a.id}' and '${b.id}'`);
297
+ }
298
+ }
299
+ function isRecord(value) {
300
+ if (!isPlainObject(value)) return false;
301
+ const provider = value.provider;
302
+ const location = value.location;
303
+ const anchor = value.anchor;
304
+ const labels = value.labels;
305
+ const extensions = value.extensions;
306
+ if (value.schemaVersion !== 1 || typeof value.id !== "string" || value.id === "" || typeof value.title !== "string" || !isPlainObject(provider) || typeof provider.id !== "string" || provider.id === "" || provider.instanceId !== void 0 && typeof provider.instanceId !== "string" || !isConnectionRef(provider.connectionRef) || !isPlainObject(location) || typeof location.kind !== "string" || location.kind === "" || typeof location.root !== "string" || location.root === "" || location.options !== void 0 && (!isPlainObject(location.options) || !isJsonValue(location.options)) || !isAnchor(anchor) || typeof value.createdAt !== "string" || typeof value.updatedAt !== "string" || labels !== void 0 && (!isPlainObject(labels) || Object.values(labels).some((label) => typeof label !== "string")) || extensions !== void 0 && (!isPlainObject(extensions) || !isJsonValue(extensions))) return false;
307
+ return true;
308
+ }
309
+ /** isRecord cannot distinguish an absent connectionRef from a malformed present one without this focused guard. */
310
+ function isConnectionRef(value) {
311
+ return value === void 0 || isPlainObject(value) && typeof value.id === "string" && value.id !== "" && (value.alias === void 0 || typeof value.alias === "string");
312
+ }
313
+ /** isRecord delegates optional anchor validation here because anchor has a discriminated mode shape. */
314
+ function isAnchor(value) {
315
+ return value === void 0 || isPlainObject(value) && typeof value.path === "string" && value.path !== "" && (value.mode === "managed" || value.mode === "existing");
316
+ }
317
+ /** typeof object also accepts arrays and prototypes, so isRecord needs this stricter object check. */
318
+ function isPlainObject(value) {
319
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
320
+ const prototype = Object.getPrototypeOf(value);
321
+ return prototype === Object.prototype || prototype === null;
322
+ }
323
+ /** isRecord cannot validate arbitrarily nested options/extensions inline, so this recursive JSON-value guard closes that gap. */
324
+ function isJsonValue(value) {
325
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
326
+ if (typeof value === "number") return Number.isFinite(value);
327
+ if (Array.isArray(value)) return value.every(isJsonValue);
328
+ if (!isPlainObject(value)) return false;
329
+ return Object.values(value).every(isJsonValue);
330
+ }
331
+ function safeRealpathSync(path) {
332
+ try {
333
+ return realpathSync(path);
334
+ } catch {
335
+ return;
336
+ }
337
+ }
338
+ function cloneRecord(record) {
339
+ return structuredClone(record);
340
+ }
341
+ function cloneRecords(records) {
342
+ return records.map(cloneRecord);
343
+ }
344
+ function cloneChange(change) {
345
+ switch (change.type) {
346
+ case "created": return {
347
+ ...change,
348
+ record: cloneRecord(change.record)
349
+ };
350
+ case "renamed": return {
351
+ ...change,
352
+ before: cloneRecord(change.before),
353
+ record: cloneRecord(change.record)
354
+ };
355
+ case "updated": return {
356
+ ...change,
357
+ before: cloneRecord(change.before),
358
+ record: cloneRecord(change.record)
359
+ };
360
+ case "removed": return {
361
+ ...change,
362
+ record: cloneRecord(change.record)
363
+ };
364
+ case "replaced": return {
365
+ ...change,
366
+ before: cloneRecords(change.before),
367
+ records: cloneRecords(change.records)
368
+ };
369
+ }
370
+ }
371
+ /** Error.message cannot reliably identify ENOENT, so strict loading checks Node's machine-readable code. */
372
+ function isNodeErrorCode(error, code) {
373
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
374
+ }
375
+ //#endregion
376
+ export { normalizeAnchorPath as a, isPathUnderAnchor as i, WorkspaceLedger as n, assertValidRecords as r, WORKSPACE_LEDGER_SCHEMA_VERSION as t };
@@ -0,0 +1,5 @@
1
+ //#region src/base/model.ts
2
+ /** Provider API major implemented by the generic workspace foundations. */
3
+ const WORKSPACE_PROVIDER_API_VERSION = 2;
4
+ //#endregion
5
+ export { WORKSPACE_PROVIDER_API_VERSION as t };