@ddtcorex/dsh-maestro-memory 1.0.1
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/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +448 -0
- package/cordis.patch.yml +13 -0
- package/lib/client.js +756 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +627 -0
- package/lib/index.js.map +1 -0
- package/lib/memory/batch.d.ts +44 -0
- package/lib/memory/batch.d.ts.map +1 -0
- package/lib/memory/batch.js +75 -0
- package/lib/memory/batch.js.map +1 -0
- package/lib/memory/feedback.d.ts +18 -0
- package/lib/memory/feedback.d.ts.map +1 -0
- package/lib/memory/feedback.js +29 -0
- package/lib/memory/feedback.js.map +1 -0
- package/lib/memory/store.d.ts +154 -0
- package/lib/memory/store.d.ts.map +1 -0
- package/lib/memory/store.js +606 -0
- package/lib/memory/store.js.map +1 -0
- package/lib/migration/cli.d.ts +18 -0
- package/lib/migration/cli.d.ts.map +1 -0
- package/lib/migration/cli.js +143 -0
- package/lib/migration/cli.js.map +1 -0
- package/lib/migration/fixture.d.ts +45 -0
- package/lib/migration/fixture.d.ts.map +1 -0
- package/lib/migration/fixture.js +177 -0
- package/lib/migration/fixture.js.map +1 -0
- package/lib/migration/service.d.ts +75 -0
- package/lib/migration/service.d.ts.map +1 -0
- package/lib/migration/service.js +713 -0
- package/lib/migration/service.js.map +1 -0
- package/lib/prompt/snapshot.d.ts +26 -0
- package/lib/prompt/snapshot.d.ts.map +1 -0
- package/lib/prompt/snapshot.js +74 -0
- package/lib/prompt/snapshot.js.map +1 -0
- package/lib/review/queue.d.ts +72 -0
- package/lib/review/queue.d.ts.map +1 -0
- package/lib/review/queue.js +306 -0
- package/lib/review/queue.js.map +1 -0
- package/lib/skills-browser.d.ts +67 -0
- package/lib/skills-browser.d.ts.map +1 -0
- package/lib/skills-browser.js +185 -0
- package/lib/skills-browser.js.map +1 -0
- package/lib/storage/atomic-store.d.ts +211 -0
- package/lib/storage/atomic-store.d.ts.map +1 -0
- package/lib/storage/atomic-store.js +582 -0
- package/lib/storage/atomic-store.js.map +1 -0
- package/lib/storage/layout.d.ts +81 -0
- package/lib/storage/layout.d.ts.map +1 -0
- package/lib/storage/layout.js +162 -0
- package/lib/storage/layout.js.map +1 -0
- package/lib/storage/legacy-format.d.ts +104 -0
- package/lib/storage/legacy-format.d.ts.map +1 -0
- package/lib/storage/legacy-format.js +262 -0
- package/lib/storage/legacy-format.js.map +1 -0
- package/lib/sync/config.d.ts +22 -0
- package/lib/sync/config.d.ts.map +1 -0
- package/lib/sync/config.js +57 -0
- package/lib/sync/config.js.map +1 -0
- package/lib/sync/git.d.ts +97 -0
- package/lib/sync/git.d.ts.map +1 -0
- package/lib/sync/git.js +282 -0
- package/lib/sync/git.js.map +1 -0
- package/lib/sync/index.d.ts +6 -0
- package/lib/sync/index.d.ts.map +1 -0
- package/lib/sync/index.js +6 -0
- package/lib/sync/index.js.map +1 -0
- package/lib/sync/layout.d.ts +15 -0
- package/lib/sync/layout.d.ts.map +1 -0
- package/lib/sync/layout.js +33 -0
- package/lib/sync/layout.js.map +1 -0
- package/lib/sync/merge.d.ts +42 -0
- package/lib/sync/merge.d.ts.map +1 -0
- package/lib/sync/merge.js +159 -0
- package/lib/sync/merge.js.map +1 -0
- package/lib/sync/service.d.ts +70 -0
- package/lib/sync/service.d.ts.map +1 -0
- package/lib/sync/service.js +508 -0
- package/lib/sync/service.js.map +1 -0
- package/lib/todo/store.d.ts +142 -0
- package/lib/todo/store.d.ts.map +1 -0
- package/lib/todo/store.js +452 -0
- package/lib/todo/store.js.map +1 -0
- package/lib/types/client/index.d.ts +7 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/package.json +63 -0
- package/src/client/index.tsx +994 -0
- package/src/host/index.ts +591 -0
- package/src/host/memory/batch.ts +107 -0
- package/src/host/memory/feedback.ts +36 -0
- package/src/host/memory/store.ts +650 -0
- package/src/host/migration/cli.ts +139 -0
- package/src/host/migration/fixture.ts +218 -0
- package/src/host/migration/service.ts +786 -0
- package/src/host/prompt/snapshot.ts +95 -0
- package/src/host/review/queue.ts +325 -0
- package/src/host/skills-browser.ts +191 -0
- package/src/host/storage/atomic-store.ts +605 -0
- package/src/host/storage/layout.ts +191 -0
- package/src/host/storage/legacy-format.ts +308 -0
- package/src/host/sync/config.ts +67 -0
- package/src/host/sync/git.ts +280 -0
- package/src/host/sync/index.ts +5 -0
- package/src/host/sync/layout.ts +39 -0
- package/src/host/sync/merge.ts +188 -0
- package/src/host/sync/service.ts +494 -0
- package/src/host/todo/store.ts +480 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* host/skills-browser.ts — M6 read-first skills browser
|
|
3
|
+
*
|
|
4
|
+
* Goal: expose existing Maestro skills without taking ownership.
|
|
5
|
+
* - Confirm boundary with maestro-skills and list metadata/origin only.
|
|
6
|
+
* - Read-only: no mutation, no write API.
|
|
7
|
+
* - Model suggestions cannot change skills.
|
|
8
|
+
* - Browser cannot alter maestro-skills discovery behavior (no provider registration, no writes).
|
|
9
|
+
*
|
|
10
|
+
* If later approved to edit, mutation must require explicit user action and
|
|
11
|
+
* symlink/path containment tests before any write support. This file prepares
|
|
12
|
+
* the containment helper (isPathContained) and documents the guard, but does
|
|
13
|
+
* not implement mutation in M6.
|
|
14
|
+
*
|
|
15
|
+
* NOTE for future mutation (NOT implemented in M6):
|
|
16
|
+
* Any future mutation endpoint must require an explicit user action
|
|
17
|
+
* (UI click with confirmation) and must verify path containment via
|
|
18
|
+
* isPathContained(targetPath, allowedRoot) including realpath resolution
|
|
19
|
+
* for symlinks, rejecting any traversal or symlink escape. Tests for
|
|
20
|
+
* symlink/path containment must pass before mutation is enabled.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, readdirSync, statSync, lstatSync, readFileSync } from 'node:fs';
|
|
23
|
+
import { join, resolve, relative } from 'node:path';
|
|
24
|
+
/**
|
|
25
|
+
* Whether child is contained inside parent (no traversal escape).
|
|
26
|
+
* Uses resolved absolute paths and checks relative path does not start with "..".
|
|
27
|
+
* This helper prepares symlink/path containment tests required before any
|
|
28
|
+
* future mutation support — explicit user action + containment check.
|
|
29
|
+
*/
|
|
30
|
+
export function isPathContained(child, parent) {
|
|
31
|
+
const resolvedChild = resolve(child);
|
|
32
|
+
const resolvedParent = resolve(parent);
|
|
33
|
+
if (resolvedChild === resolvedParent)
|
|
34
|
+
return true;
|
|
35
|
+
const rel = relative(resolvedParent, resolvedChild);
|
|
36
|
+
// contained if rel does not start with ".." and is not absolute
|
|
37
|
+
return rel !== '' && !rel.startsWith('..') && !rel.startsWith('/');
|
|
38
|
+
}
|
|
39
|
+
function parseFrontmatter(rawContent) {
|
|
40
|
+
if (!rawContent.startsWith('---'))
|
|
41
|
+
return { metadata: {}, body: rawContent };
|
|
42
|
+
const endIdx = rawContent.indexOf('\n---', 3);
|
|
43
|
+
if (endIdx === -1)
|
|
44
|
+
return { metadata: {}, body: rawContent };
|
|
45
|
+
const yamlLines = rawContent.slice(3, endIdx).split('\n');
|
|
46
|
+
const body = rawContent.slice(endIdx + 4).trim();
|
|
47
|
+
const metadata = {};
|
|
48
|
+
let blockKey;
|
|
49
|
+
for (const line of yamlLines) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
const indent = line.length - trimmed.length;
|
|
52
|
+
if (blockKey !== undefined) {
|
|
53
|
+
if (trimmed === '') {
|
|
54
|
+
blockKey = undefined;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (indent > 0) {
|
|
58
|
+
metadata[blockKey] = (metadata[blockKey] + ' ' + trimmed).replace(/\s+/g, ' ').trim();
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
blockKey = undefined;
|
|
62
|
+
}
|
|
63
|
+
const colonIdx = trimmed.indexOf(':');
|
|
64
|
+
if (colonIdx <= 0)
|
|
65
|
+
continue;
|
|
66
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
67
|
+
const val = trimmed.slice(colonIdx + 1).trim();
|
|
68
|
+
if (val.startsWith('|') || val.startsWith('>')) {
|
|
69
|
+
metadata[key] = '';
|
|
70
|
+
blockKey = key;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
metadata[key] = val.startsWith('"') && val.endsWith('"') ? val.slice(1, -1) : val;
|
|
74
|
+
}
|
|
75
|
+
return { metadata, body };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* List skills from a single directory, read-only.
|
|
79
|
+
* - Scans <skillsDir>/* /SKILL.md
|
|
80
|
+
* - Parses frontmatter for name/description
|
|
81
|
+
* - Returns metadata/origin only, does not return body/content
|
|
82
|
+
* - Does not follow symlink escapes for future mutation; for listing we
|
|
83
|
+
* report the logical path, but future mutation will verify containment via
|
|
84
|
+
* isPathContained + realpath.
|
|
85
|
+
*/
|
|
86
|
+
export function listSkillsSync(skillsDir, origin = 'custom') {
|
|
87
|
+
if (!skillsDir)
|
|
88
|
+
return [];
|
|
89
|
+
let entries = [];
|
|
90
|
+
try {
|
|
91
|
+
entries = readdirSync(skillsDir);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const out = [];
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const skillFolder = join(skillsDir, entry);
|
|
99
|
+
let st = null;
|
|
100
|
+
try {
|
|
101
|
+
st = lstatSync(skillFolder);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// allow directory or symlink to directory
|
|
107
|
+
const isDir = st.isDirectory() || st.isSymbolicLink();
|
|
108
|
+
if (!isDir)
|
|
109
|
+
continue;
|
|
110
|
+
// verify it is a directory (follow symlink)
|
|
111
|
+
try {
|
|
112
|
+
const follow = statSync(skillFolder);
|
|
113
|
+
if (!follow.isDirectory())
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const skillFilePath = join(skillFolder, 'SKILL.md');
|
|
120
|
+
try {
|
|
121
|
+
const fileSt = statSync(skillFilePath);
|
|
122
|
+
if (!fileSt.isFile())
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let rawContent = '';
|
|
129
|
+
try {
|
|
130
|
+
rawContent = readFileSync(skillFilePath, 'utf8');
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const { metadata } = parseFrontmatter(rawContent);
|
|
136
|
+
const skillName = metadata.name || entry;
|
|
137
|
+
const description = metadata.description || `Skill for ${skillName}`;
|
|
138
|
+
out.push({
|
|
139
|
+
name: skillName,
|
|
140
|
+
description,
|
|
141
|
+
origin,
|
|
142
|
+
path: skillFilePath,
|
|
143
|
+
locator: skillFilePath,
|
|
144
|
+
metadata,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Async variant aggregating multiple roots.
|
|
151
|
+
* Each root is { dir, origin }. If only skillsDir is supplied, use that.
|
|
152
|
+
*/
|
|
153
|
+
export async function listSkills(opts = {}) {
|
|
154
|
+
if (opts.roots && opts.roots.length > 0) {
|
|
155
|
+
const all = [];
|
|
156
|
+
for (const r of opts.roots) {
|
|
157
|
+
const entries = listSkillsSync(r.dir, r.origin);
|
|
158
|
+
all.push(...entries);
|
|
159
|
+
}
|
|
160
|
+
return all;
|
|
161
|
+
}
|
|
162
|
+
if (opts.skillsDir) {
|
|
163
|
+
return listSkillsSync(opts.skillsDir, opts.origin ?? 'custom');
|
|
164
|
+
}
|
|
165
|
+
// default: no dir => empty (caller should supply maestro-skills dir explicitly)
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Resolve default maestro-skills directory for browser (read-only).
|
|
170
|
+
* Tries well-known checkout location; returns null if not present.
|
|
171
|
+
* This does NOT alter maestro-skills discovery — it only reads the directory
|
|
172
|
+
* if it exists, for metadata/origin listing.
|
|
173
|
+
*/
|
|
174
|
+
export function resolveDefaultMaestroSkillsDir() {
|
|
175
|
+
const candidates = [
|
|
176
|
+
'/home/kai/Work/htdocs/maestro-harness/maestro-skills/skills',
|
|
177
|
+
join(process.cwd(), '../maestro-skills/skills'),
|
|
178
|
+
];
|
|
179
|
+
for (const c of candidates) {
|
|
180
|
+
if (existsSync(c))
|
|
181
|
+
return c;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=skills-browser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills-browser.js","sourceRoot":"","sources":["../src/host/skills-browser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic file store for Maestro memory — per-directory lock, temp-write/rename,
|
|
3
|
+
* reread validation, backups, duplicate detection.
|
|
4
|
+
*
|
|
5
|
+
* Each mutation:
|
|
6
|
+
* 1. Acquires a same-directory lock file (.maestro.lock) with stale detection
|
|
7
|
+
* (mtime timeout + pid liveness via kill(pid,0)).
|
|
8
|
+
* 2. Validates the on-disk content round-trips through parseEntries/serializeEntries.
|
|
9
|
+
* Non-canonical files are backed up to `<file>.bak.<timestamp>` before the
|
|
10
|
+
* operation is refused (drift guard).
|
|
11
|
+
* 3. Checks for exact duplicates (ID-stripped) so re-adds are no-ops.
|
|
12
|
+
* 4. Writes to a same-directory temp file (`.<uuid>.tmp`, mode 0o600, flag wx),
|
|
13
|
+
* fsyncs the temp, renames atomically over the target, fsyncs the parent
|
|
14
|
+
* directory on POSIX, then rereads and validates the result.
|
|
15
|
+
* 5. Cleans up the temp file on any failure; the previous file is never
|
|
16
|
+
* clobbered on error.
|
|
17
|
+
*
|
|
18
|
+
* Uses only `node:fs` / `node:fs/promises` atomic primitives: open(wx),
|
|
19
|
+
* writeFile, fsync, rename, unlink. No third-party dependencies.
|
|
20
|
+
*
|
|
21
|
+
* @module storage/atomic-store
|
|
22
|
+
*/
|
|
23
|
+
/** Entry delimiter, byte-compatible with Hermes MEMORY.md / USER.md. */
|
|
24
|
+
export declare const ENTRY_DELIMITER = "\n\u00A7\n";
|
|
25
|
+
/**
|
|
26
|
+
* Split raw file text into trimmed, non-empty entries.
|
|
27
|
+
* @param text - raw file content
|
|
28
|
+
* @returns entries
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseEntries(text: string): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Serialize entries into canonical file text.
|
|
33
|
+
* @param entries - entries to serialize
|
|
34
|
+
* @returns canonical file content (entries joined by delimiter + trailing newline)
|
|
35
|
+
*/
|
|
36
|
+
export declare function serializeEntries(entries: string[]): string;
|
|
37
|
+
/**
|
|
38
|
+
* Whether raw text is the canonical serialization of its own entries.
|
|
39
|
+
* Blank / whitespace-only text counts as canonical (empty store).
|
|
40
|
+
* @param text - raw file content
|
|
41
|
+
* @returns true when round-tripping preserves the text
|
|
42
|
+
*/
|
|
43
|
+
export declare function isCanonical(text: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Strip the `[id:xxxxxxxx]` prefix from an entry, if present.
|
|
46
|
+
* @param entry - full entry text
|
|
47
|
+
* @returns text without the ID prefix
|
|
48
|
+
*/
|
|
49
|
+
export declare function stripEntryId(entry: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* Check whether `candidate` already exists in `entries`, comparing after
|
|
52
|
+
* stripping IDs so a re-add with a new random ID is still detected as a
|
|
53
|
+
* duplicate (IDs are random per write, direct includes would never hit).
|
|
54
|
+
* @param entries - existing entries
|
|
55
|
+
* @param candidate - new entry text
|
|
56
|
+
* @returns true when a duplicate exists
|
|
57
|
+
*/
|
|
58
|
+
export declare function isDuplicate(entries: string[], candidate: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Find the exact index of `exact` in `entries`, ID-immune (strip-and-compare).
|
|
61
|
+
* Returns -1 when not found or when multiple hits make the match ambiguous.
|
|
62
|
+
* @param entries - existing entries
|
|
63
|
+
* @param exact - full entry text to find
|
|
64
|
+
* @returns index or -1
|
|
65
|
+
*/
|
|
66
|
+
export declare function findExactIndex(entries: string[], exact: string): number;
|
|
67
|
+
/** A lock file older than this is considered abandoned (stale). */
|
|
68
|
+
export declare const STALE_LOCK_MS = 10000;
|
|
69
|
+
/** How long to keep waiting for the lock before failing. */
|
|
70
|
+
export declare const LOCK_TIMEOUT_MS = 5000;
|
|
71
|
+
/** Spin interval while waiting for the lock. */
|
|
72
|
+
export declare const LOCK_RETRY_MS = 25;
|
|
73
|
+
/** Lock file name inside each directory. */
|
|
74
|
+
export declare const LOCK_FILE = ".maestro.lock";
|
|
75
|
+
/**
|
|
76
|
+
* Determine whether a lock is stale (exported for tests): mtime timeout or
|
|
77
|
+
* the pid inside the lock file is no longer alive (killed / power loss ->
|
|
78
|
+
* stale immediately without waiting for timeout).
|
|
79
|
+
* @param lockPath - lock file path
|
|
80
|
+
* @returns true when stale
|
|
81
|
+
*/
|
|
82
|
+
export declare function isStaleLock(lockPath: string): boolean;
|
|
83
|
+
/** Async variant of stale check for the async lock path. */
|
|
84
|
+
export declare function isStaleLockAsync(lockPath: string): Promise<boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* Acquire the directory lock synchronously, run `fn`, release.
|
|
87
|
+
* Reentrant within this process (outer section remains exclusive vs others).
|
|
88
|
+
* @param dir - directory whose lock to take
|
|
89
|
+
* @param fn - critical section
|
|
90
|
+
* @returns section result
|
|
91
|
+
*/
|
|
92
|
+
export declare function withLockSync<T>(dir: string, fn: () => T): T;
|
|
93
|
+
/**
|
|
94
|
+
* Acquire the directory lock asynchronously, run `fn`, release.
|
|
95
|
+
* Reentrant within this process.
|
|
96
|
+
* @param dir - directory whose lock to take
|
|
97
|
+
* @param fn - critical section (may be async)
|
|
98
|
+
* @returns section result
|
|
99
|
+
*/
|
|
100
|
+
export declare function withLock<T>(dir: string, fn: () => T | Promise<T>): Promise<T>;
|
|
101
|
+
/**
|
|
102
|
+
* Create a backup of `filePath` at `<file>.bak.<timestamp>`.
|
|
103
|
+
* Returns the backup path, or null when the source does not exist.
|
|
104
|
+
* @param filePath - file to back up
|
|
105
|
+
* @returns backup path or null
|
|
106
|
+
*/
|
|
107
|
+
export declare function createBackupSync(filePath: string): string | null;
|
|
108
|
+
/**
|
|
109
|
+
* Async backup.
|
|
110
|
+
* @param filePath - file to back up
|
|
111
|
+
* @returns backup path or null
|
|
112
|
+
*/
|
|
113
|
+
export declare function createBackup(filePath: string): Promise<string | null>;
|
|
114
|
+
/**
|
|
115
|
+
* Durably replace `filePath` with `content` via same-directory temp file,
|
|
116
|
+
* fsync, atomic rename, directory fsync, and reread validation.
|
|
117
|
+
* @param filePath - absolute target file path
|
|
118
|
+
* @param content - full new file content
|
|
119
|
+
*/
|
|
120
|
+
export declare function writeAtomic(filePath: string, content: string): Promise<void>;
|
|
121
|
+
/**
|
|
122
|
+
* Synchronous atomic write (same guarantees, blocking).
|
|
123
|
+
* @param filePath - absolute target file path
|
|
124
|
+
* @param content - full new file content
|
|
125
|
+
*/
|
|
126
|
+
export declare function writeAtomicSync(filePath: string, content: string): void;
|
|
127
|
+
/**
|
|
128
|
+
* Read entries from `filePath`. Missing file yields [].
|
|
129
|
+
* @param filePath - file to read
|
|
130
|
+
* @returns parsed entries
|
|
131
|
+
*/
|
|
132
|
+
export declare function readEntriesSync(filePath: string): string[];
|
|
133
|
+
/**
|
|
134
|
+
* Async read entries.
|
|
135
|
+
* @param filePath - file to read
|
|
136
|
+
* @returns parsed entries
|
|
137
|
+
*/
|
|
138
|
+
export declare function readEntries(filePath: string): Promise<string[]>;
|
|
139
|
+
/**
|
|
140
|
+
* Append `entry` to `filePath` atomically, with duplicate detection and
|
|
141
|
+
* drift-guard backup.
|
|
142
|
+
*
|
|
143
|
+
* - Acquires per-directory lock.
|
|
144
|
+
* - Validates existing content is canonical; if not, backs up and throws.
|
|
145
|
+
* - Checks for duplicate (ID-stripped); returns `{duplicate:true}` without writing.
|
|
146
|
+
* - Writes via temp+rename+reread.
|
|
147
|
+
*
|
|
148
|
+
* @param filePath - target file
|
|
149
|
+
* @param entry - entry to append (already trimmed)
|
|
150
|
+
* @returns result
|
|
151
|
+
*/
|
|
152
|
+
export declare function appendEntryAtomic(filePath: string, entry: string): Promise<{
|
|
153
|
+
ok: true;
|
|
154
|
+
duplicate?: boolean;
|
|
155
|
+
entries: string[];
|
|
156
|
+
} | {
|
|
157
|
+
ok: false;
|
|
158
|
+
error: string;
|
|
159
|
+
backup?: string;
|
|
160
|
+
}>;
|
|
161
|
+
/**
|
|
162
|
+
* Synchronous variant of appendEntryAtomic.
|
|
163
|
+
* @param filePath - target file
|
|
164
|
+
* @param entry - entry to append
|
|
165
|
+
* @returns result
|
|
166
|
+
*/
|
|
167
|
+
export declare function appendEntryAtomicSync(filePath: string, entry: string): {
|
|
168
|
+
ok: true;
|
|
169
|
+
duplicate?: boolean;
|
|
170
|
+
entries: string[];
|
|
171
|
+
} | {
|
|
172
|
+
ok: false;
|
|
173
|
+
error: string;
|
|
174
|
+
backup?: string;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Replace or remove via atomic write with drift guard and backup.
|
|
178
|
+
* Caller provides the next entries array; this validates, backs up on drift, and writes.
|
|
179
|
+
* @param filePath - target file
|
|
180
|
+
* @param nextEntries - complete new entries array
|
|
181
|
+
* @returns result
|
|
182
|
+
*/
|
|
183
|
+
export declare function writeEntriesAtomic(filePath: string, nextEntries: string[]): Promise<{
|
|
184
|
+
ok: true;
|
|
185
|
+
entries: string[];
|
|
186
|
+
} | {
|
|
187
|
+
ok: false;
|
|
188
|
+
error: string;
|
|
189
|
+
backup?: string;
|
|
190
|
+
}>;
|
|
191
|
+
/**
|
|
192
|
+
* Synchronous variant.
|
|
193
|
+
* @param filePath - target file
|
|
194
|
+
* @param nextEntries - complete new entries array
|
|
195
|
+
* @returns result
|
|
196
|
+
*/
|
|
197
|
+
export declare function writeEntriesAtomicSync(filePath: string, nextEntries: string[]): {
|
|
198
|
+
ok: true;
|
|
199
|
+
entries: string[];
|
|
200
|
+
} | {
|
|
201
|
+
ok: false;
|
|
202
|
+
error: string;
|
|
203
|
+
backup?: string;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* Stable 12-hex project hash for a cwd.
|
|
207
|
+
* @param cwd - working directory
|
|
208
|
+
* @returns 12-char hash
|
|
209
|
+
*/
|
|
210
|
+
export declare function projectHash(cwd: string): string;
|
|
211
|
+
//# sourceMappingURL=atomic-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"atomic-store.d.ts","sourceRoot":"","sources":["../../src/host/storage/atomic-store.ts"],"names":[],"mappings":"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"}
|