@alfe.ai/openclaw-sync 0.0.16 → 0.0.18
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/dist/cli/index.cjs +45 -63
- package/dist/cli/index.js +44 -62
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +4 -10
- package/dist/index.d.cts +955 -202
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +955 -202
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/plugin.d.cts +16 -18
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.ts +16 -18
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin2.cjs +118 -109
- package/dist/plugin2.js +117 -108
- package/dist/plugin2.js.map +1 -1
- package/dist/sync-engine.cjs +248 -372
- package/dist/sync-engine.js +207 -348
- package/dist/sync-engine.js.map +1 -1
- package/package.json +2 -1
- package/dist/ignore.cjs +0 -120
- package/dist/ignore.js +0 -74
- package/dist/ignore.js.map +0 -1
package/dist/sync-engine.cjs
CHANGED
|
@@ -1,112 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* sync plugin works on any agent that's already logged in — no separate
|
|
15
|
-
* `.alfesync/config.json` or manual `alfesync init` required.
|
|
16
|
-
*
|
|
17
|
-
* The only field not directly in @alfe.ai/config is `agentId`, which is
|
|
18
|
-
* resolved once at startup by calling POST /auth/validate (same pattern
|
|
19
|
-
* the gateway daemon uses to bootstrap its own identity).
|
|
20
|
-
*/
|
|
21
|
-
const SYNC_STATE_DIR = (0, node_path.join)((0, node_path.join)((0, node_os.homedir)(), ".alfe"), "sync");
|
|
22
|
-
/**
|
|
23
|
-
* In-memory cache — agentId is stable for process lifetime.
|
|
24
|
-
*
|
|
25
|
-
* We cache the *Promise* (not just the resolved value) so that concurrent
|
|
26
|
-
* callers during plugin activation share a single `/auth/validate` round-trip
|
|
27
|
-
* instead of each firing their own.
|
|
28
|
-
*/
|
|
29
|
-
let cachedSyncConfigPromise = null;
|
|
30
|
-
/**
|
|
31
|
-
* Local on-disk state directory (manifest, etc.) — separate from credentials.
|
|
32
|
-
* Lives under ~/.alfe/sync/ so sync state stays alongside the rest of Alfe's
|
|
33
|
-
* agent state, not scattered across the workspace.
|
|
34
|
-
*/
|
|
35
|
-
function syncStateDir() {
|
|
36
|
-
return SYNC_STATE_DIR;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* True if the agent has logged in (~/.alfe/config.toml exists).
|
|
40
|
-
* Sync needs no separate initialization — login is enough.
|
|
41
|
-
*/
|
|
42
|
-
function isInitialized() {
|
|
43
|
-
return (0, _alfe_ai_config.configExists)();
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Invalidate the in-memory config cache. Call after login changes.
|
|
47
|
-
*/
|
|
48
|
-
function invalidateSyncConfigCache() {
|
|
49
|
-
cachedSyncConfigPromise = null;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Resolve the full sync config — apiKey + apiUrl from @alfe.ai/config,
|
|
53
|
-
* agentId/orgId fetched once from /auth/validate.
|
|
54
|
-
*
|
|
55
|
-
* Returns null if the agent isn't logged in or validation fails.
|
|
56
|
-
* Callers that require config should use requireConfig().
|
|
57
|
-
*
|
|
58
|
-
* Concurrent callers share a single in-flight promise; failures are
|
|
59
|
-
* not cached so a transient API blip doesn't poison the process.
|
|
60
|
-
*/
|
|
61
|
-
async function resolveSyncConfig() {
|
|
62
|
-
if (cachedSyncConfigPromise) return cachedSyncConfigPromise;
|
|
63
|
-
const promise = doResolveSyncConfig();
|
|
64
|
-
cachedSyncConfigPromise = promise;
|
|
65
|
-
const result = await promise;
|
|
66
|
-
if (result === null) cachedSyncConfigPromise = null;
|
|
67
|
-
return result;
|
|
68
|
-
}
|
|
69
|
-
async function doResolveSyncConfig() {
|
|
70
|
-
if (!(0, _alfe_ai_config.configExists)()) return null;
|
|
71
|
-
let alfe;
|
|
72
|
-
try {
|
|
73
|
-
alfe = (0, _alfe_ai_config.resolveConfig)();
|
|
74
|
-
} catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
const validated = await validateToken(alfe.apiUrl, alfe.apiKey);
|
|
78
|
-
if (!validated?.agentId || !validated.tenantId) return null;
|
|
79
|
-
return {
|
|
80
|
-
agentId: validated.agentId,
|
|
81
|
-
orgId: validated.tenantId,
|
|
82
|
-
token: alfe.apiKey,
|
|
83
|
-
workspacePath: alfe.workspacePath,
|
|
84
|
-
apiUrl: alfe.apiUrl
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Resolve config or throw with a clear message. Use in CLI entry points.
|
|
89
|
-
*/
|
|
90
|
-
async function requireConfig() {
|
|
91
|
-
if (!(0, _alfe_ai_config.configExists)()) throw new Error("Alfe not configured — run `alfe login` first.");
|
|
92
|
-
const config = await resolveSyncConfig();
|
|
93
|
-
if (!config) throw new Error("Failed to resolve sync config — token may be invalid or the API is unreachable.");
|
|
94
|
-
return config;
|
|
95
|
-
}
|
|
96
|
-
async function validateToken(apiUrl, token) {
|
|
97
|
-
try {
|
|
98
|
-
const res = await fetch(`${apiUrl}/auth/validate`, {
|
|
99
|
-
method: "POST",
|
|
100
|
-
headers: { "Content-Type": "application/json" },
|
|
101
|
-
body: JSON.stringify({ token })
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
102
14
|
});
|
|
103
|
-
if (!res.ok) return null;
|
|
104
|
-
return await res.json();
|
|
105
|
-
} catch {
|
|
106
|
-
return null;
|
|
107
15
|
}
|
|
108
|
-
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
109
22
|
//#endregion
|
|
23
|
+
let node_fs_promises = require("node:fs/promises");
|
|
24
|
+
let node_fs = require("node:fs");
|
|
25
|
+
let node_crypto = require("node:crypto");
|
|
26
|
+
let node_path = require("node:path");
|
|
27
|
+
let node_os = require("node:os");
|
|
28
|
+
let micromatch = require("micromatch");
|
|
29
|
+
micromatch = __toESM(micromatch);
|
|
30
|
+
let _auriclabs_logger = require("@auriclabs/logger");
|
|
110
31
|
//#region src/manifest.ts
|
|
111
32
|
/**
|
|
112
33
|
* AlfeSync manifest — local file manifest at `~/.alfe/sync/manifest.json`.
|
|
@@ -119,13 +40,19 @@ async function validateToken(apiUrl, token) {
|
|
|
119
40
|
* rest of the package, but the manifest itself is workspace-independent
|
|
120
41
|
* (one agent, one workspace, one manifest).
|
|
121
42
|
*/
|
|
43
|
+
/**
|
|
44
|
+
* Local state directory for sync — manifest, etc. Lives under `~/.alfe/sync/`
|
|
45
|
+
* so it stays alongside the rest of Alfe's agent state instead of polluting
|
|
46
|
+
* the workspace.
|
|
47
|
+
*/
|
|
48
|
+
const SYNC_STATE_DIR = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "sync");
|
|
122
49
|
const MANIFEST_FILE = "manifest.json";
|
|
123
50
|
/**
|
|
124
51
|
* Resolve the manifest file path. Lives under `~/.alfe/sync/`, independent
|
|
125
52
|
* of the workspace path — one agent has one manifest.
|
|
126
53
|
*/
|
|
127
54
|
function manifestPath() {
|
|
128
|
-
return (0, node_path.join)(
|
|
55
|
+
return (0, node_path.join)(SYNC_STATE_DIR, MANIFEST_FILE);
|
|
129
56
|
}
|
|
130
57
|
/**
|
|
131
58
|
* Read the local manifest. Returns empty manifest if not found.
|
|
@@ -150,7 +77,7 @@ async function readManifest(workspacePath) {
|
|
|
150
77
|
*/
|
|
151
78
|
async function writeManifest(workspacePath, manifest) {
|
|
152
79
|
const path = manifestPath();
|
|
153
|
-
await (0, node_fs_promises.mkdir)(
|
|
80
|
+
await (0, node_fs_promises.mkdir)(SYNC_STATE_DIR, { recursive: true });
|
|
154
81
|
await (0, node_fs_promises.writeFile)(path, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
|
|
155
82
|
}
|
|
156
83
|
/**
|
|
@@ -216,118 +143,100 @@ function diffManifests(local, remote) {
|
|
|
216
143
|
};
|
|
217
144
|
}
|
|
218
145
|
//#endregion
|
|
219
|
-
//#region src/
|
|
146
|
+
//#region src/ignore.ts
|
|
220
147
|
/**
|
|
221
|
-
*
|
|
148
|
+
* AlfeSync ignore — parse `.alfesyncignore` (gitignore-style glob matching).
|
|
149
|
+
*
|
|
150
|
+
* Uses micromatch for glob pattern matching, compatible with .gitignore syntax.
|
|
222
151
|
*/
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
throw new Error(`AlfeSync API error (${String(response.status)}): ${errorMsg}`);
|
|
152
|
+
/** Default ignore patterns — always applied */
|
|
153
|
+
const DEFAULT_IGNORES = [
|
|
154
|
+
".alfesync/**",
|
|
155
|
+
"node_modules/**",
|
|
156
|
+
"*.tmp",
|
|
157
|
+
".DS_Store",
|
|
158
|
+
".git/**",
|
|
159
|
+
".sst/**",
|
|
160
|
+
".build/**",
|
|
161
|
+
"dist/**"
|
|
162
|
+
];
|
|
163
|
+
/**
|
|
164
|
+
* Load ignore patterns from `.alfesyncignore` file + defaults.
|
|
165
|
+
*/
|
|
166
|
+
async function loadIgnorePatterns(workspacePath) {
|
|
167
|
+
const ignoreFile = (0, node_path.join)(workspacePath, ".alfesyncignore");
|
|
168
|
+
const patterns = [...DEFAULT_IGNORES];
|
|
169
|
+
if ((0, node_fs.existsSync)(ignoreFile)) try {
|
|
170
|
+
const lines = (await (0, node_fs_promises.readFile)(ignoreFile, "utf-8")).split("\n");
|
|
171
|
+
for (const line of lines) {
|
|
172
|
+
const trimmed = line.trim();
|
|
173
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
174
|
+
patterns.push(trimmed);
|
|
247
175
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return request("GET", `/sync/agents/${agentId}/stats`);
|
|
291
|
-
},
|
|
292
|
-
async registerAgent(displayName) {
|
|
293
|
-
return request("POST", "/sync/agents", {
|
|
294
|
-
agentId,
|
|
295
|
-
displayName
|
|
296
|
-
});
|
|
297
|
-
},
|
|
298
|
-
async getFileHistory(filePath) {
|
|
299
|
-
return (await request("GET", `/sync/agents/${agentId}/files/${filePath}/versions`)).versions;
|
|
300
|
-
},
|
|
301
|
-
async reconstruct(mode = "full") {
|
|
302
|
-
return request("POST", `/sync/agents/${agentId}/reconstruct`, { mode });
|
|
176
|
+
} catch {}
|
|
177
|
+
return patterns;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Check if a relative path should be ignored.
|
|
181
|
+
*/
|
|
182
|
+
function shouldIgnore(relativePath, patterns) {
|
|
183
|
+
return micromatch.default.isMatch(relativePath, patterns, {
|
|
184
|
+
dot: true,
|
|
185
|
+
matchBase: true
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Filter a list of relative paths, removing ignored ones.
|
|
190
|
+
*/
|
|
191
|
+
function filterIgnored(paths, patterns) {
|
|
192
|
+
return paths.filter((p) => !shouldIgnore(p, patterns));
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/retry.ts
|
|
196
|
+
/**
|
|
197
|
+
* Tiny retry helper used by uploader/downloader. Extracted so both
|
|
198
|
+
* use the same timing and surface the same final error shape.
|
|
199
|
+
*/
|
|
200
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
201
|
+
const DEFAULT_BASE_DELAY_MS = 1e3;
|
|
202
|
+
/**
|
|
203
|
+
* Run `fn` up to `maxRetries + 1` times with exponential backoff
|
|
204
|
+
* (`base * 2^attempt`). Returns the first successful value, or rethrows
|
|
205
|
+
* the last error.
|
|
206
|
+
*/
|
|
207
|
+
async function withRetry(fn, options = {}) {
|
|
208
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
209
|
+
const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
210
|
+
let lastError;
|
|
211
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) try {
|
|
212
|
+
return await fn();
|
|
213
|
+
} catch (err) {
|
|
214
|
+
lastError = err;
|
|
215
|
+
if (attempt < maxRetries) {
|
|
216
|
+
const delay = baseDelayMs * Math.pow(2, attempt);
|
|
217
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
303
218
|
}
|
|
304
|
-
}
|
|
219
|
+
}
|
|
220
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
305
221
|
}
|
|
306
222
|
//#endregion
|
|
307
223
|
//#region src/uploader.ts
|
|
308
224
|
/**
|
|
309
|
-
* AlfeSync uploader —
|
|
225
|
+
* AlfeSync uploader — push changed files to S3.
|
|
310
226
|
*
|
|
311
|
-
*
|
|
312
|
-
* 1.
|
|
313
|
-
* 2. PUT
|
|
314
|
-
* 3.
|
|
315
|
-
* 4. Update local manifest
|
|
227
|
+
* Per file:
|
|
228
|
+
* 1. Ask the agent API for a presigned PUT URL
|
|
229
|
+
* 2. PUT bytes directly to S3 (raw fetch — S3 isn't an Alfe API)
|
|
230
|
+
* 3. Tell the agent API the upload completed (`syncConfirmUpload`)
|
|
231
|
+
* 4. Update the local manifest
|
|
316
232
|
*
|
|
317
|
-
*
|
|
233
|
+
* Each step retries with exponential backoff via `withRetry`.
|
|
318
234
|
*/
|
|
319
235
|
const log$2 = (0, _auriclabs_logger.createLogger)("SyncUploader");
|
|
320
|
-
const
|
|
321
|
-
const BASE_DELAY_MS$1 = 1e3;
|
|
322
|
-
/**
|
|
323
|
-
* Determine the storage class based on file path.
|
|
324
|
-
*/
|
|
236
|
+
const ARCHIVE_PREFIXES = ["sessions/archive/", "context/archive/"];
|
|
325
237
|
function getStorageClass(relativePath) {
|
|
326
|
-
return
|
|
238
|
+
return ARCHIVE_PREFIXES.some((p) => relativePath.startsWith(p)) ? "GLACIER_IR" : "STANDARD";
|
|
327
239
|
}
|
|
328
|
-
/**
|
|
329
|
-
* Determine MIME type from file path.
|
|
330
|
-
*/
|
|
331
240
|
function getContentType(relativePath) {
|
|
332
241
|
if (relativePath.endsWith(".json")) return "application/json";
|
|
333
242
|
if (relativePath.endsWith(".md")) return "text/markdown";
|
|
@@ -336,62 +245,63 @@ function getContentType(relativePath) {
|
|
|
336
245
|
if (relativePath.endsWith(".yaml") || relativePath.endsWith(".yml")) return "text/yaml";
|
|
337
246
|
return "application/octet-stream";
|
|
338
247
|
}
|
|
339
|
-
|
|
340
|
-
* Upload a single file with retry logic.
|
|
341
|
-
*/
|
|
342
|
-
async function uploadFileWithRetry(workspacePath, relativePath, client) {
|
|
248
|
+
async function uploadOne(workspacePath, relativePath, client) {
|
|
343
249
|
const absolutePath = (0, node_path.join)(workspacePath, relativePath);
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
250
|
+
try {
|
|
251
|
+
return await withRetry(async () => {
|
|
252
|
+
const [hash, fileStat] = await Promise.all([computeFileHash(absolutePath), (0, node_fs_promises.stat)(absolutePath)]);
|
|
253
|
+
const size = fileStat.size;
|
|
254
|
+
const storageClass = getStorageClass(relativePath);
|
|
255
|
+
const contentType = getContentType(relativePath);
|
|
256
|
+
const url = (await client.syncPresign({ files: [{
|
|
257
|
+
path: relativePath,
|
|
258
|
+
operation: "put",
|
|
259
|
+
contentType
|
|
260
|
+
}] })).urls[0]?.url;
|
|
261
|
+
if (!url) throw new Error("No presigned URL returned");
|
|
262
|
+
const fileContent = await (0, node_fs_promises.readFile)(absolutePath);
|
|
263
|
+
const putResponse = await fetch(url, {
|
|
264
|
+
method: "PUT",
|
|
265
|
+
headers: { "Content-Type": contentType },
|
|
266
|
+
body: fileContent
|
|
267
|
+
});
|
|
268
|
+
if (!putResponse.ok) throw new Error(`S3 PUT failed (${String(putResponse.status)}): ${await putResponse.text()}`);
|
|
269
|
+
await client.syncConfirmUpload({
|
|
270
|
+
filePath: relativePath,
|
|
271
|
+
hash,
|
|
272
|
+
size,
|
|
273
|
+
storageClass
|
|
274
|
+
});
|
|
275
|
+
await updateManifestEntry(workspacePath, relativePath, {
|
|
276
|
+
hash,
|
|
277
|
+
size,
|
|
278
|
+
lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
|
|
279
|
+
storageClass
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
path: relativePath,
|
|
283
|
+
success: true,
|
|
284
|
+
hash,
|
|
285
|
+
size
|
|
286
|
+
};
|
|
364
287
|
});
|
|
288
|
+
} catch (err) {
|
|
365
289
|
return {
|
|
366
290
|
path: relativePath,
|
|
367
|
-
success:
|
|
368
|
-
|
|
369
|
-
size
|
|
291
|
+
success: false,
|
|
292
|
+
error: err instanceof Error ? err.message : String(err)
|
|
370
293
|
};
|
|
371
|
-
} catch (err) {
|
|
372
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
373
|
-
if (attempt < MAX_RETRIES$1) {
|
|
374
|
-
const delay = BASE_DELAY_MS$1 * Math.pow(2, attempt);
|
|
375
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
376
|
-
}
|
|
377
294
|
}
|
|
378
|
-
return {
|
|
379
|
-
path: relativePath,
|
|
380
|
-
success: false,
|
|
381
|
-
error: lastError?.message ?? "Unknown error"
|
|
382
|
-
};
|
|
383
295
|
}
|
|
384
296
|
/**
|
|
385
|
-
* Upload
|
|
386
|
-
*
|
|
387
|
-
* Uploads are performed in parallel with a concurrency limit.
|
|
297
|
+
* Upload many files, batched by `concurrency`.
|
|
388
298
|
*/
|
|
389
299
|
async function uploadFiles(workspacePath, relativePaths, client, options = {}) {
|
|
390
300
|
const { concurrency = 5, quiet = false } = options;
|
|
391
301
|
const results = [];
|
|
392
302
|
for (let i = 0; i < relativePaths.length; i += concurrency) {
|
|
393
303
|
const batch = relativePaths.slice(i, i + concurrency);
|
|
394
|
-
const batchResults = await Promise.all(batch.map((path) =>
|
|
304
|
+
const batchResults = await Promise.all(batch.map((path) => uploadOne(workspacePath, path, client)));
|
|
395
305
|
for (const result of batchResults) {
|
|
396
306
|
results.push(result);
|
|
397
307
|
if (!quiet) if (result.success) log$2.info(`Uploaded ${result.path} (${formatBytes$1(result.size ?? 0)})`);
|
|
@@ -408,65 +318,55 @@ function formatBytes$1(bytes) {
|
|
|
408
318
|
//#endregion
|
|
409
319
|
//#region src/downloader.ts
|
|
410
320
|
/**
|
|
411
|
-
* AlfeSync downloader —
|
|
321
|
+
* AlfeSync downloader — pull files from S3 to disk.
|
|
412
322
|
*
|
|
413
|
-
*
|
|
414
|
-
* 1.
|
|
415
|
-
* 2. GET
|
|
416
|
-
* 3. Write to
|
|
417
|
-
* 4. Update local manifest
|
|
323
|
+
* Per file:
|
|
324
|
+
* 1. Ask the agent API for a presigned GET URL
|
|
325
|
+
* 2. GET bytes directly from S3 (raw fetch — S3 isn't an Alfe API)
|
|
326
|
+
* 3. Write to disk
|
|
327
|
+
* 4. Update the local manifest
|
|
418
328
|
*/
|
|
419
329
|
const log$1 = (0, _auriclabs_logger.createLogger)("SyncDownloader");
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
330
|
+
async function downloadOne(workspacePath, relativePath, client, remoteEntry) {
|
|
331
|
+
try {
|
|
332
|
+
return await withRetry(async () => {
|
|
333
|
+
const url = (await client.syncPresign({ files: [{
|
|
334
|
+
path: relativePath,
|
|
335
|
+
operation: "get"
|
|
336
|
+
}] })).urls[0]?.url;
|
|
337
|
+
if (!url) throw new Error("No presigned URL returned");
|
|
338
|
+
const response = await fetch(url);
|
|
339
|
+
if (!response.ok) throw new Error(`S3 GET failed (${String(response.status)}): ${await response.text()}`);
|
|
340
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
341
|
+
const absolutePath = (0, node_path.join)(workspacePath, relativePath);
|
|
342
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(absolutePath), { recursive: true });
|
|
343
|
+
await (0, node_fs_promises.writeFile)(absolutePath, buffer);
|
|
344
|
+
await updateManifestEntry(workspacePath, relativePath, {
|
|
345
|
+
hash: remoteEntry?.hash ?? "",
|
|
346
|
+
size: buffer.length,
|
|
347
|
+
lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
|
|
348
|
+
storageClass: remoteEntry?.storageClass ?? "STANDARD"
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
path: relativePath,
|
|
352
|
+
success: true,
|
|
353
|
+
size: buffer.length
|
|
354
|
+
};
|
|
440
355
|
});
|
|
356
|
+
} catch (err) {
|
|
441
357
|
return {
|
|
442
358
|
path: relativePath,
|
|
443
|
-
success:
|
|
444
|
-
|
|
359
|
+
success: false,
|
|
360
|
+
error: err instanceof Error ? err.message : String(err)
|
|
445
361
|
};
|
|
446
|
-
} catch (err) {
|
|
447
|
-
lastError = err instanceof Error ? err : new Error(String(err));
|
|
448
|
-
if (attempt < MAX_RETRIES) {
|
|
449
|
-
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
450
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
451
|
-
}
|
|
452
362
|
}
|
|
453
|
-
return {
|
|
454
|
-
path: relativePath,
|
|
455
|
-
success: false,
|
|
456
|
-
error: lastError?.message ?? "Unknown error"
|
|
457
|
-
};
|
|
458
363
|
}
|
|
459
|
-
/**
|
|
460
|
-
* Download multiple files from S3.
|
|
461
|
-
*
|
|
462
|
-
* Downloads are performed in parallel with a concurrency limit.
|
|
463
|
-
*/
|
|
464
364
|
async function downloadFiles(workspacePath, relativePaths, client, remoteManifest, options = {}) {
|
|
465
365
|
const { concurrency = 5, quiet = false } = options;
|
|
466
366
|
const results = [];
|
|
467
367
|
for (let i = 0; i < relativePaths.length; i += concurrency) {
|
|
468
368
|
const batch = relativePaths.slice(i, i + concurrency);
|
|
469
|
-
const batchResults = await Promise.all(batch.map((path) =>
|
|
369
|
+
const batchResults = await Promise.all(batch.map((path) => downloadOne(workspacePath, path, client, remoteManifest?.files[path])));
|
|
470
370
|
for (const result of batchResults) {
|
|
471
371
|
results.push(result);
|
|
472
372
|
if (!quiet) if (result.success) log$1.info(`Downloaded ${result.path} (${formatBytes(result.size ?? 0)})`);
|
|
@@ -483,38 +383,31 @@ function formatBytes(bytes) {
|
|
|
483
383
|
//#endregion
|
|
484
384
|
//#region src/sync-engine.ts
|
|
485
385
|
/**
|
|
486
|
-
* AlfeSync engine — orchestrates push, pull, and full sync
|
|
386
|
+
* AlfeSync engine — orchestrates push, pull, and full sync.
|
|
487
387
|
*
|
|
488
|
-
* Handles:
|
|
489
388
|
* - push(paths[]): upload changed files to S3
|
|
490
389
|
* - pull(): download files newer on remote
|
|
491
390
|
* - fullSync(): bidirectional sync with conflict detection
|
|
492
391
|
*
|
|
493
|
-
* Conflict resolution:
|
|
494
|
-
* AND local file has changed
|
|
392
|
+
* Conflict resolution: when a remote file is newer than the local manifest
|
|
393
|
+
* entry AND the local file has also changed, the local copy is renamed to
|
|
394
|
+
* `<name>.conflict-<timestamp>.<ext>` and the remote version wins.
|
|
495
395
|
*/
|
|
496
396
|
const log = (0, _auriclabs_logger.createLogger)("SyncEngine");
|
|
497
397
|
/**
|
|
498
|
-
*
|
|
499
|
-
*
|
|
398
|
+
* Construct a sync engine bound to a workspace + agent API client.
|
|
399
|
+
*
|
|
400
|
+
* The client is constructed once in plugin.ts (or the CLI) and passed in,
|
|
401
|
+
* so credentials never leak into multiple places.
|
|
500
402
|
*/
|
|
501
|
-
|
|
502
|
-
const config = await requireConfig();
|
|
503
|
-
const workspacePath = workspacePathOverride ?? config.workspacePath;
|
|
504
|
-
const client = createApiClient({
|
|
505
|
-
apiUrl: config.apiUrl,
|
|
506
|
-
token: config.token,
|
|
507
|
-
agentId: config.agentId
|
|
508
|
-
});
|
|
403
|
+
function createSyncEngine({ workspacePath, client }) {
|
|
509
404
|
return {
|
|
510
|
-
|
|
405
|
+
workspacePath,
|
|
511
406
|
client,
|
|
512
407
|
async push(paths, options = {}) {
|
|
513
408
|
const { quiet = false, filter } = options;
|
|
514
|
-
const ignorePatterns = await
|
|
515
|
-
let filesToPush;
|
|
516
|
-
if (paths && paths.length > 0) filesToPush = require_ignore.filterIgnored(paths, ignorePatterns);
|
|
517
|
-
else filesToPush = await detectLocalChanges(workspacePath, ignorePatterns);
|
|
409
|
+
const ignorePatterns = await loadIgnorePatterns(workspacePath);
|
|
410
|
+
let filesToPush = paths && paths.length > 0 ? filterIgnored(paths, ignorePatterns) : await detectLocalChanges(workspacePath, ignorePatterns);
|
|
518
411
|
if (filter) filesToPush = filesToPush.filter((p) => p.startsWith(filter));
|
|
519
412
|
if (filesToPush.length === 0) {
|
|
520
413
|
if (!quiet) log.info("Nothing to push");
|
|
@@ -539,10 +432,9 @@ async function createSyncEngine(workspacePathOverride) {
|
|
|
539
432
|
},
|
|
540
433
|
async pull(options = {}) {
|
|
541
434
|
const { quiet = false } = options;
|
|
542
|
-
const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.
|
|
435
|
+
const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.syncGetManifest()]);
|
|
543
436
|
const diff = diffManifests(localManifest, remoteManifest);
|
|
544
|
-
|
|
545
|
-
if (filesToPull.length === 0) {
|
|
437
|
+
if (diff.toPull.length === 0) {
|
|
546
438
|
if (!quiet) log.info("Nothing to pull");
|
|
547
439
|
return {
|
|
548
440
|
pushed: 0,
|
|
@@ -551,8 +443,8 @@ async function createSyncEngine(workspacePathOverride) {
|
|
|
551
443
|
errors: 0
|
|
552
444
|
};
|
|
553
445
|
}
|
|
554
|
-
if (!quiet) log.info(`Pulling ${String(
|
|
555
|
-
const results = await downloadFiles(workspacePath,
|
|
446
|
+
if (!quiet) log.info(`Pulling ${String(diff.toPull.length)} file(s)`);
|
|
447
|
+
const results = await downloadFiles(workspacePath, [...diff.toPull], client, remoteManifest, { quiet });
|
|
556
448
|
const pulled = results.filter((r) => r.success).length;
|
|
557
449
|
const errors = results.filter((r) => !r.success).length;
|
|
558
450
|
if (!quiet) log.info(`Pull complete: ${String(pulled)} downloaded, ${String(errors)} failed`);
|
|
@@ -565,8 +457,8 @@ async function createSyncEngine(workspacePathOverride) {
|
|
|
565
457
|
},
|
|
566
458
|
async fullSync(options = {}) {
|
|
567
459
|
const { quiet = false } = options;
|
|
568
|
-
const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.
|
|
569
|
-
const ignorePatterns = await
|
|
460
|
+
const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.syncGetManifest()]);
|
|
461
|
+
const ignorePatterns = await loadIgnorePatterns(workspacePath);
|
|
570
462
|
const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);
|
|
571
463
|
const diff = diffManifests(localManifest, remoteManifest);
|
|
572
464
|
const trueConflicts = diff.conflicts.filter((p) => localChanges.includes(p));
|
|
@@ -574,17 +466,16 @@ async function createSyncEngine(workspacePathOverride) {
|
|
|
574
466
|
let conflictCount = 0;
|
|
575
467
|
for (const conflictPath of trueConflicts) {
|
|
576
468
|
const absolutePath = (0, node_path.join)(workspacePath, conflictPath);
|
|
577
|
-
if ((0, node_fs.existsSync)(absolutePath))
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
}
|
|
469
|
+
if (!(0, node_fs.existsSync)(absolutePath)) continue;
|
|
470
|
+
const ext = (0, node_path.extname)(conflictPath);
|
|
471
|
+
const base = (0, node_path.basename)(conflictPath, ext);
|
|
472
|
+
const dir = (0, node_path.dirname)(conflictPath);
|
|
473
|
+
const conflictName = `${base}.conflict-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}${ext}`;
|
|
474
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(workspacePath, dir, conflictName), await (0, node_fs_promises.readFile)(absolutePath));
|
|
475
|
+
if (!quiet) log.warn(`Conflict: ${conflictPath} — saved as ${conflictName}`);
|
|
476
|
+
conflictCount++;
|
|
586
477
|
}
|
|
587
|
-
const filesToPush =
|
|
478
|
+
const filesToPush = filterIgnored([...diff.toPush, ...localChanges.filter((p) => !diff.conflicts.includes(p))], ignorePatterns);
|
|
588
479
|
const filesToPull = [
|
|
589
480
|
...diff.toPull,
|
|
590
481
|
...remoteOnlyChanges,
|
|
@@ -627,13 +518,12 @@ async function createSyncEngine(workspacePathOverride) {
|
|
|
627
518
|
conflicts: 0,
|
|
628
519
|
errors: 0
|
|
629
520
|
};
|
|
630
|
-
const remoteManifest = await client.
|
|
521
|
+
const remoteManifest = await client.syncGetManifest();
|
|
631
522
|
const localManifest = await readManifest(workspacePath);
|
|
632
523
|
const filesToPull = paths.filter((p) => {
|
|
633
524
|
if (!(p in remoteManifest.files)) return false;
|
|
634
|
-
const remoteEntry = remoteManifest.files[p];
|
|
635
525
|
if (!(p in localManifest.files)) return true;
|
|
636
|
-
return localManifest.files[p].hash !==
|
|
526
|
+
return localManifest.files[p].hash !== remoteManifest.files[p].hash;
|
|
637
527
|
});
|
|
638
528
|
if (filesToPull.length === 0) {
|
|
639
529
|
if (!quiet) log.info("All notified files already in sync");
|
|
@@ -673,10 +563,8 @@ async function createSyncEngine(workspacePathOverride) {
|
|
|
673
563
|
};
|
|
674
564
|
}
|
|
675
565
|
/**
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
* Compares current file hashes against the local manifest.
|
|
679
|
-
* Returns list of relative paths that need pushing.
|
|
566
|
+
* Walk the workspace and return paths whose hash differs from the manifest
|
|
567
|
+
* (or that are missing from it entirely).
|
|
680
568
|
*/
|
|
681
569
|
async function detectLocalChanges(workspacePath, ignorePatterns) {
|
|
682
570
|
const manifest = await readManifest(workspacePath);
|
|
@@ -689,43 +577,37 @@ async function detectLocalChanges(workspacePath, ignorePatterns) {
|
|
|
689
577
|
const relativePath = fullPath.slice(workspacePath.length + 1).replace(/\\/g, "/");
|
|
690
578
|
if (shouldSkipDir(entry.name)) continue;
|
|
691
579
|
if (entry.isDirectory()) {
|
|
692
|
-
if (!ignorePatterns.some((p) =>
|
|
693
|
-
if (p.endsWith("/**")) return relativePath.startsWith(p.slice(0, -3));
|
|
694
|
-
return false;
|
|
695
|
-
})) await walk(fullPath);
|
|
580
|
+
if (!ignorePatterns.some((p) => p.endsWith("/**") && relativePath.startsWith(p.slice(0, -3)))) await walk(fullPath);
|
|
696
581
|
} else if (entry.isFile()) {
|
|
697
|
-
const { shouldIgnore } = await Promise.resolve().then(() => require("./ignore.cjs")).then((n) => n.ignore_exports);
|
|
698
582
|
if (shouldIgnore(relativePath, ignorePatterns)) continue;
|
|
699
|
-
if (!(relativePath in manifest.files))
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
try {
|
|
703
|
-
if (await computeFileHash(fullPath) !== manifestEntry.hash) changed.push(relativePath);
|
|
704
|
-
} catch {}
|
|
583
|
+
if (!(relativePath in manifest.files)) {
|
|
584
|
+
changed.push(relativePath);
|
|
585
|
+
continue;
|
|
705
586
|
}
|
|
587
|
+
try {
|
|
588
|
+
if (await computeFileHash(fullPath) !== manifest.files[relativePath].hash) changed.push(relativePath);
|
|
589
|
+
} catch {}
|
|
706
590
|
}
|
|
707
591
|
}
|
|
708
592
|
}
|
|
709
593
|
await walk(workspacePath);
|
|
710
594
|
return changed;
|
|
711
595
|
}
|
|
712
|
-
/**
|
|
713
|
-
* Directories to always skip during walks.
|
|
714
|
-
*/
|
|
596
|
+
/** Directories the engine never descends into. */
|
|
715
597
|
function shouldSkipDir(name) {
|
|
716
598
|
return name === "node_modules" || name === ".git" || name === ".sst" || name === ".build" || name === "dist";
|
|
717
599
|
}
|
|
718
600
|
//#endregion
|
|
719
|
-
Object.defineProperty(exports, "
|
|
601
|
+
Object.defineProperty(exports, "__toESM", {
|
|
720
602
|
enumerable: true,
|
|
721
603
|
get: function() {
|
|
722
|
-
return
|
|
604
|
+
return __toESM;
|
|
723
605
|
}
|
|
724
606
|
});
|
|
725
|
-
Object.defineProperty(exports, "
|
|
607
|
+
Object.defineProperty(exports, "computeFileHash", {
|
|
726
608
|
enumerable: true,
|
|
727
609
|
get: function() {
|
|
728
|
-
return
|
|
610
|
+
return computeFileHash;
|
|
729
611
|
}
|
|
730
612
|
});
|
|
731
613
|
Object.defineProperty(exports, "createSyncEngine", {
|
|
@@ -746,16 +628,16 @@ Object.defineProperty(exports, "downloadFiles", {
|
|
|
746
628
|
return downloadFiles;
|
|
747
629
|
}
|
|
748
630
|
});
|
|
749
|
-
Object.defineProperty(exports, "
|
|
631
|
+
Object.defineProperty(exports, "filterIgnored", {
|
|
750
632
|
enumerable: true,
|
|
751
633
|
get: function() {
|
|
752
|
-
return
|
|
634
|
+
return filterIgnored;
|
|
753
635
|
}
|
|
754
636
|
});
|
|
755
|
-
Object.defineProperty(exports, "
|
|
637
|
+
Object.defineProperty(exports, "loadIgnorePatterns", {
|
|
756
638
|
enumerable: true,
|
|
757
639
|
get: function() {
|
|
758
|
-
return
|
|
640
|
+
return loadIgnorePatterns;
|
|
759
641
|
}
|
|
760
642
|
});
|
|
761
643
|
Object.defineProperty(exports, "readManifest", {
|
|
@@ -770,22 +652,10 @@ Object.defineProperty(exports, "removeManifestEntry", {
|
|
|
770
652
|
return removeManifestEntry;
|
|
771
653
|
}
|
|
772
654
|
});
|
|
773
|
-
Object.defineProperty(exports, "
|
|
774
|
-
enumerable: true,
|
|
775
|
-
get: function() {
|
|
776
|
-
return requireConfig;
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
Object.defineProperty(exports, "resolveSyncConfig", {
|
|
780
|
-
enumerable: true,
|
|
781
|
-
get: function() {
|
|
782
|
-
return resolveSyncConfig;
|
|
783
|
-
}
|
|
784
|
-
});
|
|
785
|
-
Object.defineProperty(exports, "syncStateDir", {
|
|
655
|
+
Object.defineProperty(exports, "shouldIgnore", {
|
|
786
656
|
enumerable: true,
|
|
787
657
|
get: function() {
|
|
788
|
-
return
|
|
658
|
+
return shouldIgnore;
|
|
789
659
|
}
|
|
790
660
|
});
|
|
791
661
|
Object.defineProperty(exports, "updateManifestEntry", {
|
|
@@ -800,6 +670,12 @@ Object.defineProperty(exports, "uploadFiles", {
|
|
|
800
670
|
return uploadFiles;
|
|
801
671
|
}
|
|
802
672
|
});
|
|
673
|
+
Object.defineProperty(exports, "withRetry", {
|
|
674
|
+
enumerable: true,
|
|
675
|
+
get: function() {
|
|
676
|
+
return withRetry;
|
|
677
|
+
}
|
|
678
|
+
});
|
|
803
679
|
Object.defineProperty(exports, "writeManifest", {
|
|
804
680
|
enumerable: true,
|
|
805
681
|
get: function() {
|