@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.
- package/LICENSE +1 -1
- package/README.md +407 -53
- package/cordis.patch.yml +14 -0
- package/lib/agent/present.js +1 -1
- package/lib/agent/present.js.map +1 -1
- package/lib/agent/projections.d.ts +22 -6
- package/lib/agent/projections.d.ts.map +1 -1
- package/lib/agent/projections.js +21 -10
- package/lib/agent/projections.js.map +1 -1
- package/lib/agent/prompt-history.d.ts +89 -0
- package/lib/agent/prompt-history.d.ts.map +1 -0
- package/lib/agent/prompt-history.js +382 -0
- package/lib/agent/prompt-history.js.map +1 -0
- package/lib/agent/status.d.ts +6 -0
- package/lib/agent/status.d.ts.map +1 -1
- package/lib/agent/status.js +1 -0
- package/lib/agent/status.js.map +1 -1
- package/lib/cards.d.ts +83 -13
- package/lib/cards.d.ts.map +1 -1
- package/lib/cards.js +96 -28
- package/lib/cards.js.map +1 -1
- package/lib/export.d.ts +3 -3
- package/lib/export.d.ts.map +1 -1
- package/lib/export.js +23 -13
- package/lib/export.js.map +1 -1
- package/lib/fold-cursor.d.ts +12 -5
- package/lib/fold-cursor.d.ts.map +1 -1
- package/lib/fold-cursor.js +16 -5
- package/lib/fold-cursor.js.map +1 -1
- package/lib/gates.d.ts.map +1 -1
- package/lib/gates.js +42 -9
- package/lib/gates.js.map +1 -1
- package/lib/herdr/client.d.ts +57 -0
- package/lib/herdr/client.d.ts.map +1 -0
- package/lib/herdr/client.js +218 -0
- package/lib/herdr/client.js.map +1 -0
- package/lib/herdr/constants.d.ts +93 -0
- package/lib/herdr/constants.d.ts.map +1 -0
- package/lib/herdr/constants.js +79 -0
- package/lib/herdr/constants.js.map +1 -0
- package/lib/herdr/reporter.d.ts +60 -0
- package/lib/herdr/reporter.d.ts.map +1 -0
- package/lib/herdr/reporter.js +217 -0
- package/lib/herdr/reporter.js.map +1 -0
- package/lib/herdr/state.d.ts +57 -0
- package/lib/herdr/state.d.ts.map +1 -0
- package/lib/herdr/state.js +62 -0
- package/lib/herdr/state.js.map +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +700 -81
- package/lib/index.js.map +1 -1
- package/lib/injection.d.ts +9 -0
- package/lib/injection.d.ts.map +1 -0
- package/lib/injection.js +128 -0
- package/lib/injection.js.map +1 -0
- package/lib/input/actions.d.ts +19 -2
- package/lib/input/actions.d.ts.map +1 -1
- package/lib/input/actions.js +60 -11
- package/lib/input/actions.js.map +1 -1
- package/lib/input/cancel.d.ts +55 -0
- package/lib/input/cancel.d.ts.map +1 -0
- package/lib/input/cancel.js +47 -0
- package/lib/input/cancel.js.map +1 -0
- package/lib/input/completion.d.ts +5 -2
- package/lib/input/completion.d.ts.map +1 -1
- package/lib/input/completion.js +79 -4
- package/lib/input/completion.js.map +1 -1
- package/lib/input/file-search.d.ts +105 -0
- package/lib/input/file-search.d.ts.map +1 -0
- package/lib/input/file-search.js +665 -0
- package/lib/input/file-search.js.map +1 -0
- package/lib/input/fuzzy.d.ts +42 -0
- package/lib/input/fuzzy.d.ts.map +1 -0
- package/lib/input/fuzzy.js +289 -0
- package/lib/input/fuzzy.js.map +1 -0
- package/lib/input/ghost.d.ts +46 -0
- package/lib/input/ghost.d.ts.map +1 -0
- package/lib/input/ghost.js +58 -0
- package/lib/input/ghost.js.map +1 -0
- package/lib/input/keymap.d.ts +3 -1
- package/lib/input/keymap.d.ts.map +1 -1
- package/lib/input/keymap.js +16 -3
- package/lib/input/keymap.js.map +1 -1
- package/lib/input/match.d.ts +12 -4
- package/lib/input/match.d.ts.map +1 -1
- package/lib/input/match.js +50 -51
- package/lib/input/match.js.map +1 -1
- package/lib/input/submission.d.ts +34 -1
- package/lib/input/submission.d.ts.map +1 -1
- package/lib/input/submission.js +33 -5
- package/lib/input/submission.js.map +1 -1
- package/lib/keys-command.d.ts +19 -3
- package/lib/keys-command.d.ts.map +1 -1
- package/lib/keys-command.js +34 -31
- package/lib/keys-command.js.map +1 -1
- package/lib/settings-notice.d.ts +5 -0
- package/lib/settings-notice.d.ts.map +1 -1
- package/lib/settings-notice.js +9 -7
- package/lib/settings-notice.js.map +1 -1
- package/lib/stash/lock.d.ts +72 -0
- package/lib/stash/lock.d.ts.map +1 -0
- package/lib/stash/lock.js +374 -0
- package/lib/stash/lock.js.map +1 -0
- package/lib/stash/paths.d.ts +22 -0
- package/lib/stash/paths.d.ts.map +1 -0
- package/lib/stash/paths.js +84 -0
- package/lib/stash/paths.js.map +1 -0
- package/lib/stash/private-fs.d.ts +61 -0
- package/lib/stash/private-fs.d.ts.map +1 -0
- package/lib/stash/private-fs.js +379 -0
- package/lib/stash/private-fs.js.map +1 -0
- package/lib/stash/schema.d.ts +56 -0
- package/lib/stash/schema.d.ts.map +1 -0
- package/lib/stash/schema.js +108 -0
- package/lib/stash/schema.js.map +1 -0
- package/lib/stash/store.d.ts +92 -0
- package/lib/stash/store.d.ts.map +1 -0
- package/lib/stash/store.js +262 -0
- package/lib/stash/store.js.map +1 -0
- package/lib/stash.d.ts +122 -0
- package/lib/stash.d.ts.map +1 -0
- package/lib/stash.js +353 -0
- package/lib/stash.js.map +1 -0
- package/lib/terminal/external-editor.d.ts +93 -0
- package/lib/terminal/external-editor.d.ts.map +1 -0
- package/lib/terminal/external-editor.js +263 -0
- package/lib/terminal/external-editor.js.map +1 -0
- package/lib/terminal/host-writes.d.ts +39 -0
- package/lib/terminal/host-writes.d.ts.map +1 -0
- package/lib/terminal/host-writes.js +136 -0
- package/lib/terminal/host-writes.js.map +1 -0
- package/lib/terminal/signals.d.ts +25 -0
- package/lib/terminal/signals.d.ts.map +1 -0
- package/lib/terminal/signals.js +39 -0
- package/lib/terminal/signals.js.map +1 -0
- package/lib/terminal/warning-screen.d.ts +19 -1
- package/lib/terminal/warning-screen.d.ts.map +1 -1
- package/lib/terminal/warning-screen.js +37 -1
- package/lib/terminal/warning-screen.js.map +1 -1
- package/lib/terminal-text.d.ts +72 -0
- package/lib/terminal-text.d.ts.map +1 -0
- package/lib/terminal-text.js +603 -0
- package/lib/terminal-text.js.map +1 -0
- package/lib/text.d.ts +45 -6
- package/lib/text.d.ts.map +1 -1
- package/lib/text.js +106 -46
- package/lib/text.js.map +1 -1
- package/lib/theme-command.d.ts +7 -4
- package/lib/theme-command.d.ts.map +1 -1
- package/lib/theme-command.js +32 -12
- package/lib/theme-command.js.map +1 -1
- package/lib/theme-files.d.ts +110 -0
- package/lib/theme-files.d.ts.map +1 -0
- package/lib/theme-files.js +352 -0
- package/lib/theme-files.js.map +1 -0
- package/lib/theme-schema.d.ts +121 -0
- package/lib/theme-schema.d.ts.map +1 -0
- package/lib/theme-schema.js +87 -0
- package/lib/theme-schema.js.map +1 -0
- package/lib/theme-settings.d.ts +72 -10
- package/lib/theme-settings.d.ts.map +1 -1
- package/lib/theme-settings.js +162 -45
- package/lib/theme-settings.js.map +1 -1
- package/lib/theme-tokens.d.ts +39 -7
- package/lib/theme-tokens.d.ts.map +1 -1
- package/lib/theme-tokens.js +104 -13
- package/lib/theme-tokens.js.map +1 -1
- package/lib/theme.d.ts +24 -2
- package/lib/theme.d.ts.map +1 -1
- package/lib/theme.js +49 -6
- package/lib/theme.js.map +1 -1
- package/lib/todo-guard.d.ts +116 -0
- package/lib/todo-guard.d.ts.map +1 -0
- package/lib/todo-guard.js +259 -0
- package/lib/todo-guard.js.map +1 -0
- package/lib/tool-display.d.ts +57 -0
- package/lib/tool-display.d.ts.map +1 -0
- package/lib/tool-display.js +51 -0
- package/lib/tool-display.js.map +1 -0
- package/lib/transcript.d.ts +28 -0
- package/lib/transcript.d.ts.map +1 -1
- package/lib/transcript.js +126 -37
- package/lib/transcript.js.map +1 -1
- package/lib/ui/diff.d.ts +49 -0
- package/lib/ui/diff.d.ts.map +1 -0
- package/lib/ui/diff.js +208 -0
- package/lib/ui/diff.js.map +1 -0
- package/lib/ui/dock.d.ts.map +1 -1
- package/lib/ui/dock.js +9 -6
- package/lib/ui/dock.js.map +1 -1
- package/lib/ui/editor.d.ts +41 -1
- package/lib/ui/editor.d.ts.map +1 -1
- package/lib/ui/editor.js +100 -4
- package/lib/ui/editor.js.map +1 -1
- package/lib/ui/frame.d.ts +16 -5
- package/lib/ui/frame.d.ts.map +1 -1
- package/lib/ui/frame.js +32 -13
- package/lib/ui/frame.js.map +1 -1
- package/lib/ui/history-picker.d.ts +15 -0
- package/lib/ui/history-picker.d.ts.map +1 -0
- package/lib/ui/history-picker.js +26 -0
- package/lib/ui/history-picker.js.map +1 -0
- package/lib/ui/keymap-picker.d.ts +16 -0
- package/lib/ui/keymap-picker.d.ts.map +1 -0
- package/lib/ui/keymap-picker.js +41 -0
- package/lib/ui/keymap-picker.js.map +1 -0
- package/lib/ui/layout.d.ts +33 -0
- package/lib/ui/layout.d.ts.map +1 -0
- package/lib/ui/layout.js +33 -0
- package/lib/ui/layout.js.map +1 -0
- package/lib/ui/markdown.d.ts +25 -3
- package/lib/ui/markdown.d.ts.map +1 -1
- package/lib/ui/markdown.js +12 -8
- package/lib/ui/markdown.js.map +1 -1
- package/lib/ui/picker-card.d.ts +42 -0
- package/lib/ui/picker-card.d.ts.map +1 -0
- package/lib/ui/picker-card.js +148 -0
- package/lib/ui/picker-card.js.map +1 -0
- package/lib/ui/picker.d.ts +50 -2
- package/lib/ui/picker.d.ts.map +1 -1
- package/lib/ui/picker.js +66 -7
- package/lib/ui/picker.js.map +1 -1
- package/lib/ui/prompt.d.ts +12 -2
- package/lib/ui/prompt.d.ts.map +1 -1
- package/lib/ui/prompt.js +25 -2
- package/lib/ui/prompt.js.map +1 -1
- package/lib/ui/rows.d.ts +8 -6
- package/lib/ui/rows.d.ts.map +1 -1
- package/lib/ui/rows.js +10 -18
- package/lib/ui/rows.js.map +1 -1
- package/lib/ui/stash-picker.d.ts +43 -0
- package/lib/ui/stash-picker.d.ts.map +1 -0
- package/lib/ui/stash-picker.js +78 -0
- package/lib/ui/stash-picker.js.map +1 -0
- package/lib/ui/status.d.ts +2 -0
- package/lib/ui/status.d.ts.map +1 -1
- package/lib/ui/status.js +25 -16
- package/lib/ui/status.js.map +1 -1
- package/lib/ui/theme-picker.d.ts +18 -0
- package/lib/ui/theme-picker.d.ts.map +1 -0
- package/lib/ui/theme-picker.js +68 -0
- package/lib/ui/theme-picker.js.map +1 -0
- package/lib/ui/view.d.ts +146 -22
- package/lib/ui/view.d.ts.map +1 -1
- package/lib/ui/view.js +461 -154
- package/lib/ui/view.js.map +1 -1
- package/lib/work.d.ts +3 -1
- package/lib/work.d.ts.map +1 -1
- package/lib/work.js +9 -1
- package/lib/work.js.map +1 -1
- package/package.json +7 -1
- package/themes/deepseek-blue.yaml +183 -0
- 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":"AAAA,iDAAiD;AACjD,EAAE;AACF,iFAAiF;AACjF,4EAA4E;AAC5E,2EAA2E;AAC3E,2EAA2E;AAC3E,kDAAkD;AAClD,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,4EAA4E;AAC5E,8EAA8E;AAC9E,sEAAsE;AAEtE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAA;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAClC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAE1D,OAAO,EACL,sBAAsB,EACtB,sBAAsB,EACtB,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,EACnB,sBAAsB,EACtB,yBAAyB,GAC1B,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC,MAAM,aAAa,GAAG,EAAE,CAAA;AACxB,MAAM,eAAe,GAAG,IAAI,CAAA;AAC5B,MAAM,aAAa,GAAG,MAAM,CAAA;AAC5B,MAAM,eAAe,GAAG,YAAY,CAAA;AACpC,MAAM,oBAAoB,GAAG,QAAQ,CAAA;AACrC;;;;;;;;GAQG;AACH,MAAM,cAAc,GAAG,SAAS,CAAA;AAChC,MAAM,qBAAqB,GAAG,EAAE,CAAA;AAChC,MAAM,oBAAoB,GAAG,mCAAmC,CAAA;AAUhE;;;;;;GAMG;AACH,MAAM,OAAO,mBAAsC,SAAQ,KAAK;IACrD,SAAS,GAAG,IAAI,CAAA;IAChB,MAAM,CAAQ;IACd,OAAO,CAAc;IAC9B,4EAA4E;IACnE,cAAc,CAA0B;IAEjD,YAAY,MAAc,EAAE,OAAqB,EAAE,cAA6B;QAC9E,KAAK,CACH,cAAc,KAAK,SAAS;YAC1B,CAAC,CAAC,gCAAgC,OAAO,CAAC,KAAK,SAAS;YACxD,CAAC,CAAC,gCAAgC,OAAO,CAAC,KAAK,QAAQ,cAAc,CAAC,KAAK,SAAS,EACtF,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CACzB,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAA;QACjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,cAAc,GAAG,cAAc,CAAA;IACtC,CAAC;CACF;AAcD,+EAA+E;AAC/E,MAAM,UAAU,iBAAiB,CAAS,QAAgB,EAAE,SAAgC;IAC1F,OAAO,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAA;AAClD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,QAAgB,EAChB,SAAqD;IAErD,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,IAAI,CAAC,CAAA;IAC9D,OAAO,OAAO,CAAC,MAAM,CAAA;AACvB,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,QAAgB,EAChB,SAAgC,EAChC,QAAiB;IAEjB,MAAM,sBAAsB,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAA;IACpD,MAAM,OAAO,GAAG,GAAG,QAAQ,OAAO,CAAA;IAClC,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAA;IAC7C,IAAI,MAAc,CAAA;IAClB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,SAAS,EAAE,CAAA;IAC5B,CAAC;IAAC,OAAO,cAAc,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QACxC,CAAC;QAAC,OAAO,YAAY,EAAE,CAAC;YACtB,IAAI,cAAc,YAAY,mBAAmB,EAAE,CAAC;gBAClD,uEAAuE;gBACvE,kEAAkE;gBAClE,qEAAqE;gBACrE,oEAAoE;gBACpE,MAAM,IAAI,mBAAmB,CAAC,cAAc,CAAC,MAAM,EAAE,cAAc,CAAC,OAAO,EAAE;oBAC3E,KAAK,EAAE,cAAc;oBACrB,KAAK,EAAE,YAAY;iBACpB,CAAC,CAAA;YACJ,CAAC;YACD,MAAM,IAAI,cAAc,CAAC,CAAC,cAAc,EAAE,YAAY,CAAC,EAAE,8CAA8C,CAAC,CAAA;QAC1G,CAAC;QACD,MAAM,cAAc,CAAA;IACtB,CAAC;IACD,IAAI,CAAC;QACH,MAAM,gBAAgB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IACxC,CAAC;IAAC,OAAO,YAAY,EAAE,CAAC;QACtB,IAAI,QAAQ,IAAI,mBAAmB,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5C,MAAM,IAAI,mBAAmB,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAA;QAC9F,CAAC;QACD,4EAA4E;QAC5E,4EAA4E;QAC5E,6EAA6E;QAC7E,+DAA+D;QAC/D,IAAI,QAAQ;YAAE,MAAM,YAAY,CAAA;IAClC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAc;IACzC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,YAAY,IAAI,KAAK;QACrB,KAAK,CAAC,UAAU,KAAK,IAAI;QACzB,QAAQ,IAAI,KAAK,CAClB,CAAA;AACH,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,OAAe;IAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,eAAe,CAAA;IAC7C,SAAS,CAAC;QACR,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC,CAAA;YAChD,IAAI,CAAC;gBACH,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;gBACnD,OAAO,MAAM,cAAc,CAAC,OAAO,CAAC,CAAA;YACtC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,mEAAmE;gBACnE,yEAAyE;gBACzE,qEAAqE;gBACrE,wEAAwE;gBACxE,uEAAuE;gBACvE,IAAI,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,CAAC;oBACnE,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ;wBAAE,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;oBACtD,SAAQ;gBACV,CAAC;gBACD,MAAM,KAAK,CAAA;YACb,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC;gBAAE,MAAM,KAAK,CAAA;YAC/C,IAAI,CAAC;gBACH,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;YACrD,CAAC;YAAC,OAAO,eAAe,EAAE,CAAC;gBACzB,IAAI,YAAY,CAAC,eAAe,EAAE,QAAQ,CAAC;oBAAE,SAAQ;gBACrD,MAAM,eAAe,CAAA;YACvB,CAAC;YACD,IAAI,MAAM,oBAAoB,CAAC,OAAO,CAAC;gBAAE,SAAQ;YACjD,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ;gBAAE,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;YACtD,MAAM,KAAK,CAAC,aAAa,CAAC,CAAA;QAC5B,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,OAAe;IAClC,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IACvC,OAAO,IAAI,KAAK,CACd,2CAA2C,OAAO,mBAAmB,KAAK,6BAA6B,CACxG,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACxF,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,GAAG,cAAc,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,qBAAqB,CAAC,EAAE,CAAC,CAAA;AACvG,CAAC;AAED,KAAK,UAAU,cAAc,CAAC,OAAe;IAC3C,MAAM,KAAK,GAAc;QACvB,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,IAAI,EAAE,QAAQ,EAAE;QAChB,KAAK,EAAE,WAAW,EAAE;QACpB,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACpC,CAAA;IACD,MAAM,yBAAyB,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAClG,OAAO,KAAK,CAAA;AACd,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,OAAe;IAC1C,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;QACnD,IAAI,GAAG,CAAC,MAAM,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAA;IACjG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAO,SAAS,CAAA;QACnD,MAAM,KAAK,CAAA;IACb,CAAC;IACD,IAAI,CAAC;QACH,MAAM,KAAK,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QACvC,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,KAAK,IAAI,KAAK;QACd,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,GAAG,CAAC;QAC/B,MAAM,IAAI,KAAK;QACf,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ;QAC9B,OAAO,IAAI,KAAK;QAChB,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ;QAC/B,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;QACtB,WAAW,IAAI,KAAK;QACpB,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,CACpC,CAAA;AACH,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,OAAe,EAAE,QAAmB;IAClE,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAA;IAC9C,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAA;IAC9D,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,KAAK,CAAC,CAAA;IACxC,IAAI,KAAK,EAAE,KAAK,KAAK,QAAQ,CAAC,KAAK,EAAE,CAAC;QACpC,MAAM,oBAAoB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;QAC1C,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAA;IACvC,CAAC;IACD,MAAM,sBAAsB,CAAC,KAAK,EAAE,YAAY,CAAC,CAAA;AACnD,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,iBAAiB,CAAC,OAAe;IAC9C,MAAM,KAAK,GAAG,GAAG,OAAO,GAAG,oBAAoB,IAAI,WAAW,EAAE,EAAE,CAAA;IAClE,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,CAAA;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,YAAY,CAAC,KAAK,EAAE,WAAW,CAAC,IAAI,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,CAAC;YACvG,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,MAAM,KAAK,CAAA;IACb,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;;GAOG;AACH,KAAK,UAAU,oBAAoB,CAAC,KAAa,EAAE,OAAe;IAChE,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IAC9B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAM;QACzC,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACpB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,2EAA2E;QAC3E,gEAAgE;QAChE,OAAO,CAAC,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;IACtC,CAAC;AACH,CAAC;AAiBD,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAe;IAC/C,IAAI,KAAyD,CAAA;IAC7D,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;IAC7D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAO,SAAS,CAAA;QACnD,MAAM,KAAK,CAAA;IACb,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAA;IAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,EAAE,QAAQ,EAAE,KAAK,CAAC,OAAO,EAAE,CAAA;AAChF,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,KAAK,UAAU,oBAAoB,CAAC,OAAe;IACjD,MAAM,KAAK,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;IACvC,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,KAAK,CAAC,CAAA;IACvC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACrC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;QAC3C,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAA;QACtE,OAAO,MAAM,kBAAkB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACpD,CAAC;YAAS,CAAC;QACT,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QACvD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IACzD,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,OAAe,EAAE,QAAyB;IACjF,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,CAAA;IAC1C,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACvC,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,OAAO,CAAC,KAAK,KAAK,QAAQ,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IACxF,IAAI,OAAO,CAAC,KAAK,EAAE,KAAK,KAAK,QAAQ,CAAC,KAAK,EAAE,KAAK;QAAE,OAAO,KAAK,CAAA;IAChE,MAAM,sBAAsB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;IACnD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,mFAAmF;AACnF,SAAS,eAAe,CAAC,QAAyB;IAChD,IAAI,QAAQ,CAAC,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,CAAC,QAAQ,GAAG,aAAa,CAAA;IACvF,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE;QAAE,OAAO,KAAK,CAAA;IACpD,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;AAC5C,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,YAAY,CAAC,KAAa;IACvC,MAAM,KAAK,GAAG,GAAG,KAAK,IAAI,WAAW,EAAE,EAAE,CAAA;IACzC,MAAM,yBAAyB,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,CAAA;IACrG,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;QACxB,OAAO,KAAK,CAAA;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;QACvD,IAAI,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAO,SAAS,CAAA;QACnD,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC"}
|
|
@@ -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"}
|