@sagmans/dsh-tui 0.3.0 → 0.5.0

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 (253) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +407 -53
  3. package/cordis.patch.yml +14 -0
  4. package/lib/agent/present.js +1 -1
  5. package/lib/agent/present.js.map +1 -1
  6. package/lib/agent/projections.d.ts +22 -6
  7. package/lib/agent/projections.d.ts.map +1 -1
  8. package/lib/agent/projections.js +21 -10
  9. package/lib/agent/projections.js.map +1 -1
  10. package/lib/agent/prompt-history.d.ts +89 -0
  11. package/lib/agent/prompt-history.d.ts.map +1 -0
  12. package/lib/agent/prompt-history.js +382 -0
  13. package/lib/agent/prompt-history.js.map +1 -0
  14. package/lib/agent/status.d.ts +6 -0
  15. package/lib/agent/status.d.ts.map +1 -1
  16. package/lib/agent/status.js +1 -0
  17. package/lib/agent/status.js.map +1 -1
  18. package/lib/cards.d.ts +83 -13
  19. package/lib/cards.d.ts.map +1 -1
  20. package/lib/cards.js +96 -28
  21. package/lib/cards.js.map +1 -1
  22. package/lib/export.d.ts +3 -3
  23. package/lib/export.d.ts.map +1 -1
  24. package/lib/export.js +23 -13
  25. package/lib/export.js.map +1 -1
  26. package/lib/fold-cursor.d.ts +12 -5
  27. package/lib/fold-cursor.d.ts.map +1 -1
  28. package/lib/fold-cursor.js +16 -5
  29. package/lib/fold-cursor.js.map +1 -1
  30. package/lib/gates.d.ts.map +1 -1
  31. package/lib/gates.js +42 -9
  32. package/lib/gates.js.map +1 -1
  33. package/lib/herdr/client.d.ts +57 -0
  34. package/lib/herdr/client.d.ts.map +1 -0
  35. package/lib/herdr/client.js +218 -0
  36. package/lib/herdr/client.js.map +1 -0
  37. package/lib/herdr/constants.d.ts +93 -0
  38. package/lib/herdr/constants.d.ts.map +1 -0
  39. package/lib/herdr/constants.js +79 -0
  40. package/lib/herdr/constants.js.map +1 -0
  41. package/lib/herdr/reporter.d.ts +60 -0
  42. package/lib/herdr/reporter.d.ts.map +1 -0
  43. package/lib/herdr/reporter.js +217 -0
  44. package/lib/herdr/reporter.js.map +1 -0
  45. package/lib/herdr/state.d.ts +57 -0
  46. package/lib/herdr/state.d.ts.map +1 -0
  47. package/lib/herdr/state.js +62 -0
  48. package/lib/herdr/state.js.map +1 -0
  49. package/lib/index.d.ts.map +1 -1
  50. package/lib/index.js +700 -81
  51. package/lib/index.js.map +1 -1
  52. package/lib/injection.d.ts +9 -0
  53. package/lib/injection.d.ts.map +1 -0
  54. package/lib/injection.js +128 -0
  55. package/lib/injection.js.map +1 -0
  56. package/lib/input/actions.d.ts +19 -2
  57. package/lib/input/actions.d.ts.map +1 -1
  58. package/lib/input/actions.js +60 -11
  59. package/lib/input/actions.js.map +1 -1
  60. package/lib/input/cancel.d.ts +55 -0
  61. package/lib/input/cancel.d.ts.map +1 -0
  62. package/lib/input/cancel.js +47 -0
  63. package/lib/input/cancel.js.map +1 -0
  64. package/lib/input/completion.d.ts +5 -2
  65. package/lib/input/completion.d.ts.map +1 -1
  66. package/lib/input/completion.js +79 -4
  67. package/lib/input/completion.js.map +1 -1
  68. package/lib/input/file-search.d.ts +105 -0
  69. package/lib/input/file-search.d.ts.map +1 -0
  70. package/lib/input/file-search.js +665 -0
  71. package/lib/input/file-search.js.map +1 -0
  72. package/lib/input/fuzzy.d.ts +42 -0
  73. package/lib/input/fuzzy.d.ts.map +1 -0
  74. package/lib/input/fuzzy.js +289 -0
  75. package/lib/input/fuzzy.js.map +1 -0
  76. package/lib/input/ghost.d.ts +46 -0
  77. package/lib/input/ghost.d.ts.map +1 -0
  78. package/lib/input/ghost.js +58 -0
  79. package/lib/input/ghost.js.map +1 -0
  80. package/lib/input/keymap.d.ts +3 -1
  81. package/lib/input/keymap.d.ts.map +1 -1
  82. package/lib/input/keymap.js +16 -3
  83. package/lib/input/keymap.js.map +1 -1
  84. package/lib/input/match.d.ts +12 -4
  85. package/lib/input/match.d.ts.map +1 -1
  86. package/lib/input/match.js +50 -51
  87. package/lib/input/match.js.map +1 -1
  88. package/lib/input/submission.d.ts +34 -1
  89. package/lib/input/submission.d.ts.map +1 -1
  90. package/lib/input/submission.js +33 -5
  91. package/lib/input/submission.js.map +1 -1
  92. package/lib/keys-command.d.ts +19 -3
  93. package/lib/keys-command.d.ts.map +1 -1
  94. package/lib/keys-command.js +34 -31
  95. package/lib/keys-command.js.map +1 -1
  96. package/lib/settings-notice.d.ts +5 -0
  97. package/lib/settings-notice.d.ts.map +1 -1
  98. package/lib/settings-notice.js +9 -7
  99. package/lib/settings-notice.js.map +1 -1
  100. package/lib/stash/lock.d.ts +72 -0
  101. package/lib/stash/lock.d.ts.map +1 -0
  102. package/lib/stash/lock.js +374 -0
  103. package/lib/stash/lock.js.map +1 -0
  104. package/lib/stash/paths.d.ts +22 -0
  105. package/lib/stash/paths.d.ts.map +1 -0
  106. package/lib/stash/paths.js +84 -0
  107. package/lib/stash/paths.js.map +1 -0
  108. package/lib/stash/private-fs.d.ts +61 -0
  109. package/lib/stash/private-fs.d.ts.map +1 -0
  110. package/lib/stash/private-fs.js +379 -0
  111. package/lib/stash/private-fs.js.map +1 -0
  112. package/lib/stash/schema.d.ts +56 -0
  113. package/lib/stash/schema.d.ts.map +1 -0
  114. package/lib/stash/schema.js +108 -0
  115. package/lib/stash/schema.js.map +1 -0
  116. package/lib/stash/store.d.ts +92 -0
  117. package/lib/stash/store.d.ts.map +1 -0
  118. package/lib/stash/store.js +262 -0
  119. package/lib/stash/store.js.map +1 -0
  120. package/lib/stash.d.ts +122 -0
  121. package/lib/stash.d.ts.map +1 -0
  122. package/lib/stash.js +353 -0
  123. package/lib/stash.js.map +1 -0
  124. package/lib/terminal/external-editor.d.ts +93 -0
  125. package/lib/terminal/external-editor.d.ts.map +1 -0
  126. package/lib/terminal/external-editor.js +263 -0
  127. package/lib/terminal/external-editor.js.map +1 -0
  128. package/lib/terminal/host-writes.d.ts +39 -0
  129. package/lib/terminal/host-writes.d.ts.map +1 -0
  130. package/lib/terminal/host-writes.js +136 -0
  131. package/lib/terminal/host-writes.js.map +1 -0
  132. package/lib/terminal/signals.d.ts +25 -0
  133. package/lib/terminal/signals.d.ts.map +1 -0
  134. package/lib/terminal/signals.js +39 -0
  135. package/lib/terminal/signals.js.map +1 -0
  136. package/lib/terminal/warning-screen.d.ts +19 -1
  137. package/lib/terminal/warning-screen.d.ts.map +1 -1
  138. package/lib/terminal/warning-screen.js +37 -1
  139. package/lib/terminal/warning-screen.js.map +1 -1
  140. package/lib/terminal-text.d.ts +72 -0
  141. package/lib/terminal-text.d.ts.map +1 -0
  142. package/lib/terminal-text.js +603 -0
  143. package/lib/terminal-text.js.map +1 -0
  144. package/lib/text.d.ts +45 -6
  145. package/lib/text.d.ts.map +1 -1
  146. package/lib/text.js +106 -46
  147. package/lib/text.js.map +1 -1
  148. package/lib/theme-command.d.ts +7 -4
  149. package/lib/theme-command.d.ts.map +1 -1
  150. package/lib/theme-command.js +32 -12
  151. package/lib/theme-command.js.map +1 -1
  152. package/lib/theme-files.d.ts +110 -0
  153. package/lib/theme-files.d.ts.map +1 -0
  154. package/lib/theme-files.js +352 -0
  155. package/lib/theme-files.js.map +1 -0
  156. package/lib/theme-schema.d.ts +121 -0
  157. package/lib/theme-schema.d.ts.map +1 -0
  158. package/lib/theme-schema.js +87 -0
  159. package/lib/theme-schema.js.map +1 -0
  160. package/lib/theme-settings.d.ts +72 -10
  161. package/lib/theme-settings.d.ts.map +1 -1
  162. package/lib/theme-settings.js +162 -45
  163. package/lib/theme-settings.js.map +1 -1
  164. package/lib/theme-tokens.d.ts +39 -7
  165. package/lib/theme-tokens.d.ts.map +1 -1
  166. package/lib/theme-tokens.js +104 -13
  167. package/lib/theme-tokens.js.map +1 -1
  168. package/lib/theme.d.ts +24 -2
  169. package/lib/theme.d.ts.map +1 -1
  170. package/lib/theme.js +49 -6
  171. package/lib/theme.js.map +1 -1
  172. package/lib/todo-guard.d.ts +116 -0
  173. package/lib/todo-guard.d.ts.map +1 -0
  174. package/lib/todo-guard.js +259 -0
  175. package/lib/todo-guard.js.map +1 -0
  176. package/lib/tool-display.d.ts +57 -0
  177. package/lib/tool-display.d.ts.map +1 -0
  178. package/lib/tool-display.js +51 -0
  179. package/lib/tool-display.js.map +1 -0
  180. package/lib/transcript.d.ts +28 -0
  181. package/lib/transcript.d.ts.map +1 -1
  182. package/lib/transcript.js +126 -37
  183. package/lib/transcript.js.map +1 -1
  184. package/lib/ui/diff.d.ts +49 -0
  185. package/lib/ui/diff.d.ts.map +1 -0
  186. package/lib/ui/diff.js +208 -0
  187. package/lib/ui/diff.js.map +1 -0
  188. package/lib/ui/dock.d.ts.map +1 -1
  189. package/lib/ui/dock.js +9 -6
  190. package/lib/ui/dock.js.map +1 -1
  191. package/lib/ui/editor.d.ts +41 -1
  192. package/lib/ui/editor.d.ts.map +1 -1
  193. package/lib/ui/editor.js +100 -4
  194. package/lib/ui/editor.js.map +1 -1
  195. package/lib/ui/frame.d.ts +16 -5
  196. package/lib/ui/frame.d.ts.map +1 -1
  197. package/lib/ui/frame.js +32 -13
  198. package/lib/ui/frame.js.map +1 -1
  199. package/lib/ui/history-picker.d.ts +15 -0
  200. package/lib/ui/history-picker.d.ts.map +1 -0
  201. package/lib/ui/history-picker.js +26 -0
  202. package/lib/ui/history-picker.js.map +1 -0
  203. package/lib/ui/keymap-picker.d.ts +16 -0
  204. package/lib/ui/keymap-picker.d.ts.map +1 -0
  205. package/lib/ui/keymap-picker.js +41 -0
  206. package/lib/ui/keymap-picker.js.map +1 -0
  207. package/lib/ui/layout.d.ts +33 -0
  208. package/lib/ui/layout.d.ts.map +1 -0
  209. package/lib/ui/layout.js +33 -0
  210. package/lib/ui/layout.js.map +1 -0
  211. package/lib/ui/markdown.d.ts +25 -3
  212. package/lib/ui/markdown.d.ts.map +1 -1
  213. package/lib/ui/markdown.js +12 -8
  214. package/lib/ui/markdown.js.map +1 -1
  215. package/lib/ui/picker-card.d.ts +42 -0
  216. package/lib/ui/picker-card.d.ts.map +1 -0
  217. package/lib/ui/picker-card.js +148 -0
  218. package/lib/ui/picker-card.js.map +1 -0
  219. package/lib/ui/picker.d.ts +50 -2
  220. package/lib/ui/picker.d.ts.map +1 -1
  221. package/lib/ui/picker.js +66 -7
  222. package/lib/ui/picker.js.map +1 -1
  223. package/lib/ui/prompt.d.ts +12 -2
  224. package/lib/ui/prompt.d.ts.map +1 -1
  225. package/lib/ui/prompt.js +25 -2
  226. package/lib/ui/prompt.js.map +1 -1
  227. package/lib/ui/rows.d.ts +8 -6
  228. package/lib/ui/rows.d.ts.map +1 -1
  229. package/lib/ui/rows.js +10 -18
  230. package/lib/ui/rows.js.map +1 -1
  231. package/lib/ui/stash-picker.d.ts +43 -0
  232. package/lib/ui/stash-picker.d.ts.map +1 -0
  233. package/lib/ui/stash-picker.js +78 -0
  234. package/lib/ui/stash-picker.js.map +1 -0
  235. package/lib/ui/status.d.ts +2 -0
  236. package/lib/ui/status.d.ts.map +1 -1
  237. package/lib/ui/status.js +25 -16
  238. package/lib/ui/status.js.map +1 -1
  239. package/lib/ui/theme-picker.d.ts +18 -0
  240. package/lib/ui/theme-picker.d.ts.map +1 -0
  241. package/lib/ui/theme-picker.js +68 -0
  242. package/lib/ui/theme-picker.js.map +1 -0
  243. package/lib/ui/view.d.ts +146 -22
  244. package/lib/ui/view.d.ts.map +1 -1
  245. package/lib/ui/view.js +461 -154
  246. package/lib/ui/view.js.map +1 -1
  247. package/lib/work.d.ts +3 -1
  248. package/lib/work.d.ts.map +1 -1
  249. package/lib/work.js +9 -1
  250. package/lib/work.js.map +1 -1
  251. package/package.json +7 -1
  252. package/themes/deepseek-blue.yaml +183 -0
  253. package/themes/violet-orbit.yaml +178 -0
@@ -0,0 +1,374 @@
1
+ // Private cross-process lock for one stash file.
2
+ //
3
+ // Two `dsh --profile tui` runs can share a working directory, so every read that
4
+ // feeds a write and every write itself happens under an exclusive directory
5
+ // lock: none of them can lose another's entry to a read-modify-write race.
6
+ // Ownership is recorded in the lock so a lock left by a crashed process is
7
+ // reclaimed rather than wedging the bank forever.
8
+ //
9
+ // Releasing *moves* the lock out of its path with one atomic rename before it
10
+ // judges or deletes it, and reclaiming judges a lock and removes it under a
11
+ // separate claim that only one contender can take. Removing the path on the
12
+ // strength of an earlier look would let a contender delete the live lock that
13
+ // replaced the one it judged, and two writers would then run at once.
14
+ import { createHash } from 'node:crypto';
15
+ import { link, mkdir, rename, rm } from 'node:fs/promises';
16
+ import { hostname } from 'node:os';
17
+ import path from 'node:path';
18
+ import { setTimeout as delay } from 'node:timers/promises';
19
+ import { assertPrivateDirectory, ensurePrivateDirectory, hasErrorCode, PRIVATE_DIR_MODE, readPrivateTextFile, removePrivateDirectory, writePrivateFileExclusive, } from "./private-fs.js";
20
+ import { createNewId } from "./schema.js";
21
+ const LOCK_RETRY_MS = 25;
22
+ const LOCK_TIMEOUT_MS = 2000;
23
+ const LOCK_STALE_MS = 30_000;
24
+ const LOCK_OWNER_FILE = 'owner.json';
25
+ const LOCK_TAKEOVER_SUFFIX = '.taken';
26
+ /**
27
+ * The name reclaimers link their claim file onto, so exactly one of them wins.
28
+ *
29
+ * A storage directory holds a bank per working directory, so the claim is named
30
+ * after the bank it guards: one crashed reclaimer then stops recovery for that
31
+ * bank alone instead of for every directory that shares the storage. The name is
32
+ * a digest rather than the key itself, because a key at the sanitizer's limit
33
+ * plus a suffix and a token is longer than the longest name a filesystem accepts.
34
+ */
35
+ const RECLAIM_PREFIX = '.claim-';
36
+ const RECLAIM_DIGEST_LENGTH = 16;
37
+ const LOCK_CHANGED_MESSAGE = 'stash lock changed before release';
38
+ /**
39
+ * A mutation that landed on disk while a later step failed.
40
+ *
41
+ * The write is not undone — reporting it as a failure would tell the reader
42
+ * their draft was lost when it is readable — so the failure travels beside the
43
+ * result and the caller reports success plus the warning.
44
+ */
45
+ export class StashCommittedError extends Error {
46
+ committed = true;
47
+ result;
48
+ failure;
49
+ /** Set when the lock could not be released either, so both are reported. */
50
+ releaseFailure;
51
+ constructor(result, failure, releaseFailure) {
52
+ super(releaseFailure === undefined
53
+ ? `stash mutation committed but ${failure.phase} failed`
54
+ : `stash mutation committed but ${failure.phase} and ${releaseFailure.phase} failed`, { cause: failure.error });
55
+ this.name = 'StashCommittedError';
56
+ this.result = result;
57
+ this.failure = failure;
58
+ this.releaseFailure = releaseFailure;
59
+ }
60
+ }
61
+ /** Take the file lock for a read that must not observe a half-written file. */
62
+ export function withStashFileLock(filePath, operation) {
63
+ return withStashLock(filePath, operation, false);
64
+ }
65
+ /**
66
+ * Take the lock for a mutation, and classify an unlock failure as committed.
67
+ *
68
+ * Only a callback that reports `didPersist` turns a failed unlock into a
69
+ * committed error: an unlock failure after a refused mutation is an ordinary
70
+ * failure.
71
+ */
72
+ export async function withStashMutationLock(filePath, operation) {
73
+ const outcome = await withStashLock(filePath, operation, true);
74
+ return outcome.result;
75
+ }
76
+ async function withStashLock(filePath, operation, mutation) {
77
+ await ensurePrivateDirectory(path.dirname(filePath));
78
+ const lockDir = `${filePath}.lock`;
79
+ const owner = await acquireStashLock(lockDir);
80
+ let result;
81
+ try {
82
+ result = await operation();
83
+ }
84
+ catch (operationError) {
85
+ try {
86
+ await releaseStashLock(lockDir, owner);
87
+ }
88
+ catch (releaseError) {
89
+ if (operationError instanceof StashCommittedError) {
90
+ // The mutation is on disk, and the caller has to keep hearing that: an
91
+ // AggregateError would read as a plain failure, and a retry of an
92
+ // index-based drop would then delete the draft that followed the one
93
+ // already removed. Both failed steps travel on the committed error.
94
+ throw new StashCommittedError(operationError.result, operationError.failure, {
95
+ phase: 'lock-release',
96
+ error: releaseError,
97
+ });
98
+ }
99
+ throw new AggregateError([operationError, releaseError], 'stash operation and lock release both failed');
100
+ }
101
+ throw operationError;
102
+ }
103
+ try {
104
+ await releaseStashLock(lockDir, owner);
105
+ }
106
+ catch (releaseError) {
107
+ if (mutation && isPersistedMutation(result)) {
108
+ throw new StashCommittedError(result.result, { phase: 'lock-release', error: releaseError });
109
+ }
110
+ // A read already has its answer, and the answer cannot be wrong because the
111
+ // lock outlived it: the next contender reclaims a lock whose owner is gone.
112
+ // Failing here would throw away what the caller just learned — including the
113
+ // path of a bank that was quarantined while it was being read.
114
+ if (mutation)
115
+ throw releaseError;
116
+ }
117
+ return result;
118
+ }
119
+ function isPersistedMutation(value) {
120
+ return (typeof value === 'object' &&
121
+ value !== null &&
122
+ 'didPersist' in value &&
123
+ value.didPersist === true &&
124
+ 'result' in value);
125
+ }
126
+ async function acquireStashLock(lockDir) {
127
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
128
+ for (;;) {
129
+ try {
130
+ await mkdir(lockDir, { mode: PRIVATE_DIR_MODE });
131
+ try {
132
+ await assertPrivateDirectory(lockDir, 'stash lock');
133
+ return await writeLockOwner(lockDir);
134
+ }
135
+ catch (error) {
136
+ // Losing the name while publishing it is a retry, not a failure: a
137
+ // contender that judged a lock abandoned can take the name away, and the
138
+ // owner write then either finds the name gone or finds a successor's
139
+ // owner file. Deleting the directory in either case would be deleting a
140
+ // lock this process never held, and the successor would lose its turn.
141
+ if (hasErrorCode(error, 'ENOENT') || hasErrorCode(error, 'EEXIST')) {
142
+ if (Date.now() >= deadline)
143
+ throw lockTimeout(lockDir);
144
+ continue;
145
+ }
146
+ throw error;
147
+ }
148
+ }
149
+ catch (error) {
150
+ if (!hasErrorCode(error, 'EEXIST'))
151
+ throw error;
152
+ try {
153
+ await assertPrivateDirectory(lockDir, 'stash lock');
154
+ }
155
+ catch (validationError) {
156
+ if (hasErrorCode(validationError, 'ENOENT'))
157
+ continue;
158
+ throw validationError;
159
+ }
160
+ if (await reclaimAbandonedLock(lockDir))
161
+ continue;
162
+ if (Date.now() >= deadline)
163
+ throw lockTimeout(lockDir);
164
+ await delay(LOCK_RETRY_MS);
165
+ }
166
+ }
167
+ }
168
+ function lockTimeout(lockDir) {
169
+ const mutex = reclaimMutexPath(lockDir);
170
+ return new Error(`timed out waiting for the stash lock at ${lockDir}; remove it and ${mutex} once no surface is running`);
171
+ }
172
+ /**
173
+ * The claim a reclaimer takes, named after the bank whose lock it guards.
174
+ *
175
+ * Exported because it is the path a timeout tells the reader to clear by hand.
176
+ */
177
+ export function reclaimMutexPath(lockDir) {
178
+ const digest = createHash('sha256').update(path.basename(lockDir), 'utf8').digest('hex');
179
+ return path.join(path.dirname(lockDir), `${RECLAIM_PREFIX}${digest.slice(0, RECLAIM_DIGEST_LENGTH)}`);
180
+ }
181
+ async function writeLockOwner(lockDir) {
182
+ const owner = {
183
+ pid: process.pid,
184
+ host: hostname(),
185
+ token: createNewId(),
186
+ createdAt: new Date().toISOString(),
187
+ };
188
+ await writePrivateFileExclusive(path.join(lockDir, LOCK_OWNER_FILE), `${JSON.stringify(owner)}\n`);
189
+ return owner;
190
+ }
191
+ async function readLockOwner(lockDir) {
192
+ let text;
193
+ try {
194
+ await assertPrivateDirectory(lockDir, 'stash lock');
195
+ text = (await readPrivateTextFile(path.join(lockDir, LOCK_OWNER_FILE), 'lock owner file')).text;
196
+ }
197
+ catch (error) {
198
+ if (hasErrorCode(error, 'ENOENT'))
199
+ return undefined;
200
+ throw error;
201
+ }
202
+ try {
203
+ const value = JSON.parse(text);
204
+ return isLockOwner(value) ? value : undefined;
205
+ }
206
+ catch {
207
+ return undefined;
208
+ }
209
+ }
210
+ function isLockOwner(value) {
211
+ return (typeof value === 'object' &&
212
+ value !== null &&
213
+ 'pid' in value &&
214
+ Number.isSafeInteger(value.pid) &&
215
+ 'host' in value &&
216
+ typeof value.host === 'string' &&
217
+ 'token' in value &&
218
+ typeof value.token === 'string' &&
219
+ value.token.length > 0 &&
220
+ 'createdAt' in value &&
221
+ typeof value.createdAt === 'string');
222
+ }
223
+ async function releaseStashLock(lockDir, expected) {
224
+ const taken = await takeLockDirectory(lockDir);
225
+ if (taken === undefined)
226
+ throw new Error(LOCK_CHANGED_MESSAGE);
227
+ const owner = await readLockOwner(taken);
228
+ if (owner?.token !== expected.token) {
229
+ await putBackLockDirectory(taken, lockDir);
230
+ throw new Error(LOCK_CHANGED_MESSAGE);
231
+ }
232
+ await removePrivateDirectory(taken, 'stash lock');
233
+ }
234
+ /**
235
+ * Move the lock out of its path, so only this caller can act on what it finds.
236
+ *
237
+ * One rename is atomic: two contenders cannot both take the same lock, and a
238
+ * lock published afterwards lands at the original path rather than under this
239
+ * name. Nothing is ever deleted through the path a fresh lock would occupy.
240
+ */
241
+ async function takeLockDirectory(lockDir) {
242
+ const taken = `${lockDir}${LOCK_TAKEOVER_SUFFIX}.${createNewId()}`;
243
+ try {
244
+ await rename(lockDir, taken);
245
+ }
246
+ catch (error) {
247
+ if (hasErrorCode(error, 'ENOENT') || hasErrorCode(error, 'ENOTEMPTY') || hasErrorCode(error, 'EEXIST')) {
248
+ return undefined;
249
+ }
250
+ throw error;
251
+ }
252
+ return taken;
253
+ }
254
+ /**
255
+ * Put a lock back that turned out to belong to someone else.
256
+ *
257
+ * A fresh lock at the original path means this one can never be restored. It is
258
+ * left where it is rather than deleted: whoever holds it is running, so a lock
259
+ * that is merely hard to find costs a warning on release, while a deleted one
260
+ * costs a second writer in the bank.
261
+ */
262
+ async function putBackLockDirectory(taken, lockDir) {
263
+ try {
264
+ await rename(taken, lockDir);
265
+ }
266
+ catch (error) {
267
+ if (hasErrorCode(error, 'ENOENT'))
268
+ return;
269
+ throw error;
270
+ }
271
+ }
272
+ function processIsAlive(pid) {
273
+ try {
274
+ process.kill(pid, 0);
275
+ return true;
276
+ }
277
+ catch (error) {
278
+ // Only ESRCH proves the process is gone; every other failure (permissions,
279
+ // an unknown error) is uncertainty and must keep the lock held.
280
+ return !hasErrorCode(error, 'ESRCH');
281
+ }
282
+ }
283
+ /** Read a lock and the identity of the directory it was read from, in one pass. */
284
+ export async function observeLock(lockDir) {
285
+ let stats;
286
+ try {
287
+ stats = await assertPrivateDirectory(lockDir, 'stash lock');
288
+ }
289
+ catch (error) {
290
+ if (hasErrorCode(error, 'ENOENT'))
291
+ return undefined;
292
+ throw error;
293
+ }
294
+ const owner = await readLockOwner(lockDir);
295
+ return { owner, device: stats.dev, inode: stats.ino, modified: stats.mtimeMs };
296
+ }
297
+ /**
298
+ * Reclaim a lock whose owner is gone.
299
+ *
300
+ * Reclaimers serialize on a claim file created by link, which exactly one of
301
+ * them can win, so only one of them ever judges and removes a given lock. The
302
+ * winner looks once, decides from that look, and then removes only if the lock
303
+ * still is the directory it looked at: a holder that released in between would
304
+ * otherwise leave its successor's live lock to be deleted, and both writers
305
+ * would then run at once.
306
+ *
307
+ * A reclaimer killed inside that section leaves the claim file behind, and every
308
+ * later reclaim fails closed at the timeout instead of guessing. That message
309
+ * names the file, because clearing it by hand is the one recovery that cannot
310
+ * cost an update.
311
+ */
312
+ async function reclaimAbandonedLock(lockDir) {
313
+ const mutex = reclaimMutexPath(lockDir);
314
+ const claim = await claimReclaim(mutex);
315
+ if (claim === undefined)
316
+ return false;
317
+ try {
318
+ const observed = await observeLock(lockDir);
319
+ if (observed === undefined || !isAbandonedLock(observed))
320
+ return false;
321
+ return await removeObservedLock(lockDir, observed);
322
+ }
323
+ finally {
324
+ await rm(claim, { force: true }).catch(() => undefined);
325
+ await rm(mutex, { force: true }).catch(() => undefined);
326
+ }
327
+ }
328
+ /**
329
+ * Remove the lock a judgement was made about, and nothing that replaced it.
330
+ *
331
+ * Exported so the race this exists for can be driven from a test: hold an
332
+ * observation, publish a live lock in its place, and the removal has to refuse.
333
+ */
334
+ export async function removeObservedLock(lockDir, observed) {
335
+ const current = await observeLock(lockDir);
336
+ if (current === undefined)
337
+ return false;
338
+ if (current.device !== observed.device || current.inode !== observed.inode)
339
+ return false;
340
+ if (current.owner?.token !== observed.owner?.token)
341
+ return false;
342
+ await removePrivateDirectory(lockDir, 'stash lock');
343
+ return true;
344
+ }
345
+ /** Whether one observation of a lock shows an owner that can no longer hold it. */
346
+ function isAbandonedLock(observed) {
347
+ if (observed.owner === undefined)
348
+ return Date.now() - observed.modified > LOCK_STALE_MS;
349
+ if (observed.owner.host !== hostname())
350
+ return false;
351
+ return !processIsAlive(observed.owner.pid);
352
+ }
353
+ /**
354
+ * Win the right to reclaim, by linking a private claim file onto one name.
355
+ *
356
+ * `link` is the atomic test-and-set this needs: it fails when the name exists,
357
+ * and it never replaces what is already there, so a contender that loses reads
358
+ * the winner's claim rather than breaking it.
359
+ */
360
+ async function claimReclaim(mutex) {
361
+ const claim = `${mutex}.${createNewId()}`;
362
+ await writePrivateFileExclusive(claim, `${JSON.stringify({ pid: process.pid, host: hostname() })}\n`);
363
+ try {
364
+ await link(claim, mutex);
365
+ return claim;
366
+ }
367
+ catch (error) {
368
+ await rm(claim, { force: true }).catch(() => undefined);
369
+ if (hasErrorCode(error, 'EEXIST'))
370
+ return undefined;
371
+ throw error;
372
+ }
373
+ }
374
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../../src/stash/lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /** The stash file for one session. */
2
+ export interface StashPaths {
3
+ /** The exact session this bank belongs to. */
4
+ readonly sessionId: string;
5
+ /** Flattened session id, used as the on-disk key. */
6
+ readonly key: string;
7
+ /** JSON file holding this session's stash entries. */
8
+ readonly file: string;
9
+ }
10
+ export declare function sanitizeSessionId(sessionId: string): string;
11
+ /**
12
+ * The directory the harness keeps its own state in.
13
+ *
14
+ * DSH_HOME is the one thing that moves that directory, and it is resolved the
15
+ * same way the settings document resolves it, so a scratch home keeps stash data
16
+ * out of a reader's real home during a test run.
17
+ */
18
+ export declare function dshHomeDir(env?: NodeJS.ProcessEnv, home?: string): string;
19
+ /** The root every session's stash file sits under. */
20
+ export declare function stashBaseDir(env?: NodeJS.ProcessEnv, home?: string): string;
21
+ export declare function resolveStashPaths(sessionId: string, baseDir?: string): StashPaths;
22
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/stash/paths.ts"],"names":[],"mappings":"AA0CA,sCAAsC;AACtC,MAAM,WAAW,UAAU;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,qDAAqD;IACrD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,sDAAsD;IACtD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAuBD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAM3D;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAGjG;AAED,sDAAsD;AACtD,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,EAAE,IAAI,GAAE,MAAkB,GAAG,MAAM,CAEnG;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,MAAuB,GAAG,UAAU,CAGjG"}
@@ -0,0 +1,84 @@
1
+ // Disk layout for prompt stashes.
2
+ //
3
+ // A bank is scoped to one session: two terminals working in the same directory
4
+ // keep separate drafts, and a resumed session finds the drafts it parked before
5
+ // the restart, because the session id is what a resume preserves. The id is
6
+ // flattened into a filename-safe key that stays human-readable in the agent
7
+ // directory.
8
+ //
9
+ // Readability cannot be bought with injectivity: a separator made of hyphens
10
+ // cannot be told apart from a hyphen inside a session id, and truncating a long
11
+ // id can land on a name a shorter id already owns. Two sessions sharing one bank
12
+ // would let one read or delete another's drafts, so every key ends with a digest
13
+ // of the exact session id. The readable part is then only a label: two distinct
14
+ // ids collide in the label, never in the file.
15
+ import { createHash } from 'node:crypto';
16
+ import { homedir } from 'node:os';
17
+ import path from 'node:path';
18
+ /** The directory `DSH_HOME` points at when the environment names none. */
19
+ const DEFAULT_DSH_HOME_DIR = '.dsh';
20
+ /** The subtree of `DSH_HOME` this surface owns. */
21
+ const STASH_DIR_NAME = 'tui-stash';
22
+ // POSIX filenames may not contain "/" or NUL, and everything else is legal, so
23
+ // once separators are flattened the result is filename-safe on the platforms
24
+ // this surface targets. 200 keeps the whole key, digest included, well under the
25
+ // common 255-byte filename limit.
26
+ const SANITIZE_MAX_LENGTH = 200;
27
+ const SEPARATOR = '--';
28
+ const KEY_FORMAT_VERSION = 'v2';
29
+ const KEY_PREFIX = `${KEY_FORMAT_VERSION}${SEPARATOR}`;
30
+ const ESCAPE_CHARACTER = '%';
31
+ const ESCAPED_ESCAPE_CHARACTER = '%25';
32
+ const ESCAPED_SEPARATOR = '%2D%2D';
33
+ const BACKSLASH = '\\';
34
+ const ESCAPED_BACKSLASH = '%5C';
35
+ const HASH_ALGORITHM = 'sha256';
36
+ /** 64 bits of digest, which no deliberate name can be built to collide with. */
37
+ const HASH_LENGTH = 16;
38
+ /** Escape one key segment so it cannot collide with the separator or an escape. */
39
+ function escapeSegment(segment) {
40
+ return segment
41
+ .replaceAll(ESCAPE_CHARACTER, ESCAPED_ESCAPE_CHARACTER)
42
+ .replaceAll(BACKSLASH, ESCAPED_BACKSLASH)
43
+ .replaceAll(SEPARATOR, ESCAPED_SEPARATOR);
44
+ }
45
+ /** Keep whole characters while fitting a byte budget, so a cut cannot split one. */
46
+ function truncateToUtf8Bytes(value, maxBytes) {
47
+ let bytes = 0;
48
+ let result = '';
49
+ for (const character of value) {
50
+ const characterBytes = Buffer.byteLength(character);
51
+ if (bytes + characterBytes > maxBytes)
52
+ break;
53
+ result += character;
54
+ bytes += characterBytes;
55
+ }
56
+ return result;
57
+ }
58
+ export function sanitizeSessionId(sessionId) {
59
+ const readable = sessionId.split('/').filter(Boolean).map(escapeSegment).join(SEPARATOR);
60
+ const digest = createHash(HASH_ALGORITHM).update(sessionId, 'utf8').digest('hex').slice(0, HASH_LENGTH);
61
+ const suffix = `${SEPARATOR}${digest}`;
62
+ const budget = SANITIZE_MAX_LENGTH - Buffer.byteLength(KEY_PREFIX) - Buffer.byteLength(suffix);
63
+ return `${KEY_PREFIX}${truncateToUtf8Bytes(readable, Math.max(budget, 0))}${suffix}`;
64
+ }
65
+ /**
66
+ * The directory the harness keeps its own state in.
67
+ *
68
+ * DSH_HOME is the one thing that moves that directory, and it is resolved the
69
+ * same way the settings document resolves it, so a scratch home keeps stash data
70
+ * out of a reader's real home during a test run.
71
+ */
72
+ export function dshHomeDir(env = process.env, home = homedir()) {
73
+ const configured = env.DSH_HOME?.trim();
74
+ return configured === undefined || configured === '' ? path.join(home, DEFAULT_DSH_HOME_DIR) : configured;
75
+ }
76
+ /** The root every session's stash file sits under. */
77
+ export function stashBaseDir(env = process.env, home = homedir()) {
78
+ return path.join(dshHomeDir(env, home), STASH_DIR_NAME);
79
+ }
80
+ export function resolveStashPaths(sessionId, baseDir = stashBaseDir()) {
81
+ const key = sanitizeSessionId(sessionId);
82
+ return { sessionId, key, file: path.join(baseDir, `${key}.json`) };
83
+ }
84
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/stash/paths.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ import { type Stats } from 'node:fs';
2
+ export declare const PRIVATE_DIR_MODE = 448;
3
+ export declare const PRIVATE_FILE_MODE = 384;
4
+ type FileIdentity = Pick<Stats, 'dev' | 'ino'>;
5
+ export interface PrivateTextFile {
6
+ readonly text: string;
7
+ readonly identity: FileIdentity;
8
+ }
9
+ /** Where a quarantined file went, and whether that location reached the disk. */
10
+ export interface QuarantineResult {
11
+ readonly path: string;
12
+ /** Set when the directory naming the copy could not be synced. */
13
+ readonly syncError: unknown;
14
+ }
15
+ /** A file that exceeded the caller's byte budget, which is a refusal, not corruption. */
16
+ export declare class FileTooLargeError extends Error {
17
+ constructor(label: string);
18
+ }
19
+ /**
20
+ * Prepare the storage directory, and make the directories that name it durable.
21
+ *
22
+ * A directory that names a new child holds the only record of that child, so
23
+ * every directory this call creates is flushed through its parent before the
24
+ * call returns. A step that fails takes the created directories away again:
25
+ * leaving them behind would turn the next attempt into one that finds nothing
26
+ * left to flush, and a save could then report success for a tree that a power
27
+ * loss is free to drop.
28
+ *
29
+ * The rollback only ever removes an empty directory. Another surface can reach a
30
+ * directory this call just created and save into it while this one is flushing,
31
+ * and a recursive removal would then delete that surface's committed bank: an
32
+ * entry left unflushed costs durability, while a removed bank costs the draft.
33
+ */
34
+ export declare function ensurePrivateDirectory(directory: string, label?: string, syncDirectory?: typeof syncDirectoryEntry): Promise<void>;
35
+ /**
36
+ * Flush one directory entry without asserting who owns the directory.
37
+ *
38
+ * An ancestor may legitimately be root-owned (`/tmp`, a mount point) and still
39
+ * have to be synced for the entry below it to survive a power loss, so the
40
+ * ownership rule that guards the storage directory itself is not applied here.
41
+ * A directory that is already gone needs no flushing.
42
+ */
43
+ export declare function syncDirectoryEntry(directory: string): Promise<void>;
44
+ export declare function assertPrivateDirectory(directory: string, label: string): Promise<Stats>;
45
+ export declare function syncPrivateDirectory(directory: string, label?: string): Promise<void>;
46
+ export declare function readPrivateTextFile(filePath: string, label?: string, maxBytes?: number): Promise<PrivateTextFile>;
47
+ export declare function writePrivateFileExclusive(filePath: string, data: string | Uint8Array): Promise<void>;
48
+ /**
49
+ * Move a file aside without losing it.
50
+ *
51
+ * A hard-link reservation keeps the original bytes reachable under a new name
52
+ * before the source is unlinked, so an interrupted quarantine leaves either the
53
+ * readable original or the readable copy, never neither.
54
+ */
55
+ export declare function quarantinePrivateFile(filePath: string, identity: FileIdentity, label: string, syncDirectory?: typeof syncPrivateDirectory): Promise<QuarantineResult>;
56
+ export declare function removePrivateDirectory(directory: string, label?: string): Promise<void>;
57
+ export declare function assertRegularOwnedFile(stats: Stats, label: string): void;
58
+ export declare function pathExists(target: string): Promise<boolean>;
59
+ export declare function hasErrorCode(value: unknown, code: string): boolean;
60
+ export {};
61
+ //# sourceMappingURL=private-fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"private-fs.d.ts","sourceRoot":"","sources":["../../src/stash/private-fs.ts"],"names":[],"mappings":"AAQA,OAAO,EAAa,KAAK,KAAK,EAAE,MAAM,SAAS,CAAA;AAI/C,eAAO,MAAM,gBAAgB,MAAQ,CAAA;AACrC,eAAO,MAAM,iBAAiB,MAAQ,CAAA;AAgBtC,KAAK,YAAY,GAAG,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAC,CAAA;AAE9C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;CAChC;AAED,iFAAiF;AACjF,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;CAC5B;AAED,yFAAyF;AACzF,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,KAAK,EAAE,MAAM;CAI1B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,KAAK,SAAsB,EAC3B,aAAa,GAAE,OAAO,kBAAuC,GAC5D,OAAO,CAAC,IAAI,CAAC,CAyBf;AAuGD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAazE;AAED,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAO7F;AAED,wBAAsB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,SAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAOxG;AAED,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,KAAK,SAAe,EACpB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CAa1B;AAeD,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAa1G;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,YAAY,EACtB,KAAK,EAAE,MAAM,EACb,aAAa,GAAE,OAAO,oBAA2C,GAChE,OAAO,CAAC,gBAAgB,CAAC,CAgC3B;AAED,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,SAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAY1G;AAwBD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIxE;AAmBD,wBAAsB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQjE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAOlE"}