@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/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 formatProgress(current, total) {
4700
- const percentage = total > 0 ? Math.round(current / total * 100) : 0;
4701
- return `${current}/${total} (${percentage}%)`;
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
- var brandColor;
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/compress.ts
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
- function compressBuffer(data) {
5134
- return new Promise((resolve, reject) => {
5135
- const gzip = createGzip();
5136
- const chunks = [];
5137
- gzip.on("data", (chunk) => chunks.push(chunk));
5138
- gzip.on("end", () => resolve(Buffer.concat(chunks)));
5139
- gzip.on("error", reject);
5140
- gzip.end(data);
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 decompressBuffer(data) {
5144
- return new Promise((resolve, reject) => {
5145
- const gunzip = createGunzip();
5146
- const chunks = [];
5147
- gunzip.on("data", (chunk) => chunks.push(chunk));
5148
- gunzip.on("end", () => resolve(Buffer.concat(chunks)));
5149
- gunzip.on("error", reject);
5150
- gunzip.end(data);
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 init_compress = __esm({
5154
- "src/lib/compress.ts"() {
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 { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, lstat as lstat2, symlink, unlink, rename } from "fs/promises";
5169
- import { join as join3, dirname as dirname2 } from "path";
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 = join3(homedir4(), import_utils3.CLAUDE_DIR, import_utils3.PROJECTS_DIR);
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 = join3(projectsDir, ctx.symlinkTarget);
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 = { uploaded: 0, projectId: projectLink.projectId, failed: false };
5219
- let pullResult = { downloaded: 0, failed: false };
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 pushPhase(client, config.deviceId, cwd, manifest, projectDir, projectLink.projectId, options);
5326
+ pushResult = await bundlePushPhase(client, config.deviceId, cwd, manifest, projectDir, projectLink.projectId, options);
5222
5327
  }
5223
5328
  if (!pushResult.failed) {
5224
- pullResult = await pullPhase(client, config.deviceId, cwd, projectDir, projectLink.projectId, options);
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.uploaded > 0 || pullResult.downloaded > 0) {
5337
+ if (pushResult.fileCount > 0 || pullResult.fileCount > 0) {
5233
5338
  const parts = [];
5234
- if (pushResult.uploaded > 0) parts.push(`${success(`+${pushResult.uploaded}`)} pushed`);
5235
- if (pullResult.downloaded > 0) parts.push(`${success(`+${pullResult.downloaded}`)} pulled`);
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 result = await pushPhase(client, config.deviceId, cwd, manifest, projectDir, void 0, options);
5285
- if (result.failed) {
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: result.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 downloaded = await crossMachinePull(
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 (downloaded === 0 && !options.dryRun) {
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 = join3(projectsDir, localEncoded);
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 = join3(projectsDir, localEncoded);
5401
- await mkdir2(projectsDir, { recursive: true });
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 crossMachinePull(client, deviceId, projectPath, fromDeviceId, projectId, foreignEncodedDir, projectsDir, options) {
5412
- const spinner = createSpinner("Preparing pull from remote device...").start();
5413
- let prepareResponse;
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
- spinner.stop();
5428
- const filesToDownload = prepareResponse.filesToDownload;
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 filesToDownload) {
5434
- console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5520
+ for (const entry of manifest) {
5521
+ console.log(` ${success("+")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5435
5522
  }
5436
5523
  }
5437
- return filesToDownload.length;
5524
+ return { fileCount: manifest.length, projectId: projectId || "", failed: false };
5438
5525
  }
5439
- const targetDir = join3(projectsDir, foreignEncodedDir);
5440
- let downloaded = 0;
5441
- for (const entry of filesToDownload) {
5442
- const dlSpinner = createSpinner(
5443
- `${formatProgress(downloaded + 1, filesToDownload.length)} ${entry.path}`
5444
- ).start();
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
- const urlResponse = await client.post("/api/sync/pull/download-url", {
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
- key: entry.path
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
- dlSpinner.stop();
5462
- }
5463
- try {
5464
- await client.post("/api/sync/pull/complete", {
5465
- syncEventId: prepareResponse.syncEventId,
5466
- manifest: filesToDownload
5467
- });
5468
- } catch {
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 pushPhase(client, deviceId, projectPath, manifest, projectDir, projectId, options) {
5473
- const spinner = createSpinner("Preparing push...").start();
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/push/prepare", {
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(`Push failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5486
- return { uploaded: 0, projectId: projectId || "", failed: true };
5583
+ printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5584
+ return { fileCount: 0, failed: true };
5487
5585
  }
5488
- spinner.stop();
5489
- const resolvedProjectId = prepareResponse.projectId;
5490
- const filesToUpload = prepareResponse.filesToUpload;
5491
- if (filesToUpload.length === 0) return { uploaded: 0, projectId: resolvedProjectId, failed: false };
5492
- printInfo(`${brand(String(filesToUpload.length))} files to push`);
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 filesToUpload) {
5496
- console.log(` ${success("+")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5595
+ for (const entry of prepareResponse.manifest) {
5596
+ console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5497
5597
  }
5498
5598
  }
5499
- return { uploaded: filesToUpload.length, projectId: resolvedProjectId, failed: false };
5599
+ return { fileCount, failed: false };
5500
5600
  }
5501
- let uploaded = 0;
5502
- for (const entry of filesToUpload) {
5503
- const uploadSpinner = createSpinner(
5504
- `${formatProgress(uploaded + 1, filesToUpload.length)} ${entry.path}`
5505
- ).start();
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 urlResponse = await client.post("/api/sync/push/upload-url", {
5610
+ const completeManifest = await walkDirectory(projectDir);
5611
+ await client.post("/api/sync/pull/bundle/complete", {
5508
5612
  syncEventId: prepareResponse.syncEventId,
5509
- key: entry.path
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
- uploadSpinner.stop();
5525
- }
5526
- try {
5527
- await client.post("/api/sync/push/complete", {
5528
- syncEventId: prepareResponse.syncEventId,
5529
- manifest
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 pullPhase(client, deviceId, projectPath, projectDir, projectId, options) {
5536
- const spinner = createSpinner("Preparing pull...").start();
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 { downloaded: 0, failed: true };
5640
+ return { fileCount: 0, failed: true };
5549
5641
  }
5550
- spinner.stop();
5551
- const filesToDownload = prepareResponse.filesToDownload;
5552
- if (filesToDownload.length === 0) return { downloaded: 0, failed: false };
5553
- printInfo(`${brand(String(filesToDownload.length))} files to pull`);
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 filesToDownload) {
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 { downloaded: filesToDownload.length, failed: false };
5655
+ return { fileCount, failed: false };
5561
5656
  }
5562
- let downloaded = 0;
5563
- for (const entry of filesToDownload) {
5564
- const dlSpinner = createSpinner(
5565
- `${formatProgress(downloaded + 1, filesToDownload.length)} ${entry.path}`
5566
- ).start();
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 urlResponse = await client.post("/api/sync/pull/download-url", {
5668
+ const completeManifest = await walkDirectory(targetDir);
5669
+ await client.post("/api/sync/pull/bundle/complete", {
5569
5670
  syncEventId: prepareResponse.syncEventId,
5570
- key: entry.path
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
- dlSpinner.stop();
5584
- }
5585
- try {
5586
- await client.post("/api/sync/pull/complete", {
5587
- syncEventId: prepareResponse.syncEventId,
5588
- manifest: filesToDownload
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
- init_compress();
5698
+ init_bundle();
5609
5699
  import_utils3 = __toESM(require_dist(), 1);
5610
5700
  init_theme();
5611
5701
  }