@claude-sync/cli 0.1.14 → 0.1.15
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/bin/claude-sync.js +265 -177
- package/dist/bin/claude-sync.js.map +1 -1
- package/dist/src/index.js +265 -177
- package/dist/src/index.js.map +1 -1
- package/package.json +6 -4
package/dist/bin/claude-sync.js
CHANGED
|
@@ -4697,16 +4697,49 @@ function formatBytes(bytes) {
|
|
|
4697
4697
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
4698
4698
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
4699
4699
|
}
|
|
4700
|
-
function
|
|
4701
|
-
const
|
|
4702
|
-
|
|
4700
|
+
function createBar(percent, width = 20) {
|
|
4701
|
+
const filled = Math.round(percent / 100 * width);
|
|
4702
|
+
const empty = width - filled;
|
|
4703
|
+
return dimColor("[") + successColor("\u2588".repeat(filled)) + dimColor("\u2591".repeat(empty) + "]");
|
|
4703
4704
|
}
|
|
4704
|
-
|
|
4705
|
+
function formatSpeed(bytesPerSecond) {
|
|
4706
|
+
return `${formatBytes(bytesPerSecond)}/s`;
|
|
4707
|
+
}
|
|
4708
|
+
function createTransferProgress(initialLabel, totalBytes, spinner) {
|
|
4709
|
+
const startTime = Date.now();
|
|
4710
|
+
let lastUpdate = 0;
|
|
4711
|
+
let currentLabel = initialLabel;
|
|
4712
|
+
return {
|
|
4713
|
+
update(bytesTransferred) {
|
|
4714
|
+
const now = Date.now();
|
|
4715
|
+
if (now - lastUpdate < 50) return;
|
|
4716
|
+
lastUpdate = now;
|
|
4717
|
+
const percent = Math.round(bytesTransferred / totalBytes * 100);
|
|
4718
|
+
const elapsed = (now - startTime) / 1e3;
|
|
4719
|
+
const speed = elapsed > 0 ? bytesTransferred / elapsed : 0;
|
|
4720
|
+
const bar = createBar(percent);
|
|
4721
|
+
spinner.text = `${currentLabel} ${bar} ${dimColor(`${percent}%`)} ${dimColor("\u2022")} ${formatBytes(bytesTransferred)}/${formatBytes(totalBytes)} ${dimColor("\u2022")} ${formatSpeed(speed)}`;
|
|
4722
|
+
},
|
|
4723
|
+
setLabel(label) {
|
|
4724
|
+
currentLabel = label;
|
|
4725
|
+
},
|
|
4726
|
+
finish(message) {
|
|
4727
|
+
if (message) {
|
|
4728
|
+
spinner.succeed(message);
|
|
4729
|
+
} else {
|
|
4730
|
+
spinner.stop();
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
};
|
|
4734
|
+
}
|
|
4735
|
+
var brandColor, dimColor, successColor;
|
|
4705
4736
|
var init_progress = __esm({
|
|
4706
4737
|
"src/lib/progress.ts"() {
|
|
4707
4738
|
"use strict";
|
|
4708
4739
|
init_esm_shims();
|
|
4709
4740
|
brandColor = chalk.hex("#8a8cdd");
|
|
4741
|
+
dimColor = chalk.dim;
|
|
4742
|
+
successColor = chalk.hex("#22c55e");
|
|
4710
4743
|
}
|
|
4711
4744
|
});
|
|
4712
4745
|
|
|
@@ -5127,32 +5160,105 @@ var init_sync_engine = __esm({
|
|
|
5127
5160
|
}
|
|
5128
5161
|
});
|
|
5129
5162
|
|
|
5130
|
-
// src/lib/
|
|
5163
|
+
// src/lib/bundle.ts
|
|
5131
5164
|
import { createGzip, createGunzip } from "zlib";
|
|
5132
5165
|
import { createReadStream, createWriteStream } from "fs";
|
|
5166
|
+
import { readFile as readFile2, stat as stat2, mkdir as mkdir2 } from "fs/promises";
|
|
5167
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
5133
5168
|
import { pipeline } from "stream/promises";
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5169
|
+
import { pack, extract } from "tar-stream";
|
|
5170
|
+
async function createBundle(sourceDir, files, outputPath, onProgress) {
|
|
5171
|
+
const totalBytes = files.reduce((sum, f) => sum + f.size, 0);
|
|
5172
|
+
let processedBytes = 0;
|
|
5173
|
+
const archive = pack();
|
|
5174
|
+
const gzip = createGzip({ level: 6 });
|
|
5175
|
+
const output = createWriteStream(outputPath);
|
|
5176
|
+
const pipelinePromise = pipeline(archive, gzip, output);
|
|
5177
|
+
for (const file of files) {
|
|
5178
|
+
const filePath = join3(sourceDir, file.path);
|
|
5179
|
+
const content = await readFile2(filePath);
|
|
5180
|
+
archive.entry({ name: file.path, size: content.length }, content);
|
|
5181
|
+
processedBytes += file.size;
|
|
5182
|
+
onProgress?.(processedBytes, totalBytes, "compressing");
|
|
5183
|
+
}
|
|
5184
|
+
archive.finalize();
|
|
5185
|
+
await pipelinePromise;
|
|
5186
|
+
const outputStat = await stat2(outputPath);
|
|
5187
|
+
return {
|
|
5188
|
+
size: outputStat.size,
|
|
5189
|
+
fileCount: files.length,
|
|
5190
|
+
originalSize: totalBytes
|
|
5191
|
+
};
|
|
5192
|
+
}
|
|
5193
|
+
async function extractBundle(bundlePath, targetDir, onProgress) {
|
|
5194
|
+
await mkdir2(targetDir, { recursive: true });
|
|
5195
|
+
const extractor = extract();
|
|
5196
|
+
const gunzip = createGunzip();
|
|
5197
|
+
const input = createReadStream(bundlePath);
|
|
5198
|
+
let count = 0;
|
|
5199
|
+
extractor.on("entry", (header, stream, next) => {
|
|
5200
|
+
count++;
|
|
5201
|
+
onProgress?.(count, header.name);
|
|
5202
|
+
const outputPath = join3(targetDir, header.name);
|
|
5203
|
+
const dir = dirname2(outputPath);
|
|
5204
|
+
mkdir2(dir, { recursive: true }).then(() => {
|
|
5205
|
+
const fileOutput = createWriteStream(outputPath);
|
|
5206
|
+
stream.pipe(fileOutput);
|
|
5207
|
+
fileOutput.on("finish", next);
|
|
5208
|
+
fileOutput.on("error", next);
|
|
5209
|
+
}).catch(next);
|
|
5142
5210
|
});
|
|
5211
|
+
await pipeline(input, gunzip, extractor);
|
|
5212
|
+
return count;
|
|
5143
5213
|
}
|
|
5144
|
-
function
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5214
|
+
async function streamUpload(url, filePath, onProgress) {
|
|
5215
|
+
const fileStat = await stat2(filePath);
|
|
5216
|
+
const totalSize = fileStat.size;
|
|
5217
|
+
const fileStream = createReadStream(filePath);
|
|
5218
|
+
let uploaded = 0;
|
|
5219
|
+
const chunks = [];
|
|
5220
|
+
for await (const chunk of fileStream) {
|
|
5221
|
+
chunks.push(chunk);
|
|
5222
|
+
uploaded += chunk.length;
|
|
5223
|
+
onProgress?.(uploaded, totalSize);
|
|
5224
|
+
}
|
|
5225
|
+
const body = Buffer.concat(chunks);
|
|
5226
|
+
const response = await fetch(url, {
|
|
5227
|
+
method: "PUT",
|
|
5228
|
+
body,
|
|
5229
|
+
headers: {
|
|
5230
|
+
"Content-Type": "application/gzip",
|
|
5231
|
+
"Content-Length": String(totalSize)
|
|
5232
|
+
}
|
|
5233
|
+
});
|
|
5234
|
+
if (!response.ok) {
|
|
5235
|
+
throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
async function streamDownload(url, outputPath, expectedSize, onProgress) {
|
|
5239
|
+
const response = await fetch(url);
|
|
5240
|
+
if (!response.ok) {
|
|
5241
|
+
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
5242
|
+
}
|
|
5243
|
+
const reader = response.body?.getReader();
|
|
5244
|
+
if (!reader) throw new Error("No response body");
|
|
5245
|
+
const output = createWriteStream(outputPath);
|
|
5246
|
+
let downloaded = 0;
|
|
5247
|
+
while (true) {
|
|
5248
|
+
const { done, value } = await reader.read();
|
|
5249
|
+
if (done) break;
|
|
5250
|
+
output.write(value);
|
|
5251
|
+
downloaded += value.length;
|
|
5252
|
+
onProgress?.(downloaded, expectedSize);
|
|
5253
|
+
}
|
|
5254
|
+
await new Promise((resolve, reject) => {
|
|
5255
|
+
output.end();
|
|
5256
|
+
output.on("finish", resolve);
|
|
5257
|
+
output.on("error", reject);
|
|
5152
5258
|
});
|
|
5153
5259
|
}
|
|
5154
|
-
var
|
|
5155
|
-
"src/lib/
|
|
5260
|
+
var init_bundle = __esm({
|
|
5261
|
+
"src/lib/bundle.ts"() {
|
|
5156
5262
|
"use strict";
|
|
5157
5263
|
init_esm_shims();
|
|
5158
5264
|
}
|
|
@@ -5166,9 +5272,9 @@ __export(sync_exports, {
|
|
|
5166
5272
|
});
|
|
5167
5273
|
import { Command as Command4 } from "commander";
|
|
5168
5274
|
import { select as select2, isCancel as isCancel3 } from "@clack/prompts";
|
|
5169
|
-
import {
|
|
5170
|
-
import { join as
|
|
5171
|
-
import { homedir as homedir4 } from "os";
|
|
5275
|
+
import { mkdir as mkdir3, lstat as lstat2, symlink, unlink, rename, rm } from "fs/promises";
|
|
5276
|
+
import { join as join4 } from "path";
|
|
5277
|
+
import { homedir as homedir4, tmpdir } from "os";
|
|
5172
5278
|
async function runSync(options) {
|
|
5173
5279
|
printIntro("Sync");
|
|
5174
5280
|
const config = await loadConfig();
|
|
@@ -5183,11 +5289,11 @@ async function runSync(options) {
|
|
|
5183
5289
|
const cwd = process.cwd();
|
|
5184
5290
|
const ctx = await resolveProjectState(cwd);
|
|
5185
5291
|
const client = new ApiClient(config.apiUrl);
|
|
5186
|
-
const projectsDir =
|
|
5292
|
+
const projectsDir = join4(homedir4(), import_utils3.CLAUDE_DIR, import_utils3.PROJECTS_DIR);
|
|
5187
5293
|
const projectLink = await loadProjectLink(cwd);
|
|
5188
5294
|
let projectDir = ctx.projectDir;
|
|
5189
5295
|
if (ctx.state === "symlink" && ctx.symlinkTarget) {
|
|
5190
|
-
projectDir =
|
|
5296
|
+
projectDir = join4(projectsDir, ctx.symlinkTarget);
|
|
5191
5297
|
}
|
|
5192
5298
|
printInfo(`Project: ${brand(cwd)}`);
|
|
5193
5299
|
if (ctx.state === "symlink") {
|
|
@@ -5203,7 +5309,6 @@ async function runSync(options) {
|
|
|
5203
5309
|
if (result === "pulled") {
|
|
5204
5310
|
const finalManifest2 = await buildProjectManifest(cwd);
|
|
5205
5311
|
await saveProjectManifest(cwd, finalManifest2);
|
|
5206
|
-
printSuccess("Sessions synced from remote device.");
|
|
5207
5312
|
printOutro("Done");
|
|
5208
5313
|
return;
|
|
5209
5314
|
}
|
|
@@ -5216,13 +5321,13 @@ async function runSync(options) {
|
|
|
5216
5321
|
printOutro("Done");
|
|
5217
5322
|
return;
|
|
5218
5323
|
}
|
|
5219
|
-
let pushResult = {
|
|
5220
|
-
let pullResult = {
|
|
5324
|
+
let pushResult = { fileCount: 0, projectId: projectLink.projectId, failed: false };
|
|
5325
|
+
let pullResult = { fileCount: 0, failed: false };
|
|
5221
5326
|
if (manifest.length > 0) {
|
|
5222
|
-
pushResult = await
|
|
5327
|
+
pushResult = await bundlePushPhase(client, config.deviceId, cwd, manifest, projectDir, projectLink.projectId, options);
|
|
5223
5328
|
}
|
|
5224
5329
|
if (!pushResult.failed) {
|
|
5225
|
-
pullResult = await
|
|
5330
|
+
pullResult = await bundlePullPhase(client, config.deviceId, cwd, projectDir, projectLink.projectId, options);
|
|
5226
5331
|
}
|
|
5227
5332
|
if (pushResult.failed && pullResult.failed) {
|
|
5228
5333
|
printOutro("Sync failed.");
|
|
@@ -5230,11 +5335,11 @@ async function runSync(options) {
|
|
|
5230
5335
|
}
|
|
5231
5336
|
const finalManifest = await buildProjectManifest(cwd);
|
|
5232
5337
|
await saveProjectManifest(cwd, finalManifest);
|
|
5233
|
-
if (pushResult.
|
|
5338
|
+
if (pushResult.fileCount > 0 || pullResult.fileCount > 0) {
|
|
5234
5339
|
const parts = [];
|
|
5235
|
-
if (pushResult.
|
|
5236
|
-
if (pullResult.
|
|
5237
|
-
printSuccess(`Synced: ${parts.join(", ")}`);
|
|
5340
|
+
if (pushResult.fileCount > 0) parts.push(`${success(`${pushResult.fileCount}`)} pushed`);
|
|
5341
|
+
if (pullResult.fileCount > 0) parts.push(`${success(`${pullResult.fileCount}`)} pulled`);
|
|
5342
|
+
printSuccess(`Synced: ${parts.join(", ")} files`);
|
|
5238
5343
|
} else if (!pushResult.failed && !pullResult.failed) {
|
|
5239
5344
|
printSuccess("Everything is up to date.");
|
|
5240
5345
|
}
|
|
@@ -5282,15 +5387,12 @@ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, pr
|
|
|
5282
5387
|
});
|
|
5283
5388
|
if (isCancel3(choice)) return "cancelled";
|
|
5284
5389
|
if (choice === "push") {
|
|
5285
|
-
const
|
|
5286
|
-
if (
|
|
5390
|
+
const result2 = await bundlePushPhase(client, config.deviceId, cwd, manifest, projectDir, void 0, options);
|
|
5391
|
+
if (result2.failed) {
|
|
5287
5392
|
return "cancelled";
|
|
5288
5393
|
}
|
|
5289
|
-
if (result.uploaded > 0) {
|
|
5290
|
-
printSuccess(`${success(`+${result.uploaded}`)} files pushed`);
|
|
5291
|
-
}
|
|
5292
5394
|
await saveProjectLink(cwd, {
|
|
5293
|
-
projectId:
|
|
5395
|
+
projectId: result2.projectId,
|
|
5294
5396
|
foreignEncodedDir: ctx.encodedPath,
|
|
5295
5397
|
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5296
5398
|
});
|
|
@@ -5334,7 +5436,7 @@ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, pr
|
|
|
5334
5436
|
if (existingCheck === "cancelled") {
|
|
5335
5437
|
return "cancelled";
|
|
5336
5438
|
}
|
|
5337
|
-
const
|
|
5439
|
+
const result = await crossMachineBundlePull(
|
|
5338
5440
|
client,
|
|
5339
5441
|
config.deviceId,
|
|
5340
5442
|
cwd,
|
|
@@ -5344,7 +5446,7 @@ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, pr
|
|
|
5344
5446
|
projectsDir,
|
|
5345
5447
|
options
|
|
5346
5448
|
);
|
|
5347
|
-
if (
|
|
5449
|
+
if (result.fileCount === 0 && !options.dryRun) {
|
|
5348
5450
|
printInfo("No files to download.");
|
|
5349
5451
|
return "cancelled";
|
|
5350
5452
|
}
|
|
@@ -5355,13 +5457,12 @@ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, pr
|
|
|
5355
5457
|
foreignEncodedDir: selectedProject.encodedDir,
|
|
5356
5458
|
linkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5357
5459
|
});
|
|
5358
|
-
printSuccess(`${success(`+${downloaded}`)} files pulled`);
|
|
5359
5460
|
printInfo(`Symlink: ${dim(ctx.encodedPath)} \u2192 ${dim(selectedProject.encodedDir)}`);
|
|
5360
5461
|
}
|
|
5361
5462
|
return "pulled";
|
|
5362
5463
|
}
|
|
5363
5464
|
async function handleExistingSessionDir(projectsDir, localEncoded) {
|
|
5364
|
-
const symlinkPath =
|
|
5465
|
+
const symlinkPath = join4(projectsDir, localEncoded);
|
|
5365
5466
|
try {
|
|
5366
5467
|
const stats = await lstat2(symlinkPath);
|
|
5367
5468
|
if (stats.isSymbolicLink()) {
|
|
@@ -5398,8 +5499,8 @@ async function handleExistingSessionDir(projectsDir, localEncoded) {
|
|
|
5398
5499
|
return "continue";
|
|
5399
5500
|
}
|
|
5400
5501
|
async function createProjectSymlink(projectsDir, localEncoded, foreignEncoded) {
|
|
5401
|
-
const symlinkPath =
|
|
5402
|
-
await
|
|
5502
|
+
const symlinkPath = join4(projectsDir, localEncoded);
|
|
5503
|
+
await mkdir3(projectsDir, { recursive: true });
|
|
5403
5504
|
try {
|
|
5404
5505
|
const stats = await lstat2(symlinkPath);
|
|
5405
5506
|
if (stats.isSymbolicLink()) {
|
|
@@ -5409,190 +5510,177 @@ async function createProjectSymlink(projectsDir, localEncoded, foreignEncoded) {
|
|
|
5409
5510
|
}
|
|
5410
5511
|
await symlink(foreignEncoded, symlinkPath);
|
|
5411
5512
|
}
|
|
5412
|
-
async function
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
try {
|
|
5416
|
-
prepareResponse = await client.post("/api/sync/pull/prepare", {
|
|
5417
|
-
deviceId,
|
|
5418
|
-
projectPath,
|
|
5419
|
-
fromDeviceId,
|
|
5420
|
-
projectId
|
|
5421
|
-
});
|
|
5422
|
-
} catch (err) {
|
|
5423
|
-
spinner.stop();
|
|
5424
|
-
if (err instanceof AuthExpiredError) throw err;
|
|
5425
|
-
printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
5426
|
-
return 0;
|
|
5513
|
+
async function bundlePushPhase(client, deviceId, projectPath, manifest, projectDir, projectId, options) {
|
|
5514
|
+
if (manifest.length === 0) {
|
|
5515
|
+
return { fileCount: 0, projectId: projectId || "", failed: false };
|
|
5427
5516
|
}
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
if (filesToDownload.length === 0) return 0;
|
|
5431
|
-
printInfo(`${brand(String(filesToDownload.length))} files to pull`);
|
|
5517
|
+
const totalBytes = manifest.reduce((sum, f) => sum + f.size, 0);
|
|
5518
|
+
printInfo(`${brand(String(manifest.length))} files ${dim(`(${formatBytes(totalBytes)})`)} to push`);
|
|
5432
5519
|
if (options.dryRun) {
|
|
5433
5520
|
if (options.verbose) {
|
|
5434
|
-
for (const entry of
|
|
5435
|
-
console.log(` ${
|
|
5521
|
+
for (const entry of manifest) {
|
|
5522
|
+
console.log(` ${success("+")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
|
|
5436
5523
|
}
|
|
5437
5524
|
}
|
|
5438
|
-
return
|
|
5525
|
+
return { fileCount: manifest.length, projectId: projectId || "", failed: false };
|
|
5439
5526
|
}
|
|
5440
|
-
const
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
const
|
|
5444
|
-
|
|
5445
|
-
|
|
5527
|
+
const tempBundle = join4(tmpdir(), `claude-sync-bundle-${Date.now()}.tar.gz`);
|
|
5528
|
+
try {
|
|
5529
|
+
const spinner = createSpinner("Syncing...").start();
|
|
5530
|
+
const bundleResult = await createBundle(projectDir, manifest, tempBundle, (bytes, total, phase) => {
|
|
5531
|
+
if (phase === "compressing") {
|
|
5532
|
+
const percent = Math.round(bytes / total * 100);
|
|
5533
|
+
spinner.text = `Compressing files... ${dim(`${percent}%`)}`;
|
|
5534
|
+
}
|
|
5535
|
+
});
|
|
5536
|
+
const progress = createTransferProgress("Uploading", bundleResult.size, spinner);
|
|
5537
|
+
let prepareResponse;
|
|
5446
5538
|
try {
|
|
5447
|
-
|
|
5539
|
+
prepareResponse = await client.post("/api/sync/push/bundle/prepare", {
|
|
5540
|
+
deviceId,
|
|
5541
|
+
projectPath,
|
|
5542
|
+
projectId,
|
|
5543
|
+
manifest,
|
|
5544
|
+
bundleSize: bundleResult.size
|
|
5545
|
+
});
|
|
5546
|
+
} catch (err) {
|
|
5547
|
+
spinner.stop();
|
|
5548
|
+
if (err instanceof AuthExpiredError) throw err;
|
|
5549
|
+
printError(`Push failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
5550
|
+
return { fileCount: 0, projectId: projectId || "", failed: true };
|
|
5551
|
+
}
|
|
5552
|
+
await streamUpload(prepareResponse.uploadUrl, tempBundle, (bytes, total) => {
|
|
5553
|
+
progress.update(bytes);
|
|
5554
|
+
});
|
|
5555
|
+
try {
|
|
5556
|
+
await client.post("/api/sync/push/bundle/complete", {
|
|
5448
5557
|
syncEventId: prepareResponse.syncEventId,
|
|
5449
|
-
|
|
5558
|
+
manifest
|
|
5450
5559
|
});
|
|
5451
|
-
const response = await fetch(urlResponse.url);
|
|
5452
|
-
let data = Buffer.from(await response.arrayBuffer());
|
|
5453
|
-
if (entry.isCompressed) {
|
|
5454
|
-
data = await decompressBuffer(data);
|
|
5455
|
-
}
|
|
5456
|
-
const outputPath = join3(targetDir, entry.path);
|
|
5457
|
-
await mkdir2(dirname2(outputPath), { recursive: true });
|
|
5458
|
-
await writeFile2(outputPath, data);
|
|
5459
|
-
downloaded++;
|
|
5460
5560
|
} catch {
|
|
5461
5561
|
}
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
}
|
|
5470
|
-
} catch {
|
|
5562
|
+
const compressionRatio = Math.round((1 - bundleResult.size / bundleResult.originalSize) * 100);
|
|
5563
|
+
progress.finish(`Pushed ${manifest.length} files ${dim(`(${formatBytes(bundleResult.size)}, ${compressionRatio}% smaller)`)}`);
|
|
5564
|
+
return { fileCount: manifest.length, projectId: prepareResponse.projectId, failed: false };
|
|
5565
|
+
} finally {
|
|
5566
|
+
try {
|
|
5567
|
+
await rm(tempBundle, { force: true });
|
|
5568
|
+
} catch {
|
|
5569
|
+
}
|
|
5471
5570
|
}
|
|
5472
|
-
return downloaded;
|
|
5473
5571
|
}
|
|
5474
|
-
async function
|
|
5475
|
-
const spinner = createSpinner("
|
|
5572
|
+
async function bundlePullPhase(client, deviceId, projectPath, projectDir, projectId, options) {
|
|
5573
|
+
const spinner = createSpinner("Checking for updates...").start();
|
|
5476
5574
|
let prepareResponse;
|
|
5477
5575
|
try {
|
|
5478
|
-
prepareResponse = await client.post("/api/sync/
|
|
5576
|
+
prepareResponse = await client.post("/api/sync/pull/bundle/prepare", {
|
|
5479
5577
|
deviceId,
|
|
5480
5578
|
projectPath,
|
|
5481
|
-
projectId
|
|
5482
|
-
manifest
|
|
5579
|
+
projectId
|
|
5483
5580
|
});
|
|
5484
5581
|
} catch (err) {
|
|
5485
5582
|
spinner.stop();
|
|
5486
5583
|
if (err instanceof AuthExpiredError) throw err;
|
|
5487
|
-
printError(`
|
|
5488
|
-
return {
|
|
5584
|
+
printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
5585
|
+
return { fileCount: 0, failed: true };
|
|
5489
5586
|
}
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5587
|
+
if (!prepareResponse.downloadUrl || !prepareResponse.bundleSize) {
|
|
5588
|
+
spinner.stop();
|
|
5589
|
+
return { fileCount: 0, failed: false };
|
|
5590
|
+
}
|
|
5591
|
+
const fileCount = prepareResponse.manifest.length;
|
|
5495
5592
|
if (options.dryRun) {
|
|
5593
|
+
spinner.stop();
|
|
5594
|
+
printInfo(`${brand(String(fileCount))} files to pull`);
|
|
5496
5595
|
if (options.verbose) {
|
|
5497
|
-
for (const entry of
|
|
5498
|
-
console.log(` ${
|
|
5596
|
+
for (const entry of prepareResponse.manifest) {
|
|
5597
|
+
console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
|
|
5499
5598
|
}
|
|
5500
5599
|
}
|
|
5501
|
-
return {
|
|
5600
|
+
return { fileCount, failed: false };
|
|
5502
5601
|
}
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
const
|
|
5506
|
-
|
|
5507
|
-
|
|
5602
|
+
const tempBundle = join4(tmpdir(), `claude-sync-bundle-${Date.now()}.tar.gz`);
|
|
5603
|
+
try {
|
|
5604
|
+
const progress = createTransferProgress("Downloading", prepareResponse.bundleSize, spinner);
|
|
5605
|
+
await streamDownload(prepareResponse.downloadUrl, tempBundle, prepareResponse.bundleSize, (bytes) => {
|
|
5606
|
+
progress.update(bytes);
|
|
5607
|
+
});
|
|
5608
|
+
spinner.text = "Extracting files...";
|
|
5609
|
+
const extractedCount = await extractBundle(tempBundle, projectDir);
|
|
5508
5610
|
try {
|
|
5509
|
-
const
|
|
5611
|
+
const completeManifest = await walkDirectory(projectDir);
|
|
5612
|
+
await client.post("/api/sync/pull/bundle/complete", {
|
|
5510
5613
|
syncEventId: prepareResponse.syncEventId,
|
|
5511
|
-
|
|
5512
|
-
});
|
|
5513
|
-
const filePath = join3(projectDir, entry.path);
|
|
5514
|
-
let body = await readFile2(filePath);
|
|
5515
|
-
if (entry.isCompressed) {
|
|
5516
|
-
body = await compressBuffer(body);
|
|
5517
|
-
}
|
|
5518
|
-
await fetch(urlResponse.url, {
|
|
5519
|
-
method: "PUT",
|
|
5520
|
-
body,
|
|
5521
|
-
headers: { "Content-Type": "application/octet-stream" }
|
|
5614
|
+
manifest: completeManifest
|
|
5522
5615
|
});
|
|
5523
|
-
uploaded++;
|
|
5524
5616
|
} catch {
|
|
5525
5617
|
}
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
}
|
|
5533
|
-
} catch {
|
|
5618
|
+
progress.finish(`Pulled ${extractedCount} files ${dim(`(${formatBytes(prepareResponse.bundleSize)})`)}`);
|
|
5619
|
+
return { fileCount: extractedCount, failed: false };
|
|
5620
|
+
} finally {
|
|
5621
|
+
try {
|
|
5622
|
+
await rm(tempBundle, { force: true });
|
|
5623
|
+
} catch {
|
|
5624
|
+
}
|
|
5534
5625
|
}
|
|
5535
|
-
return { uploaded, projectId: resolvedProjectId, failed: false };
|
|
5536
5626
|
}
|
|
5537
|
-
async function
|
|
5538
|
-
const spinner = createSpinner("Preparing
|
|
5627
|
+
async function crossMachineBundlePull(client, deviceId, projectPath, fromDeviceId, projectId, foreignEncodedDir, projectsDir, options) {
|
|
5628
|
+
const spinner = createSpinner("Preparing download...").start();
|
|
5539
5629
|
let prepareResponse;
|
|
5540
5630
|
try {
|
|
5541
|
-
prepareResponse = await client.post("/api/sync/pull/prepare", {
|
|
5631
|
+
prepareResponse = await client.post("/api/sync/pull/bundle/prepare", {
|
|
5542
5632
|
deviceId,
|
|
5543
5633
|
projectPath,
|
|
5634
|
+
fromDeviceId,
|
|
5544
5635
|
projectId
|
|
5545
5636
|
});
|
|
5546
5637
|
} catch (err) {
|
|
5547
5638
|
spinner.stop();
|
|
5548
5639
|
if (err instanceof AuthExpiredError) throw err;
|
|
5549
5640
|
printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
|
|
5550
|
-
return {
|
|
5641
|
+
return { fileCount: 0, failed: true };
|
|
5551
5642
|
}
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5643
|
+
if (!prepareResponse.downloadUrl || !prepareResponse.bundleSize) {
|
|
5644
|
+
spinner.stop();
|
|
5645
|
+
return { fileCount: 0, failed: false };
|
|
5646
|
+
}
|
|
5647
|
+
const fileCount = prepareResponse.manifest.length;
|
|
5556
5648
|
if (options.dryRun) {
|
|
5649
|
+
spinner.stop();
|
|
5650
|
+
printInfo(`${brand(String(fileCount))} files to pull`);
|
|
5557
5651
|
if (options.verbose) {
|
|
5558
|
-
for (const entry of
|
|
5652
|
+
for (const entry of prepareResponse.manifest) {
|
|
5559
5653
|
console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
|
|
5560
5654
|
}
|
|
5561
5655
|
}
|
|
5562
|
-
return {
|
|
5656
|
+
return { fileCount, failed: false };
|
|
5563
5657
|
}
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5658
|
+
const targetDir = join4(projectsDir, foreignEncodedDir);
|
|
5659
|
+
const tempBundle = join4(tmpdir(), `claude-sync-bundle-${Date.now()}.tar.gz`);
|
|
5660
|
+
try {
|
|
5661
|
+
const progress = createTransferProgress("Downloading", prepareResponse.bundleSize, spinner);
|
|
5662
|
+
await streamDownload(prepareResponse.downloadUrl, tempBundle, prepareResponse.bundleSize, (bytes) => {
|
|
5663
|
+
progress.update(bytes);
|
|
5664
|
+
});
|
|
5665
|
+
spinner.text = "Extracting files...";
|
|
5666
|
+
await mkdir3(targetDir, { recursive: true });
|
|
5667
|
+
const extractedCount = await extractBundle(tempBundle, targetDir);
|
|
5569
5668
|
try {
|
|
5570
|
-
const
|
|
5669
|
+
const completeManifest = await walkDirectory(targetDir);
|
|
5670
|
+
await client.post("/api/sync/pull/bundle/complete", {
|
|
5571
5671
|
syncEventId: prepareResponse.syncEventId,
|
|
5572
|
-
|
|
5672
|
+
manifest: completeManifest
|
|
5573
5673
|
});
|
|
5574
|
-
const response = await fetch(urlResponse.url);
|
|
5575
|
-
let data = Buffer.from(await response.arrayBuffer());
|
|
5576
|
-
if (entry.isCompressed) {
|
|
5577
|
-
data = await decompressBuffer(data);
|
|
5578
|
-
}
|
|
5579
|
-
const outputPath = join3(projectDir, entry.path);
|
|
5580
|
-
await mkdir2(dirname2(outputPath), { recursive: true });
|
|
5581
|
-
await writeFile2(outputPath, data);
|
|
5582
|
-
downloaded++;
|
|
5583
5674
|
} catch {
|
|
5584
5675
|
}
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
});
|
|
5593
|
-
} catch {
|
|
5676
|
+
progress.finish(`Pulled ${extractedCount} files ${dim(`(${formatBytes(prepareResponse.bundleSize)})`)}`);
|
|
5677
|
+
return { fileCount: extractedCount, failed: false };
|
|
5678
|
+
} finally {
|
|
5679
|
+
try {
|
|
5680
|
+
await rm(tempBundle, { force: true });
|
|
5681
|
+
} catch {
|
|
5682
|
+
}
|
|
5594
5683
|
}
|
|
5595
|
-
return { downloaded, failed: false };
|
|
5596
5684
|
}
|
|
5597
5685
|
function syncCommand() {
|
|
5598
5686
|
return new Command4("sync").description("Sync current project sessions with cloud").option("--dry-run", "Show what would be synced without syncing").option("--verbose", "Show detailed output").action(async (options) => {
|
|
@@ -5608,7 +5696,7 @@ var init_sync = __esm({
|
|
|
5608
5696
|
init_api_client();
|
|
5609
5697
|
init_sync_engine();
|
|
5610
5698
|
init_progress();
|
|
5611
|
-
|
|
5699
|
+
init_bundle();
|
|
5612
5700
|
import_utils3 = __toESM(require_dist(), 1);
|
|
5613
5701
|
init_theme();
|
|
5614
5702
|
}
|