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