agent-relay 4.0.32 → 4.0.33
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/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/src/cli/commands/on/start.d.ts.map +1 -1
- package/dist/src/cli/commands/on/start.js +149 -111
- package/dist/src/cli/commands/on/start.js.map +1 -1
- package/node_modules/@agent-relay/cloud/package.json +2 -2
- package/node_modules/@agent-relay/config/package.json +1 -1
- package/node_modules/@agent-relay/hooks/package.json +4 -4
- package/node_modules/@agent-relay/sdk/package.json +2 -2
- package/node_modules/@agent-relay/telemetry/package.json +1 -1
- package/node_modules/@agent-relay/trajectory/package.json +2 -2
- package/node_modules/@agent-relay/user-directory/package.json +2 -2
- package/node_modules/@agent-relay/utils/package.json +2 -2
- package/node_modules/@relayfile/local-mount/README.md +215 -0
- package/node_modules/@relayfile/local-mount/dist/auto-sync.d.ts +31 -0
- package/node_modules/@relayfile/local-mount/dist/auto-sync.js +466 -0
- package/node_modules/@relayfile/local-mount/dist/dotfiles.d.ts +18 -0
- package/node_modules/@relayfile/local-mount/dist/dotfiles.js +43 -0
- package/node_modules/@relayfile/local-mount/dist/index.d.ts +4 -0
- package/node_modules/@relayfile/local-mount/dist/index.js +3 -0
- package/node_modules/@relayfile/local-mount/dist/launch.d.ts +50 -0
- package/node_modules/@relayfile/local-mount/dist/launch.js +129 -0
- package/node_modules/@relayfile/local-mount/dist/symlink-mount.d.ts +23 -0
- package/{dist/src/cli/commands/on → node_modules/@relayfile/local-mount/dist}/symlink-mount.js +88 -20
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/LICENSE +21 -0
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/README.md +305 -0
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/esm/handler.d.ts +90 -0
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/esm/handler.js +629 -0
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/esm/index.d.ts +215 -0
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/esm/index.js +798 -0
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/esm/package.json +1 -0
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/handler.d.ts +90 -0
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/handler.js +635 -0
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/index.d.ts +215 -0
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/index.js +804 -0
- package/node_modules/@relayfile/local-mount/node_modules/chokidar/package.json +69 -0
- package/node_modules/@relayfile/local-mount/node_modules/readdirp/LICENSE +21 -0
- package/node_modules/@relayfile/local-mount/node_modules/readdirp/README.md +120 -0
- package/node_modules/@relayfile/local-mount/node_modules/readdirp/esm/index.d.ts +108 -0
- package/node_modules/@relayfile/local-mount/node_modules/readdirp/esm/index.js +257 -0
- package/node_modules/@relayfile/local-mount/node_modules/readdirp/esm/package.json +1 -0
- package/node_modules/@relayfile/local-mount/node_modules/readdirp/index.d.ts +108 -0
- package/node_modules/@relayfile/local-mount/node_modules/readdirp/index.js +263 -0
- package/node_modules/@relayfile/local-mount/node_modules/readdirp/package.json +70 -0
- package/node_modules/@relayfile/local-mount/package.json +47 -0
- package/node_modules/@smithy/config-resolver/package.json +2 -2
- package/node_modules/@smithy/core/dist-cjs/index.js +2 -1
- package/node_modules/@smithy/core/dist-cjs/submodules/cbor/index.js +32 -14
- package/node_modules/@smithy/core/dist-cjs/submodules/endpoints/index.js +2 -2
- package/node_modules/@smithy/core/dist-cjs/submodules/event-streams/index.js +16 -8
- package/node_modules/@smithy/core/dist-cjs/submodules/protocols/index.js +17 -10
- package/node_modules/@smithy/core/dist-cjs/submodules/schema/index.js +6 -1
- package/node_modules/@smithy/core/dist-cjs/submodules/serde/index.js +6 -3
- package/node_modules/@smithy/core/dist-cjs/util-identity-and-auth/DefaultIdentityProviderConfig.js +2 -1
- package/node_modules/@smithy/core/dist-es/submodules/cbor/CborCodec.js +23 -11
- package/node_modules/@smithy/core/dist-es/submodules/cbor/parseCborBody.js +9 -3
- package/node_modules/@smithy/core/dist-es/submodules/endpoints/toEndpointV1.js +2 -2
- package/node_modules/@smithy/core/dist-es/submodules/event-streams/EventStreamSerde.js +16 -8
- package/node_modules/@smithy/core/dist-es/submodules/protocols/HttpBindingProtocol.js +9 -4
- package/node_modules/@smithy/core/dist-es/submodules/protocols/HttpProtocol.js +8 -6
- package/node_modules/@smithy/core/dist-es/submodules/schema/TypeRegistry.js +6 -1
- package/node_modules/@smithy/core/dist-es/submodules/serde/parse-utils.js +6 -3
- package/node_modules/@smithy/core/dist-es/util-identity-and-auth/DefaultIdentityProviderConfig.js +2 -1
- package/node_modules/@smithy/core/dist-types/submodules/schema/TypeRegistry.d.ts +1 -1
- package/node_modules/@smithy/core/package.json +2 -2
- package/node_modules/@smithy/middleware-endpoint/package.json +3 -3
- package/node_modules/@smithy/middleware-retry/package.json +5 -5
- package/node_modules/@smithy/middleware-serde/package.json +2 -2
- package/node_modules/@smithy/node-http-handler/dist-cjs/index.js +188 -93
- package/node_modules/@smithy/node-http-handler/dist-es/http2/ClientHttp2SessionRef.js +45 -0
- package/node_modules/@smithy/node-http-handler/dist-es/node-http2-connection-manager.js +71 -35
- package/node_modules/@smithy/node-http-handler/dist-es/node-http2-connection-pool.js +32 -18
- package/node_modules/@smithy/node-http-handler/dist-es/node-http2-handler.js +41 -40
- package/node_modules/@smithy/node-http-handler/dist-types/http2/ClientHttp2SessionRef.d.ts +42 -0
- package/node_modules/@smithy/node-http-handler/dist-types/node-http2-connection-manager.d.ts +34 -14
- package/node_modules/@smithy/node-http-handler/dist-types/node-http2-connection-pool.d.ts +32 -8
- package/node_modules/@smithy/node-http-handler/dist-types/node-http2-handler.d.ts +0 -5
- package/node_modules/@smithy/node-http-handler/package.json +1 -1
- package/node_modules/@smithy/service-error-classification/dist-cjs/index.js +5 -0
- package/node_modules/@smithy/service-error-classification/dist-es/index.js +4 -0
- package/node_modules/@smithy/service-error-classification/dist-types/index.d.ts +6 -0
- package/node_modules/@smithy/service-error-classification/package.json +1 -1
- package/node_modules/@smithy/smithy-client/package.json +4 -4
- package/node_modules/@smithy/util-defaults-mode-browser/package.json +2 -2
- package/node_modules/@smithy/util-defaults-mode-node/package.json +3 -3
- package/node_modules/@smithy/util-endpoints/dist-cjs/index.js +65 -53
- package/node_modules/@smithy/util-endpoints/dist-es/utils/evaluateCondition.js +9 -7
- package/node_modules/@smithy/util-endpoints/dist-es/utils/evaluateConditions.js +12 -8
- package/node_modules/@smithy/util-endpoints/dist-es/utils/evaluateEndpointRule.js +14 -13
- package/node_modules/@smithy/util-endpoints/dist-es/utils/evaluateErrorRule.js +7 -4
- package/node_modules/@smithy/util-endpoints/dist-es/utils/evaluateExpression.js +10 -8
- package/node_modules/@smithy/util-endpoints/dist-es/utils/evaluateRules.js +4 -4
- package/node_modules/@smithy/util-endpoints/dist-es/utils/getEndpointHeaders.js +5 -5
- package/node_modules/@smithy/util-endpoints/dist-es/utils/getEndpointProperties.js +4 -4
- package/node_modules/@smithy/util-endpoints/dist-types/types/shared.d.ts +3 -3
- package/node_modules/@smithy/util-endpoints/dist-types/utils/endpointFunctions.d.ts +2 -15
- package/node_modules/@smithy/util-endpoints/dist-types/utils/evaluateCondition.d.ts +6 -3
- package/node_modules/@smithy/util-endpoints/dist-types/utils/evaluateConditions.d.ts +3 -3
- package/node_modules/@smithy/util-endpoints/dist-types/utils/getEndpointHeaders.d.ts +1 -1
- package/node_modules/@smithy/util-endpoints/dist-types/utils/getEndpointProperties.d.ts +2 -2
- package/node_modules/@smithy/util-endpoints/dist-types/utils/getReferenceValue.d.ts +2 -2
- package/node_modules/@smithy/util-endpoints/package.json +1 -1
- package/node_modules/@smithy/util-retry/package.json +2 -2
- package/node_modules/@smithy/util-stream/package.json +2 -2
- package/package.json +25 -11
- package/packages/cloud/package.json +2 -2
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/sdk/package.json +2 -2
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
- package/dist/src/cli/commands/on/symlink-mount.d.ts +0 -12
- package/dist/src/cli/commands/on/symlink-mount.d.ts.map +0 -1
- package/dist/src/cli/commands/on/symlink-mount.js.map +0 -1
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chokidar from 'chokidar';
|
|
4
|
+
export function startAutoSync(ctx, opts = {}) {
|
|
5
|
+
const scanIntervalMs = opts.scanIntervalMs ?? 10_000;
|
|
6
|
+
const writeFinishMs = opts.writeFinishMs ?? 200;
|
|
7
|
+
const onError = opts.onError ?? (() => { });
|
|
8
|
+
const state = new Map();
|
|
9
|
+
primeState(state, ctx);
|
|
10
|
+
let syncing = false;
|
|
11
|
+
let pending = false;
|
|
12
|
+
let totalChanges = 0;
|
|
13
|
+
const runReconcile = async () => {
|
|
14
|
+
if (syncing) {
|
|
15
|
+
pending = true;
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
syncing = true;
|
|
19
|
+
let count = 0;
|
|
20
|
+
try {
|
|
21
|
+
count = reconcile(state, ctx, onError);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
onError(err);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
syncing = false;
|
|
28
|
+
}
|
|
29
|
+
if (pending) {
|
|
30
|
+
pending = false;
|
|
31
|
+
try {
|
|
32
|
+
count += reconcile(state, ctx, onError);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
onError(err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
totalChanges += count;
|
|
39
|
+
return count;
|
|
40
|
+
};
|
|
41
|
+
const syncPathFromRoot = (root, absPath) => {
|
|
42
|
+
const rel = path.relative(root, absPath);
|
|
43
|
+
if (rel === '' || rel.startsWith('..'))
|
|
44
|
+
return;
|
|
45
|
+
const relPosix = rel.split(path.sep).join('/');
|
|
46
|
+
if (!isSyncCandidate(relPosix, ctx))
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
const changed = syncOneFile(relPosix, state, ctx);
|
|
50
|
+
if (changed)
|
|
51
|
+
totalChanges += 1;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
onError(err);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const makeWatcher = (root) => {
|
|
58
|
+
const watcher = chokidar.watch(root, {
|
|
59
|
+
ignoreInitial: true,
|
|
60
|
+
persistent: true,
|
|
61
|
+
followSymlinks: false,
|
|
62
|
+
awaitWriteFinish: {
|
|
63
|
+
stabilityThreshold: writeFinishMs,
|
|
64
|
+
pollInterval: 50,
|
|
65
|
+
},
|
|
66
|
+
ignored: (candidate, stats) => shouldChokidarIgnore(candidate, root, ctx, stats),
|
|
67
|
+
});
|
|
68
|
+
const onEvent = (p) => syncPathFromRoot(root, p);
|
|
69
|
+
watcher.on('add', onEvent);
|
|
70
|
+
watcher.on('change', onEvent);
|
|
71
|
+
watcher.on('unlink', onEvent);
|
|
72
|
+
watcher.on('error', (err) => onError(err));
|
|
73
|
+
const ready = new Promise((resolve) => {
|
|
74
|
+
watcher.once('ready', () => resolve());
|
|
75
|
+
});
|
|
76
|
+
return { watcher, ready };
|
|
77
|
+
};
|
|
78
|
+
const mount = makeWatcher(ctx.realMountDir);
|
|
79
|
+
const project = makeWatcher(ctx.realProjectDir);
|
|
80
|
+
const mountWatcher = mount.watcher;
|
|
81
|
+
const projectWatcher = project.watcher;
|
|
82
|
+
const watchersReady = Promise.all([mount.ready, project.ready]);
|
|
83
|
+
const interval = setInterval(() => {
|
|
84
|
+
void runReconcile();
|
|
85
|
+
}, scanIntervalMs);
|
|
86
|
+
// Do not keep the event loop alive just because of our scan timer.
|
|
87
|
+
interval.unref?.();
|
|
88
|
+
return {
|
|
89
|
+
async stop() {
|
|
90
|
+
clearInterval(interval);
|
|
91
|
+
await Promise.all([mountWatcher.close(), projectWatcher.close()]);
|
|
92
|
+
// Drain any pending work so callers can rely on "stopped means quiesced".
|
|
93
|
+
await runReconcile();
|
|
94
|
+
},
|
|
95
|
+
reconcile: runReconcile,
|
|
96
|
+
totalChanges: () => totalChanges,
|
|
97
|
+
ready: async () => {
|
|
98
|
+
await watchersReady;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function primeState(state, ctx) {
|
|
103
|
+
// Record current mtimes for every file that exists in both trees with the
|
|
104
|
+
// same content. Files that differ are left out so the first reconcile sees
|
|
105
|
+
// no prev entry and picks a winner via the content-based resolution path.
|
|
106
|
+
walk(ctx.realMountDir, ctx, (abs) => {
|
|
107
|
+
const rel = toRelPosix(abs, ctx);
|
|
108
|
+
if (rel === null)
|
|
109
|
+
return;
|
|
110
|
+
if (!isSyncCandidate(rel, ctx))
|
|
111
|
+
return;
|
|
112
|
+
const mountStat = safeFileStat(abs);
|
|
113
|
+
if (!mountStat)
|
|
114
|
+
return;
|
|
115
|
+
const projectAbs = path.join(ctx.realProjectDir, rel);
|
|
116
|
+
const projectStat = safeFileStat(projectAbs);
|
|
117
|
+
if (!projectStat)
|
|
118
|
+
return;
|
|
119
|
+
if (!sameContent(abs, projectAbs))
|
|
120
|
+
return;
|
|
121
|
+
state.set(rel, {
|
|
122
|
+
mountMtimeMs: mountStat.mtimeMs,
|
|
123
|
+
projectMtimeMs: projectStat.mtimeMs,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function reconcile(state, ctx, onError) {
|
|
128
|
+
const seen = new Set();
|
|
129
|
+
let count = 0;
|
|
130
|
+
const visit = (relPosix) => {
|
|
131
|
+
if (seen.has(relPosix))
|
|
132
|
+
return;
|
|
133
|
+
seen.add(relPosix);
|
|
134
|
+
if (!isSyncCandidate(relPosix, ctx))
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
const changed = syncOneFile(relPosix, state, ctx);
|
|
138
|
+
if (changed)
|
|
139
|
+
count += 1;
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
onError(err);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
walk(ctx.realMountDir, ctx, (abs) => {
|
|
146
|
+
const rel = toRelPosix(abs, ctx);
|
|
147
|
+
if (rel !== null)
|
|
148
|
+
visit(rel);
|
|
149
|
+
});
|
|
150
|
+
walk(ctx.realProjectDir, ctx, (abs) => {
|
|
151
|
+
const rel = toRelPosixFromProject(abs, ctx);
|
|
152
|
+
if (rel !== null)
|
|
153
|
+
visit(rel);
|
|
154
|
+
});
|
|
155
|
+
// Tombstone sweep: any path in state we didn't visit had both sides absent,
|
|
156
|
+
// so it's fully gone.
|
|
157
|
+
for (const rel of Array.from(state.keys())) {
|
|
158
|
+
if (!seen.has(rel)) {
|
|
159
|
+
const mountAbs = path.join(ctx.realMountDir, rel);
|
|
160
|
+
const projectAbs = path.join(ctx.realProjectDir, rel);
|
|
161
|
+
if (!existsSync(mountAbs) && !existsSync(projectAbs)) {
|
|
162
|
+
state.delete(rel);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return count;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Sync a single relPath and return true if a copy or delete actually happened.
|
|
170
|
+
*
|
|
171
|
+
* Resolution rules ("mount wins"):
|
|
172
|
+
* - If both sides changed since last sync → mount→project.
|
|
173
|
+
* - Only mount changed → mount→project (unless mount-side change is disallowed
|
|
174
|
+
* for readonly files; then drop the mount change).
|
|
175
|
+
* - Only project changed → project→mount.
|
|
176
|
+
* - One side missing:
|
|
177
|
+
* • Other side changed since last sync → recreate the missing side.
|
|
178
|
+
* • Otherwise → propagate the delete.
|
|
179
|
+
*/
|
|
180
|
+
function syncOneFile(relPosix, state, ctx) {
|
|
181
|
+
const mountAbs = path.join(ctx.realMountDir, relPosix);
|
|
182
|
+
const projectAbs = path.join(ctx.realProjectDir, relPosix);
|
|
183
|
+
const mountStat = safeFileStat(mountAbs);
|
|
184
|
+
const projectStat = safeFileStat(projectAbs);
|
|
185
|
+
const prev = state.get(relPosix);
|
|
186
|
+
const readonly = ctx.isReadonly(relPosix);
|
|
187
|
+
if (!mountStat && !projectStat) {
|
|
188
|
+
state.delete(relPosix);
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
if (!prev) {
|
|
192
|
+
// First time we've seen this path.
|
|
193
|
+
if (mountStat && projectStat) {
|
|
194
|
+
if (sameContent(mountAbs, projectAbs)) {
|
|
195
|
+
state.set(relPosix, {
|
|
196
|
+
mountMtimeMs: mountStat.mtimeMs,
|
|
197
|
+
projectMtimeMs: projectStat.mtimeMs,
|
|
198
|
+
});
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
// Differ with no history: arbitrary tiebreak → mount wins.
|
|
202
|
+
if (readonly) {
|
|
203
|
+
// Readonly can't accept mount-side writes; fall back to project→mount.
|
|
204
|
+
return doProjectToMount(relPosix, state, ctx, projectAbs, mountAbs, readonly);
|
|
205
|
+
}
|
|
206
|
+
return doMountToProject(relPosix, state, ctx, mountAbs, projectAbs);
|
|
207
|
+
}
|
|
208
|
+
if (mountStat && !projectStat) {
|
|
209
|
+
if (readonly) {
|
|
210
|
+
// New file in mount with a readonly pattern → cannot sync back.
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return doMountToProject(relPosix, state, ctx, mountAbs, projectAbs);
|
|
214
|
+
}
|
|
215
|
+
if (!mountStat && projectStat) {
|
|
216
|
+
return doProjectToMount(relPosix, state, ctx, projectAbs, mountAbs, readonly);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Use strict inequality rather than `>`: on filesystems with coarse mtime
|
|
220
|
+
// resolution, or after a backdated touch, a real content change can land
|
|
221
|
+
// with a non-greater mtime.
|
|
222
|
+
const mountChanged = mountStat
|
|
223
|
+
? prev?.mountMtimeMs === undefined || mountStat.mtimeMs !== prev.mountMtimeMs
|
|
224
|
+
: false;
|
|
225
|
+
const projectChanged = projectStat
|
|
226
|
+
? prev?.projectMtimeMs === undefined || projectStat.mtimeMs !== prev.projectMtimeMs
|
|
227
|
+
: false;
|
|
228
|
+
if (mountStat && projectStat) {
|
|
229
|
+
if (!mountChanged && !projectChanged)
|
|
230
|
+
return false;
|
|
231
|
+
if (mountChanged && !readonly) {
|
|
232
|
+
return doMountToProject(relPosix, state, ctx, mountAbs, projectAbs);
|
|
233
|
+
}
|
|
234
|
+
if (projectChanged) {
|
|
235
|
+
return doProjectToMount(relPosix, state, ctx, projectAbs, mountAbs, readonly);
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (mountStat && !projectStat) {
|
|
240
|
+
if (mountChanged && !readonly) {
|
|
241
|
+
return doMountToProject(relPosix, state, ctx, mountAbs, projectAbs);
|
|
242
|
+
}
|
|
243
|
+
// Project deleted externally and mount hasn't been touched since → mirror.
|
|
244
|
+
return doDeleteMount(relPosix, state, mountAbs);
|
|
245
|
+
}
|
|
246
|
+
if (!mountStat && projectStat) {
|
|
247
|
+
if (projectChanged) {
|
|
248
|
+
return doProjectToMount(relPosix, state, ctx, projectAbs, mountAbs, readonly);
|
|
249
|
+
}
|
|
250
|
+
// Mount deleted and project hasn't been touched since → mirror to project.
|
|
251
|
+
if (readonly) {
|
|
252
|
+
// Readonly deletes in mount don't sync back; recreate mount from project.
|
|
253
|
+
return doProjectToMount(relPosix, state, ctx, projectAbs, mountAbs, readonly);
|
|
254
|
+
}
|
|
255
|
+
return doDeleteProject(relPosix, state, projectAbs);
|
|
256
|
+
}
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
function doMountToProject(relPosix, state, ctx, mountAbs, projectAbs) {
|
|
260
|
+
const target = resolveSafeWriteTarget(ctx.realProjectDir, projectAbs);
|
|
261
|
+
if (!target)
|
|
262
|
+
return false;
|
|
263
|
+
if (isSymlinkTarget(target))
|
|
264
|
+
return false;
|
|
265
|
+
if (existsSync(target) && sameContent(mountAbs, target)) {
|
|
266
|
+
updateState(state, relPosix, mountAbs, target);
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
copyFileSync(mountAbs, target);
|
|
270
|
+
updateState(state, relPosix, mountAbs, target);
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
function doProjectToMount(relPosix, state, ctx, projectAbs, mountAbs, readonly) {
|
|
274
|
+
const target = resolveSafeWriteTarget(ctx.realMountDir, mountAbs);
|
|
275
|
+
if (!target)
|
|
276
|
+
return false;
|
|
277
|
+
if (isSymlinkTarget(target))
|
|
278
|
+
return false;
|
|
279
|
+
if (existsSync(target) && sameContent(projectAbs, target)) {
|
|
280
|
+
updateState(state, relPosix, target, projectAbs);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
// The mount copy of a readonly file has mode 0o444, which blocks
|
|
284
|
+
// copyFileSync from overwriting it. Temporarily restore write permission.
|
|
285
|
+
if (existsSync(target)) {
|
|
286
|
+
try {
|
|
287
|
+
chmodSync(target, 0o644);
|
|
288
|
+
}
|
|
289
|
+
catch { /* best effort */ }
|
|
290
|
+
}
|
|
291
|
+
copyFileSync(projectAbs, target);
|
|
292
|
+
if (readonly) {
|
|
293
|
+
try {
|
|
294
|
+
chmodSync(target, 0o444);
|
|
295
|
+
}
|
|
296
|
+
catch { /* best effort */ }
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const mode = safeFileStat(projectAbs)?.mode;
|
|
300
|
+
if (mode !== undefined) {
|
|
301
|
+
try {
|
|
302
|
+
chmodSync(target, mode & 0o777);
|
|
303
|
+
}
|
|
304
|
+
catch { /* best effort */ }
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
updateState(state, relPosix, target, projectAbs);
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
function doDeleteMount(relPosix, state, mountAbs) {
|
|
311
|
+
try {
|
|
312
|
+
rmSync(mountAbs, { force: true });
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
state.delete(relPosix);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
function doDeleteProject(relPosix, state, projectAbs) {
|
|
321
|
+
try {
|
|
322
|
+
rmSync(projectAbs, { force: true });
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
state.delete(relPosix);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
function updateState(state, relPosix, mountAbs, projectAbs) {
|
|
331
|
+
const mountStat = safeFileStat(mountAbs);
|
|
332
|
+
const projectStat = safeFileStat(projectAbs);
|
|
333
|
+
state.set(relPosix, {
|
|
334
|
+
mountMtimeMs: mountStat?.mtimeMs,
|
|
335
|
+
projectMtimeMs: projectStat?.mtimeMs,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
function isSyncCandidate(relPosix, ctx) {
|
|
339
|
+
if (!relPosix || relPosix.startsWith('..'))
|
|
340
|
+
return false;
|
|
341
|
+
if (ctx.isReservedFile(relPosix))
|
|
342
|
+
return false;
|
|
343
|
+
if (ctx.isExcluded(relPosix))
|
|
344
|
+
return false;
|
|
345
|
+
if (ctx.isIgnored(relPosix))
|
|
346
|
+
return false;
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
function toRelPosix(absPath, ctx) {
|
|
350
|
+
const rel = path.relative(ctx.realMountDir, absPath);
|
|
351
|
+
if (rel === '' || rel.startsWith('..'))
|
|
352
|
+
return null;
|
|
353
|
+
return rel.split(path.sep).join('/');
|
|
354
|
+
}
|
|
355
|
+
function toRelPosixFromProject(absPath, ctx) {
|
|
356
|
+
const rel = path.relative(ctx.realProjectDir, absPath);
|
|
357
|
+
if (rel === '' || rel.startsWith('..'))
|
|
358
|
+
return null;
|
|
359
|
+
return rel.split(path.sep).join('/');
|
|
360
|
+
}
|
|
361
|
+
function safeFileStat(p) {
|
|
362
|
+
try {
|
|
363
|
+
const s = lstatSync(p);
|
|
364
|
+
if (s.isSymbolicLink())
|
|
365
|
+
return null;
|
|
366
|
+
if (!s.isFile())
|
|
367
|
+
return null;
|
|
368
|
+
return s;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function isSymlinkTarget(target) {
|
|
375
|
+
// If the target already exists as a symlink, writing through it would
|
|
376
|
+
// follow the link and potentially escape the mount/project root. Refuse.
|
|
377
|
+
try {
|
|
378
|
+
return lstatSync(target).isSymbolicLink();
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function sameContent(left, right) {
|
|
385
|
+
try {
|
|
386
|
+
const a = statSync(left);
|
|
387
|
+
const b = statSync(right);
|
|
388
|
+
if (a.size !== b.size)
|
|
389
|
+
return false;
|
|
390
|
+
return readFileSync(left).equals(readFileSync(right));
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function resolveSafeWriteTarget(root, candidate) {
|
|
397
|
+
const resolvedRoot = path.resolve(root);
|
|
398
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
399
|
+
if (resolvedCandidate !== resolvedRoot &&
|
|
400
|
+
!resolvedCandidate.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
const parent = path.dirname(resolvedCandidate);
|
|
404
|
+
try {
|
|
405
|
+
mkdirSync(parent, { recursive: true });
|
|
406
|
+
const realParent = realpathSync(parent);
|
|
407
|
+
if (realParent !== resolvedRoot &&
|
|
408
|
+
!realParent.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
return path.join(realParent, path.basename(resolvedCandidate));
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function walk(root, ctx, visit) {
|
|
418
|
+
const stack = [root];
|
|
419
|
+
while (stack.length > 0) {
|
|
420
|
+
const cur = stack.pop();
|
|
421
|
+
if (!cur)
|
|
422
|
+
continue;
|
|
423
|
+
let entries;
|
|
424
|
+
try {
|
|
425
|
+
entries = readdirSync(cur, { withFileTypes: true });
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
for (const entry of entries) {
|
|
431
|
+
const abs = path.join(cur, entry.name);
|
|
432
|
+
const rel = path.relative(root, abs).split(path.sep).join('/');
|
|
433
|
+
if (!rel || rel.startsWith('..'))
|
|
434
|
+
continue;
|
|
435
|
+
if (ctx.isExcluded(rel) || ctx.isIgnored(rel, entry.isDirectory()))
|
|
436
|
+
continue;
|
|
437
|
+
if (entry.isDirectory()) {
|
|
438
|
+
stack.push(abs);
|
|
439
|
+
}
|
|
440
|
+
else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
441
|
+
visit(abs);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function shouldChokidarIgnore(candidate, root, ctx, stats) {
|
|
447
|
+
if (candidate === root)
|
|
448
|
+
return false;
|
|
449
|
+
const rel = path.relative(root, candidate);
|
|
450
|
+
if (rel === '' || rel.startsWith('..'))
|
|
451
|
+
return false;
|
|
452
|
+
const relPosix = rel.split(path.sep).join('/');
|
|
453
|
+
if (ctx.isExcluded(relPosix))
|
|
454
|
+
return true;
|
|
455
|
+
if (ctx.isReservedFile(relPosix))
|
|
456
|
+
return true;
|
|
457
|
+
// chokidar calls this filter twice: first without stats (pre-stat prune),
|
|
458
|
+
// then again with stats once it knows the entry type. Only apply the
|
|
459
|
+
// directory-form match when we have stats confirming it's a directory,
|
|
460
|
+
// otherwise a directory-only pattern like `cache/` would wrongly prune a
|
|
461
|
+
// same-named file.
|
|
462
|
+
if (stats) {
|
|
463
|
+
return ctx.isIgnored(relPosix, stats.isDirectory());
|
|
464
|
+
}
|
|
465
|
+
return ctx.isIgnored(relPosix);
|
|
466
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface ReadAgentDotfilesOptions {
|
|
2
|
+
/**
|
|
3
|
+
* If provided, also reads `.{agentName}.agentignore` and `.{agentName}.agentreadonly`
|
|
4
|
+
* from the project directory and merges the resulting patterns.
|
|
5
|
+
*/
|
|
6
|
+
agentName?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AgentDotfilePatterns {
|
|
9
|
+
ignoredPatterns: string[];
|
|
10
|
+
readonlyPatterns: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Read `.agentignore` and `.agentreadonly` dotfiles from a project directory
|
|
14
|
+
* and return the compiled pattern lists. If `agentName` is supplied, also
|
|
15
|
+
* reads `.{agentName}.agentignore` and `.{agentName}.agentreadonly` and
|
|
16
|
+
* appends their patterns.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readAgentDotfiles(projectDir: string, options?: ReadAgentDotfilesOptions): AgentDotfilePatterns;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
function cleanPatterns(content) {
|
|
4
|
+
return content
|
|
5
|
+
.split(/\r?\n/)
|
|
6
|
+
.map((line) => line.trim())
|
|
7
|
+
.filter((line) => line !== '' && !line.startsWith('#'));
|
|
8
|
+
}
|
|
9
|
+
function loadPatternsFromFile(filePath) {
|
|
10
|
+
if (!existsSync(filePath)) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const content = readFileSync(filePath, 'utf8');
|
|
14
|
+
return cleanPatterns(content);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read `.agentignore` and `.agentreadonly` dotfiles from a project directory
|
|
18
|
+
* and return the compiled pattern lists. If `agentName` is supplied, also
|
|
19
|
+
* reads `.{agentName}.agentignore` and `.{agentName}.agentreadonly` and
|
|
20
|
+
* appends their patterns.
|
|
21
|
+
*/
|
|
22
|
+
export function readAgentDotfiles(projectDir, options = {}) {
|
|
23
|
+
const resolvedProjectDir = path.resolve(projectDir);
|
|
24
|
+
const ignoredPatterns = [
|
|
25
|
+
...loadPatternsFromFile(path.join(resolvedProjectDir, '.agentignore')),
|
|
26
|
+
];
|
|
27
|
+
const readonlyPatterns = [
|
|
28
|
+
...loadPatternsFromFile(path.join(resolvedProjectDir, '.agentreadonly')),
|
|
29
|
+
];
|
|
30
|
+
if (options.agentName) {
|
|
31
|
+
const safeAgentName = sanitizeAgentName(options.agentName);
|
|
32
|
+
ignoredPatterns.push(...loadPatternsFromFile(path.join(resolvedProjectDir, `.${safeAgentName}.agentignore`)));
|
|
33
|
+
readonlyPatterns.push(...loadPatternsFromFile(path.join(resolvedProjectDir, `.${safeAgentName}.agentreadonly`)));
|
|
34
|
+
}
|
|
35
|
+
return { ignoredPatterns, readonlyPatterns };
|
|
36
|
+
}
|
|
37
|
+
const AGENT_NAME_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
38
|
+
function sanitizeAgentName(agentName) {
|
|
39
|
+
if (!AGENT_NAME_PATTERN.test(agentName) || path.basename(agentName) !== agentName) {
|
|
40
|
+
throw new Error(`Invalid agentName ${JSON.stringify(agentName)}: only [A-Za-z0-9_-] are allowed`);
|
|
41
|
+
}
|
|
42
|
+
return agentName;
|
|
43
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createSymlinkMount, type SymlinkMountOptions, type SymlinkMountHandle, } from './symlink-mount.js';
|
|
2
|
+
export { type AutoSyncOptions, type AutoSyncHandle, } from './auto-sync.js';
|
|
3
|
+
export { readAgentDotfiles, type ReadAgentDotfilesOptions, type AgentDotfilePatterns, } from './dotfiles.js';
|
|
4
|
+
export { launchOnMount, type LaunchOnMountOptions, type LaunchOnMountResult, } from './launch.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AutoSyncOptions } from './auto-sync.js';
|
|
2
|
+
export interface LaunchOnMountOptions {
|
|
3
|
+
/** Binary name or absolute path to the CLI to spawn, e.g. 'claude'. */
|
|
4
|
+
cli: string;
|
|
5
|
+
/** The real project directory to mirror. */
|
|
6
|
+
projectDir: string;
|
|
7
|
+
/** Where to create the mount. Must differ from projectDir. */
|
|
8
|
+
mountDir: string;
|
|
9
|
+
/** Argv to pass to the CLI after its binary name. */
|
|
10
|
+
args: string[];
|
|
11
|
+
/** Glob-style ignore patterns (files excluded entirely from the mount). */
|
|
12
|
+
ignoredPatterns?: string[];
|
|
13
|
+
/** Glob-style readonly patterns (files copied with mode 0o444). */
|
|
14
|
+
readonlyPatterns?: string[];
|
|
15
|
+
/** Extra directory names to exclude from the mount on top of defaults. */
|
|
16
|
+
excludeDirs?: string[];
|
|
17
|
+
/** Extra env vars merged on top of `process.env`. */
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
/** Optional agent name, used in the _MOUNT_README.md "Agent:" line. */
|
|
20
|
+
agentName?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Invoked after the mount is created but before the CLI is spawned.
|
|
23
|
+
* Useful for writing additional files into the mount (overrides, extra docs).
|
|
24
|
+
*/
|
|
25
|
+
onBeforeLaunch?: (mountDir: string) => void | Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Invoked after sync-back completes, before cleanup. Receives the total
|
|
28
|
+
* number of file changes propagated during the run — the sum of autosync
|
|
29
|
+
* activity in both directions (including deletes) and the final mount→
|
|
30
|
+
* project syncBack. Use this as an "anything changed?" signal rather than
|
|
31
|
+
* a strict mount→project count.
|
|
32
|
+
*/
|
|
33
|
+
onAfterSync?: (syncedFileCount: number) => void | Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Auto-sync behavior. By default, bidirectional auto-sync runs during the
|
|
36
|
+
* lifetime of the spawned CLI. Pass `false` to disable, or an options object
|
|
37
|
+
* to tune the scan interval / write-finish debounce.
|
|
38
|
+
*/
|
|
39
|
+
autoSync?: boolean | AutoSyncOptions;
|
|
40
|
+
}
|
|
41
|
+
export interface LaunchOnMountResult {
|
|
42
|
+
exitCode: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a mount of `projectDir` at `mountDir`, spawn `cli` with `args` using
|
|
46
|
+
* the mount as its cwd, forward SIGINT/SIGTERM to the child, sync writable
|
|
47
|
+
* changes back on exit, then clean up the mount. Resolves with the child's
|
|
48
|
+
* exit code.
|
|
49
|
+
*/
|
|
50
|
+
export declare function launchOnMount(opts: LaunchOnMountOptions): Promise<LaunchOnMountResult>;
|