@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.
- package/dist/cli/index.cjs +45 -109
- package/dist/cli/index.js +45 -109
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +4 -11
- package/dist/index.d.cts +798 -190
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +798 -190
- 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 +111 -104
- package/dist/plugin2.js +110 -103
- package/dist/plugin2.js.map +1 -1
- package/dist/sync-engine.cjs +263 -344
- package/dist/sync-engine.js +226 -318
- 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,96 +1,68 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
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(
|
|
87
|
-
return (0, node_path.join)(
|
|
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(
|
|
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
|
-
|
|
107
|
-
await (0, node_fs_promises.
|
|
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/
|
|
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
|
-
*
|
|
164
|
+
* Load ignore patterns from `.alfesyncignore` file + defaults.
|
|
175
165
|
*/
|
|
176
|
-
function
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
"
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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 —
|
|
225
|
+
* AlfeSync uploader — push changed files to S3.
|
|
263
226
|
*
|
|
264
|
-
*
|
|
265
|
-
* 1.
|
|
266
|
-
* 2. PUT
|
|
267
|
-
* 3.
|
|
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
|
-
*
|
|
233
|
+
* Each step retries with exponential backoff via `withRetry`.
|
|
271
234
|
*/
|
|
272
235
|
const log$2 = (0, _auriclabs_logger.createLogger)("SyncUploader");
|
|
273
|
-
const
|
|
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
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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:
|
|
321
|
-
|
|
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
|
|
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) =>
|
|
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 —
|
|
321
|
+
* AlfeSync downloader — pull files from S3 to disk.
|
|
365
322
|
*
|
|
366
|
-
*
|
|
367
|
-
* 1.
|
|
368
|
-
* 2. GET
|
|
369
|
-
* 3. Write to
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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:
|
|
397
|
-
|
|
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) =>
|
|
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
|
|
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:
|
|
447
|
-
* 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.
|
|
448
395
|
*/
|
|
449
396
|
const log = (0, _auriclabs_logger.createLogger)("SyncEngine");
|
|
450
397
|
/**
|
|
451
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
405
|
+
workspacePath,
|
|
462
406
|
client,
|
|
463
407
|
async push(paths, options = {}) {
|
|
464
408
|
const { quiet = false, filter } = options;
|
|
465
|
-
const ignorePatterns = await
|
|
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.
|
|
435
|
+
const [localManifest, remoteManifest] = await Promise.all([readManifest(workspacePath), client.syncGetManifest()]);
|
|
494
436
|
const diff = diffManifests(localManifest, remoteManifest);
|
|
495
|
-
|
|
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(
|
|
506
|
-
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 });
|
|
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.
|
|
520
|
-
const ignorePatterns = await
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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 =
|
|
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.
|
|
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 !==
|
|
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
|
-
*
|
|
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))
|
|
651
|
-
|
|
652
|
-
|
|
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 === ".
|
|
598
|
+
return name === "node_modules" || name === ".git" || name === ".sst" || name === ".build" || name === "dist";
|
|
668
599
|
}
|
|
669
600
|
//#endregion
|
|
670
|
-
Object.defineProperty(exports, "
|
|
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
|
|
604
|
+
return __toESM;
|
|
680
605
|
}
|
|
681
606
|
});
|
|
682
|
-
Object.defineProperty(exports, "
|
|
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
|
|
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, "
|
|
631
|
+
Object.defineProperty(exports, "filterIgnored", {
|
|
713
632
|
enumerable: true,
|
|
714
633
|
get: function() {
|
|
715
|
-
return
|
|
634
|
+
return filterIgnored;
|
|
716
635
|
}
|
|
717
636
|
});
|
|
718
|
-
Object.defineProperty(exports, "
|
|
637
|
+
Object.defineProperty(exports, "loadIgnorePatterns", {
|
|
719
638
|
enumerable: true,
|
|
720
639
|
get: function() {
|
|
721
|
-
return
|
|
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, "
|
|
655
|
+
Object.defineProperty(exports, "shouldIgnore", {
|
|
737
656
|
enumerable: true,
|
|
738
657
|
get: function() {
|
|
739
|
-
return
|
|
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, "
|
|
673
|
+
Object.defineProperty(exports, "withRetry", {
|
|
755
674
|
enumerable: true,
|
|
756
675
|
get: function() {
|
|
757
|
-
return
|
|
676
|
+
return withRetry;
|
|
758
677
|
}
|
|
759
678
|
});
|
|
760
679
|
Object.defineProperty(exports, "writeManifest", {
|