@alfe.ai/openclaw-sync 0.0.15 → 0.0.17

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.
@@ -1,96 +1,68 @@
1
- const require_ignore = require("./ignore.cjs");
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
14
+ });
15
+ }
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));
22
+ //#endregion
2
23
  let node_fs_promises = require("node:fs/promises");
3
24
  let node_fs = require("node:fs");
4
- let node_path = require("node:path");
5
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);
6
30
  let _auriclabs_logger = require("@auriclabs/logger");
7
- //#region src/config.ts
8
- /**
9
- * AlfeSync configuration — read/write `.alfesync/config.json` in workspace root.
10
- */
11
- const CONFIG_DIR = ".alfesync";
12
- const CONFIG_FILE = "config.json";
13
- /**
14
- * Resolve the .alfesync directory path for a given workspace root.
15
- */
16
- function configDir(workspacePath) {
17
- return (0, node_path.join)(workspacePath, CONFIG_DIR);
18
- }
19
- /**
20
- * Resolve the config file path for a given workspace root.
21
- */
22
- function configPath(workspacePath) {
23
- return (0, node_path.join)(workspacePath, CONFIG_DIR, CONFIG_FILE);
24
- }
25
- /**
26
- * Check if a workspace has been initialized with AlfeSync.
27
- */
28
- function isInitialized(workspacePath) {
29
- return (0, node_fs.existsSync)(configPath(workspacePath));
30
- }
31
- /**
32
- * Read the AlfeSync config from a workspace.
33
- * Returns null if not initialized.
34
- */
35
- async function readConfig(workspacePath) {
36
- const path = configPath(workspacePath);
37
- if (!(0, node_fs.existsSync)(path)) return null;
38
- try {
39
- const raw = await (0, node_fs_promises.readFile)(path, "utf-8");
40
- const parsed = JSON.parse(raw);
41
- if (!parsed.agentId || !parsed.token || !parsed.apiUrl) return null;
42
- return {
43
- ...parsed,
44
- workspacePath
45
- };
46
- } catch {
47
- return null;
48
- }
49
- }
50
- /**
51
- * Write the AlfeSync config to a workspace.
52
- * Creates the .alfesync directory if it doesn't exist.
53
- */
54
- async function writeConfig(config) {
55
- await (0, node_fs_promises.mkdir)(configDir(config.workspacePath), { recursive: true });
56
- const path = configPath(config.workspacePath);
57
- const data = {
58
- agentId: config.agentId,
59
- orgId: config.orgId,
60
- token: config.token,
61
- workspacePath: config.workspacePath,
62
- apiUrl: config.apiUrl
63
- };
64
- await (0, node_fs_promises.writeFile)(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
65
- }
66
- /**
67
- * Load config from workspace, throwing if not initialized.
68
- */
69
- async function requireConfig(workspacePath) {
70
- const config = await readConfig(workspacePath);
71
- if (!config) throw new Error(`AlfeSync not initialized in ${workspacePath}. Run: alfesync init`);
72
- return config;
73
- }
74
- //#endregion
75
31
  //#region src/manifest.ts
76
32
  /**
77
- * AlfeSync manifest — local file manifest at `.alfesync/manifest.json`.
33
+ * AlfeSync manifest — local file manifest at `~/.alfe/sync/manifest.json`.
78
34
  *
79
35
  * Tracks file hashes, sizes, sync timestamps, and storage classes
80
- * to enable efficient diff-based syncing.
36
+ * to enable efficient diff-based syncing. Lives under `~/.alfe/sync/`
37
+ * so workspace directories stay clean.
38
+ *
39
+ * `workspacePath` is accepted by every function for consistency with the
40
+ * rest of the package, but the manifest itself is workspace-independent
41
+ * (one agent, one workspace, one manifest).
81
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");
82
49
  const MANIFEST_FILE = "manifest.json";
83
50
  /**
84
- * Resolve the manifest file path for a given workspace root.
51
+ * Resolve the manifest file path. Lives under `~/.alfe/sync/`, independent
52
+ * of the workspace path — one agent has one manifest.
85
53
  */
86
- function manifestPath(workspacePath) {
87
- return (0, node_path.join)(configDir(workspacePath), MANIFEST_FILE);
54
+ function manifestPath() {
55
+ return (0, node_path.join)(SYNC_STATE_DIR, MANIFEST_FILE);
88
56
  }
89
57
  /**
90
58
  * Read the local manifest. Returns empty manifest if not found.
59
+ *
60
+ * `workspacePath` is accepted for call-site symmetry with the rest of the
61
+ * package but is not used — the manifest path is resolved from
62
+ * `~/.alfe/sync/` regardless of which workspace the call comes from.
91
63
  */
92
64
  async function readManifest(workspacePath) {
93
- const path = manifestPath(workspacePath);
65
+ const path = manifestPath();
94
66
  if (!(0, node_fs.existsSync)(path)) return { files: {} };
95
67
  try {
96
68
  const raw = await (0, node_fs_promises.readFile)(path, "utf-8");
@@ -100,11 +72,13 @@ async function readManifest(workspacePath) {
100
72
  }
101
73
  }
102
74
  /**
103
- * Write the local manifest.
75
+ * Write the local manifest. `workspacePath` is accepted for call-site
76
+ * symmetry but unused (see `readManifest`).
104
77
  */
105
78
  async function writeManifest(workspacePath, manifest) {
106
- await (0, node_fs_promises.mkdir)(configDir(workspacePath), { recursive: true });
107
- await (0, node_fs_promises.writeFile)(manifestPath(workspacePath), JSON.stringify(manifest, null, 2) + "\n", "utf-8");
79
+ const path = manifestPath();
80
+ await (0, node_fs_promises.mkdir)(SYNC_STATE_DIR, { recursive: true });
81
+ await (0, node_fs_promises.writeFile)(path, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
108
82
  }
109
83
  /**
110
84
  * Update a single file entry in the local manifest.
@@ -169,118 +143,100 @@ function diffManifests(local, remote) {
169
143
  };
170
144
  }
171
145
  //#endregion
172
- //#region src/api-client.ts
146
+ //#region src/ignore.ts
147
+ /**
148
+ * AlfeSync ignore — parse `.alfesyncignore` (gitignore-style glob matching).
149
+ *
150
+ * Uses micromatch for glob pattern matching, compatible with .gitignore syntax.
151
+ */
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
+ ];
173
163
  /**
174
- * Create an AlfeSync API client.
164
+ * Load ignore patterns from `.alfesyncignore` file + defaults.
175
165
  */
176
- function createApiClient(config) {
177
- const { apiUrl, token, agentId } = config;
178
- const baseUrl = apiUrl.replace(/\/$/, "");
179
- async function request(method, path, body) {
180
- const url = `${baseUrl}${path}`;
181
- const headers = {
182
- Authorization: `Bearer ${token}`,
183
- "Content-Type": "application/json"
184
- };
185
- const response = await fetch(url, {
186
- method,
187
- headers,
188
- body: body ? JSON.stringify(body) : void 0
189
- });
190
- if (!response.ok) {
191
- const text = await response.text();
192
- let errorMsg;
193
- try {
194
- const parsed = JSON.parse(text);
195
- errorMsg = (typeof parsed.error === "string" ? parsed.error : void 0) ?? (typeof parsed.message === "string" ? parsed.message : void 0) ?? text;
196
- } catch {
197
- errorMsg = text;
198
- }
199
- throw new Error(`AlfeSync API error (${String(response.status)}): ${errorMsg}`);
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);
200
175
  }
201
- const json = await response.json();
202
- if (!json.success) throw new Error("AlfeSync API returned unsuccessful response");
203
- return json.data;
204
- }
205
- return {
206
- async getManifest() {
207
- return request("GET", `/sync/agents/${agentId}/manifest`);
208
- },
209
- async presignPut(filePath, contentType = "application/octet-stream") {
210
- return (await request("POST", `/sync/agents/${agentId}/presign`, { files: [{
211
- path: filePath,
212
- operation: "put",
213
- contentType
214
- }] })).urls[0];
215
- },
216
- async presignPutBatch(files) {
217
- return (await request("POST", `/sync/agents/${agentId}/presign`, { files: files.map((f) => ({
218
- path: f.path,
219
- operation: "put",
220
- contentType: f.contentType ?? "application/octet-stream"
221
- })) })).urls;
222
- },
223
- async confirmUpload(filePath, hash, size, storageClass = "STANDARD") {
224
- return request("POST", `/sync/agents/${agentId}/files/${filePath}/confirm`, {
225
- hash,
226
- size,
227
- storageClass
228
- });
229
- },
230
- async presignGet(filePath) {
231
- return (await request("POST", `/sync/agents/${agentId}/presign`, { files: [{
232
- path: filePath,
233
- operation: "get"
234
- }] })).urls[0];
235
- },
236
- async presignGetBatch(paths) {
237
- return (await request("POST", `/sync/agents/${agentId}/presign`, { files: paths.map((p) => ({
238
- path: p,
239
- operation: "get"
240
- })) })).urls;
241
- },
242
- async getStats() {
243
- return request("GET", `/sync/agents/${agentId}/stats`);
244
- },
245
- async registerAgent(displayName) {
246
- return request("POST", "/sync/agents", {
247
- agentId,
248
- displayName
249
- });
250
- },
251
- async getFileHistory(filePath) {
252
- return (await request("GET", `/sync/agents/${agentId}/files/${filePath}/versions`)).versions;
253
- },
254
- async reconstruct(mode = "full") {
255
- 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));
256
218
  }
257
- };
219
+ }
220
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
258
221
  }
259
222
  //#endregion
260
223
  //#region src/uploader.ts
261
224
  /**
262
- * AlfeSync uploader — upload changed files to S3 via presigned URLs.
225
+ * AlfeSync uploader — push changed files to S3.
263
226
  *
264
- * Flow per file:
265
- * 1. Request presigned PUT URL from API
266
- * 2. PUT file content directly to S3
267
- * 3. Notify API of completion (confirm endpoint)
268
- * 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
269
232
  *
270
- * Retries 3x with exponential backoff on transient failures.
233
+ * Each step retries with exponential backoff via `withRetry`.
271
234
  */
272
235
  const log$2 = (0, _auriclabs_logger.createLogger)("SyncUploader");
273
- const MAX_RETRIES$1 = 3;
274
- const BASE_DELAY_MS$1 = 1e3;
275
- /**
276
- * Determine the storage class based on file path.
277
- */
236
+ const ARCHIVE_PREFIXES = ["sessions/archive/", "context/archive/"];
278
237
  function getStorageClass(relativePath) {
279
- return relativePath.startsWith("sessions/archive/") || relativePath.startsWith("context/archive/") ? "GLACIER_IR" : "STANDARD";
238
+ return ARCHIVE_PREFIXES.some((p) => relativePath.startsWith(p)) ? "GLACIER_IR" : "STANDARD";
280
239
  }
281
- /**
282
- * Determine MIME type from file path.
283
- */
284
240
  function getContentType(relativePath) {
285
241
  if (relativePath.endsWith(".json")) return "application/json";
286
242
  if (relativePath.endsWith(".md")) return "text/markdown";
@@ -289,62 +245,63 @@ function getContentType(relativePath) {
289
245
  if (relativePath.endsWith(".yaml") || relativePath.endsWith(".yml")) return "text/yaml";
290
246
  return "application/octet-stream";
291
247
  }
292
- /**
293
- * Upload a single file with retry logic.
294
- */
295
- async function uploadFileWithRetry(workspacePath, relativePath, client) {
248
+ async function uploadOne(workspacePath, relativePath, client) {
296
249
  const absolutePath = (0, node_path.join)(workspacePath, relativePath);
297
- let lastError;
298
- for (let attempt = 0; attempt <= MAX_RETRIES$1; attempt++) try {
299
- const [hash, fileStat] = await Promise.all([computeFileHash(absolutePath), (0, node_fs_promises.stat)(absolutePath)]);
300
- const size = fileStat.size;
301
- const storageClass = getStorageClass(relativePath);
302
- const contentType = getContentType(relativePath);
303
- const presigned = await client.presignPut(relativePath, contentType);
304
- const fileContent = await (0, node_fs_promises.readFile)(absolutePath);
305
- const putResponse = await fetch(presigned.url, {
306
- method: "PUT",
307
- headers: { "Content-Type": contentType },
308
- body: fileContent
309
- });
310
- if (!putResponse.ok) throw new Error(`S3 PUT failed (${String(putResponse.status)}): ${await putResponse.text()}`);
311
- await client.confirmUpload(relativePath, hash, size, storageClass);
312
- await updateManifestEntry(workspacePath, relativePath, {
313
- hash,
314
- size,
315
- lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
316
- storageClass
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
+ };
317
287
  });
288
+ } catch (err) {
318
289
  return {
319
290
  path: relativePath,
320
- success: true,
321
- hash,
322
- size
291
+ success: false,
292
+ error: err instanceof Error ? err.message : String(err)
323
293
  };
324
- } catch (err) {
325
- lastError = err instanceof Error ? err : new Error(String(err));
326
- if (attempt < MAX_RETRIES$1) {
327
- const delay = BASE_DELAY_MS$1 * Math.pow(2, attempt);
328
- await new Promise((resolve) => setTimeout(resolve, delay));
329
- }
330
294
  }
331
- return {
332
- path: relativePath,
333
- success: false,
334
- error: lastError?.message ?? "Unknown error"
335
- };
336
295
  }
337
296
  /**
338
- * Upload multiple files to S3.
339
- *
340
- * Uploads are performed in parallel with a concurrency limit.
297
+ * Upload many files, batched by `concurrency`.
341
298
  */
342
299
  async function uploadFiles(workspacePath, relativePaths, client, options = {}) {
343
300
  const { concurrency = 5, quiet = false } = options;
344
301
  const results = [];
345
302
  for (let i = 0; i < relativePaths.length; i += concurrency) {
346
303
  const batch = relativePaths.slice(i, i + concurrency);
347
- const batchResults = await Promise.all(batch.map((path) => uploadFileWithRetry(workspacePath, path, client)));
304
+ const batchResults = await Promise.all(batch.map((path) => uploadOne(workspacePath, path, client)));
348
305
  for (const result of batchResults) {
349
306
  results.push(result);
350
307
  if (!quiet) if (result.success) log$2.info(`Uploaded ${result.path} (${formatBytes$1(result.size ?? 0)})`);
@@ -361,65 +318,55 @@ function formatBytes$1(bytes) {
361
318
  //#endregion
362
319
  //#region src/downloader.ts
363
320
  /**
364
- * AlfeSync downloader — download files from S3 via presigned URLs.
321
+ * AlfeSync downloader — pull files from S3 to disk.
365
322
  *
366
- * Flow per file:
367
- * 1. Request presigned GET URL from API
368
- * 2. GET file content from S3
369
- * 3. Write to local disk
370
- * 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
371
328
  */
372
329
  const log$1 = (0, _auriclabs_logger.createLogger)("SyncDownloader");
373
- const MAX_RETRIES = 3;
374
- const BASE_DELAY_MS = 1e3;
375
- /**
376
- * Download a single file with retry logic.
377
- */
378
- async function downloadFileWithRetry(workspacePath, relativePath, client, remoteEntry) {
379
- let lastError;
380
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) try {
381
- const presigned = await client.presignGet(relativePath);
382
- const response = await fetch(presigned.url);
383
- if (!response.ok) throw new Error(`S3 GET failed (${String(response.status)}): ${await response.text()}`);
384
- const buffer = Buffer.from(await response.arrayBuffer());
385
- const absolutePath = (0, node_path.join)(workspacePath, relativePath);
386
- await (0, node_fs_promises.mkdir)((0, node_path.dirname)(absolutePath), { recursive: true });
387
- await (0, node_fs_promises.writeFile)(absolutePath, buffer);
388
- await updateManifestEntry(workspacePath, relativePath, {
389
- hash: remoteEntry?.hash ?? "",
390
- size: buffer.length,
391
- lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
392
- storageClass: remoteEntry?.storageClass ?? "STANDARD"
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
+ };
393
355
  });
356
+ } catch (err) {
394
357
  return {
395
358
  path: relativePath,
396
- success: true,
397
- size: buffer.length
359
+ success: false,
360
+ error: err instanceof Error ? err.message : String(err)
398
361
  };
399
- } catch (err) {
400
- lastError = err instanceof Error ? err : new Error(String(err));
401
- if (attempt < MAX_RETRIES) {
402
- const delay = BASE_DELAY_MS * Math.pow(2, attempt);
403
- await new Promise((resolve) => setTimeout(resolve, delay));
404
- }
405
362
  }
406
- return {
407
- path: relativePath,
408
- success: false,
409
- error: lastError?.message ?? "Unknown error"
410
- };
411
363
  }
412
- /**
413
- * Download multiple files from S3.
414
- *
415
- * Downloads are performed in parallel with a concurrency limit.
416
- */
417
364
  async function downloadFiles(workspacePath, relativePaths, client, remoteManifest, options = {}) {
418
365
  const { concurrency = 5, quiet = false } = options;
419
366
  const results = [];
420
367
  for (let i = 0; i < relativePaths.length; i += concurrency) {
421
368
  const batch = relativePaths.slice(i, i + concurrency);
422
- const batchResults = await Promise.all(batch.map((path) => downloadFileWithRetry(workspacePath, path, client, remoteManifest?.files[path])));
369
+ const batchResults = await Promise.all(batch.map((path) => downloadOne(workspacePath, path, client, remoteManifest?.files[path])));
423
370
  for (const result of batchResults) {
424
371
  results.push(result);
425
372
  if (!quiet) if (result.success) log$1.info(`Downloaded ${result.path} (${formatBytes(result.size ?? 0)})`);
@@ -436,36 +383,31 @@ function formatBytes(bytes) {
436
383
  //#endregion
437
384
  //#region src/sync-engine.ts
438
385
  /**
439
- * AlfeSync engine — orchestrates push, pull, and full sync operations.
386
+ * AlfeSync engine — orchestrates push, pull, and full sync.
440
387
  *
441
- * Handles:
442
388
  * - push(paths[]): upload changed files to S3
443
389
  * - pull(): download files newer on remote
444
390
  * - fullSync(): bidirectional sync with conflict detection
445
391
  *
446
- * Conflict resolution: if remote file is newer than local manifest entry
447
- * AND local file has changed write `.conflict-{timestamp}` alongside original.
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.
448
395
  */
449
396
  const log = (0, _auriclabs_logger.createLogger)("SyncEngine");
450
397
  /**
451
- * Create a sync engine for a workspace.
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.
452
402
  */
453
- async function createSyncEngine(workspacePath) {
454
- const config = await requireConfig(workspacePath);
455
- const client = createApiClient({
456
- apiUrl: config.apiUrl,
457
- token: config.token,
458
- agentId: config.agentId
459
- });
403
+ function createSyncEngine({ workspacePath, client }) {
460
404
  return {
461
- config,
405
+ workspacePath,
462
406
  client,
463
407
  async push(paths, options = {}) {
464
408
  const { quiet = false, filter } = options;
465
- const ignorePatterns = await require_ignore.loadIgnorePatterns(workspacePath);
466
- let filesToPush;
467
- if (paths && paths.length > 0) filesToPush = require_ignore.filterIgnored(paths, ignorePatterns);
468
- 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);
469
411
  if (filter) filesToPush = filesToPush.filter((p) => p.startsWith(filter));
470
412
  if (filesToPush.length === 0) {
471
413
  if (!quiet) log.info("Nothing to push");
@@ -490,10 +432,9 @@ async function createSyncEngine(workspacePath) {
490
432
  },
491
433
  async pull(options = {}) {
492
434
  const { quiet = false } = options;
493
- const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.getManifest()]);
435
+ const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.syncGetManifest()]);
494
436
  const diff = diffManifests(localManifest, remoteManifest);
495
- const filesToPull = [...diff.toPull];
496
- if (filesToPull.length === 0) {
437
+ if (diff.toPull.length === 0) {
497
438
  if (!quiet) log.info("Nothing to pull");
498
439
  return {
499
440
  pushed: 0,
@@ -502,8 +443,8 @@ async function createSyncEngine(workspacePath) {
502
443
  errors: 0
503
444
  };
504
445
  }
505
- if (!quiet) log.info(`Pulling ${String(filesToPull.length)} file(s)`);
506
- const results = await downloadFiles(workspacePath, filesToPull, client, remoteManifest, { quiet });
446
+ if (!quiet) log.info(`Pulling ${String(diff.toPull.length)} file(s)`);
447
+ const results = await downloadFiles(workspacePath, [...diff.toPull], client, remoteManifest, { quiet });
507
448
  const pulled = results.filter((r) => r.success).length;
508
449
  const errors = results.filter((r) => !r.success).length;
509
450
  if (!quiet) log.info(`Pull complete: ${String(pulled)} downloaded, ${String(errors)} failed`);
@@ -516,8 +457,8 @@ async function createSyncEngine(workspacePath) {
516
457
  },
517
458
  async fullSync(options = {}) {
518
459
  const { quiet = false } = options;
519
- const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.getManifest()]);
520
- const ignorePatterns = await require_ignore.loadIgnorePatterns(workspacePath);
460
+ const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.syncGetManifest()]);
461
+ const ignorePatterns = await loadIgnorePatterns(workspacePath);
521
462
  const localChanges = await detectLocalChanges(workspacePath, ignorePatterns);
522
463
  const diff = diffManifests(localManifest, remoteManifest);
523
464
  const trueConflicts = diff.conflicts.filter((p) => localChanges.includes(p));
@@ -525,17 +466,16 @@ async function createSyncEngine(workspacePath) {
525
466
  let conflictCount = 0;
526
467
  for (const conflictPath of trueConflicts) {
527
468
  const absolutePath = (0, node_path.join)(workspacePath, conflictPath);
528
- if ((0, node_fs.existsSync)(absolutePath)) {
529
- const ext = (0, node_path.extname)(conflictPath);
530
- const base = (0, node_path.basename)(conflictPath, ext);
531
- const dir = (0, node_path.dirname)(conflictPath);
532
- const conflictName = `${base}.conflict-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}${ext}`;
533
- await (0, node_fs_promises.writeFile)((0, node_path.join)(workspacePath, dir, conflictName), await (0, node_fs_promises.readFile)(absolutePath));
534
- if (!quiet) log.warn(`Conflict: ${conflictPath} — saved as ${conflictName}`);
535
- conflictCount++;
536
- }
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++;
537
477
  }
538
- const filesToPush = require_ignore.filterIgnored([...diff.toPush, ...localChanges.filter((p) => !diff.conflicts.includes(p))], ignorePatterns);
478
+ const filesToPush = filterIgnored([...diff.toPush, ...localChanges.filter((p) => !diff.conflicts.includes(p))], ignorePatterns);
539
479
  const filesToPull = [
540
480
  ...diff.toPull,
541
481
  ...remoteOnlyChanges,
@@ -578,13 +518,12 @@ async function createSyncEngine(workspacePath) {
578
518
  conflicts: 0,
579
519
  errors: 0
580
520
  };
581
- const remoteManifest = await client.getManifest();
521
+ const remoteManifest = await client.syncGetManifest();
582
522
  const localManifest = await readManifest(workspacePath);
583
523
  const filesToPull = paths.filter((p) => {
584
524
  if (!(p in remoteManifest.files)) return false;
585
- const remoteEntry = remoteManifest.files[p];
586
525
  if (!(p in localManifest.files)) return true;
587
- return localManifest.files[p].hash !== remoteEntry.hash;
526
+ return localManifest.files[p].hash !== remoteManifest.files[p].hash;
588
527
  });
589
528
  if (filesToPull.length === 0) {
590
529
  if (!quiet) log.info("All notified files already in sync");
@@ -624,10 +563,8 @@ async function createSyncEngine(workspacePath) {
624
563
  };
625
564
  }
626
565
  /**
627
- * Detect files that have changed locally since last sync.
628
- *
629
- * Compares current file hashes against the local manifest.
630
- * 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).
631
568
  */
632
569
  async function detectLocalChanges(workspacePath, ignorePatterns) {
633
570
  const manifest = await readManifest(workspacePath);
@@ -640,55 +577,37 @@ async function detectLocalChanges(workspacePath, ignorePatterns) {
640
577
  const relativePath = fullPath.slice(workspacePath.length + 1).replace(/\\/g, "/");
641
578
  if (shouldSkipDir(entry.name)) continue;
642
579
  if (entry.isDirectory()) {
643
- if (!ignorePatterns.some((p) => {
644
- if (p.endsWith("/**")) return relativePath.startsWith(p.slice(0, -3));
645
- return false;
646
- })) await walk(fullPath);
580
+ if (!ignorePatterns.some((p) => p.endsWith("/**") && relativePath.startsWith(p.slice(0, -3)))) await walk(fullPath);
647
581
  } else if (entry.isFile()) {
648
- const { shouldIgnore } = await Promise.resolve().then(() => require("./ignore.cjs")).then((n) => n.ignore_exports);
649
582
  if (shouldIgnore(relativePath, ignorePatterns)) continue;
650
- if (!(relativePath in manifest.files)) changed.push(relativePath);
651
- else {
652
- const manifestEntry = manifest.files[relativePath];
653
- try {
654
- if (await computeFileHash(fullPath) !== manifestEntry.hash) changed.push(relativePath);
655
- } catch {}
583
+ if (!(relativePath in manifest.files)) {
584
+ changed.push(relativePath);
585
+ continue;
656
586
  }
587
+ try {
588
+ if (await computeFileHash(fullPath) !== manifest.files[relativePath].hash) changed.push(relativePath);
589
+ } catch {}
657
590
  }
658
591
  }
659
592
  }
660
593
  await walk(workspacePath);
661
594
  return changed;
662
595
  }
663
- /**
664
- * Directories to always skip during walks.
665
- */
596
+ /** Directories the engine never descends into. */
666
597
  function shouldSkipDir(name) {
667
- return name === "node_modules" || name === ".git" || name === ".sst" || name === ".alfesync" || name === ".build" || name === "dist";
598
+ return name === "node_modules" || name === ".git" || name === ".sst" || name === ".build" || name === "dist";
668
599
  }
669
600
  //#endregion
670
- Object.defineProperty(exports, "computeFileHash", {
671
- enumerable: true,
672
- get: function() {
673
- return computeFileHash;
674
- }
675
- });
676
- Object.defineProperty(exports, "configDir", {
601
+ Object.defineProperty(exports, "__toESM", {
677
602
  enumerable: true,
678
603
  get: function() {
679
- return configDir;
604
+ return __toESM;
680
605
  }
681
606
  });
682
- Object.defineProperty(exports, "configPath", {
683
- enumerable: true,
684
- get: function() {
685
- return configPath;
686
- }
687
- });
688
- Object.defineProperty(exports, "createApiClient", {
607
+ Object.defineProperty(exports, "computeFileHash", {
689
608
  enumerable: true,
690
609
  get: function() {
691
- return createApiClient;
610
+ return computeFileHash;
692
611
  }
693
612
  });
694
613
  Object.defineProperty(exports, "createSyncEngine", {
@@ -709,16 +628,16 @@ Object.defineProperty(exports, "downloadFiles", {
709
628
  return downloadFiles;
710
629
  }
711
630
  });
712
- Object.defineProperty(exports, "isInitialized", {
631
+ Object.defineProperty(exports, "filterIgnored", {
713
632
  enumerable: true,
714
633
  get: function() {
715
- return isInitialized;
634
+ return filterIgnored;
716
635
  }
717
636
  });
718
- Object.defineProperty(exports, "readConfig", {
637
+ Object.defineProperty(exports, "loadIgnorePatterns", {
719
638
  enumerable: true,
720
639
  get: function() {
721
- return readConfig;
640
+ return loadIgnorePatterns;
722
641
  }
723
642
  });
724
643
  Object.defineProperty(exports, "readManifest", {
@@ -733,10 +652,10 @@ Object.defineProperty(exports, "removeManifestEntry", {
733
652
  return removeManifestEntry;
734
653
  }
735
654
  });
736
- Object.defineProperty(exports, "requireConfig", {
655
+ Object.defineProperty(exports, "shouldIgnore", {
737
656
  enumerable: true,
738
657
  get: function() {
739
- return requireConfig;
658
+ return shouldIgnore;
740
659
  }
741
660
  });
742
661
  Object.defineProperty(exports, "updateManifestEntry", {
@@ -751,10 +670,10 @@ Object.defineProperty(exports, "uploadFiles", {
751
670
  return uploadFiles;
752
671
  }
753
672
  });
754
- Object.defineProperty(exports, "writeConfig", {
673
+ Object.defineProperty(exports, "withRetry", {
755
674
  enumerable: true,
756
675
  get: function() {
757
- return writeConfig;
676
+ return withRetry;
758
677
  }
759
678
  });
760
679
  Object.defineProperty(exports, "writeManifest", {