@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.
@@ -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 formatProgress(current, total) {
4701
- const percentage = total > 0 ? Math.round(current / total * 100) : 0;
4702
- return `${current}/${total} (${percentage}%)`;
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
- var brandColor;
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/compress.ts
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
- function compressBuffer(data) {
5135
- return new Promise((resolve, reject) => {
5136
- const gzip = createGzip();
5137
- const chunks = [];
5138
- gzip.on("data", (chunk) => chunks.push(chunk));
5139
- gzip.on("end", () => resolve(Buffer.concat(chunks)));
5140
- gzip.on("error", reject);
5141
- gzip.end(data);
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 decompressBuffer(data) {
5145
- return new Promise((resolve, reject) => {
5146
- const gunzip = createGunzip();
5147
- const chunks = [];
5148
- gunzip.on("data", (chunk) => chunks.push(chunk));
5149
- gunzip.on("end", () => resolve(Buffer.concat(chunks)));
5150
- gunzip.on("error", reject);
5151
- gunzip.end(data);
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 init_compress = __esm({
5155
- "src/lib/compress.ts"() {
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 { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, lstat as lstat2, symlink, unlink, rename } from "fs/promises";
5170
- import { join as join3, dirname as dirname2 } from "path";
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 = join3(homedir4(), import_utils3.CLAUDE_DIR, import_utils3.PROJECTS_DIR);
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 = join3(projectsDir, ctx.symlinkTarget);
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 = { uploaded: 0, projectId: projectLink.projectId, failed: false };
5220
- let pullResult = { downloaded: 0, failed: false };
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 pushPhase(client, config.deviceId, cwd, manifest, projectDir, projectLink.projectId, options);
5327
+ pushResult = await bundlePushPhase(client, config.deviceId, cwd, manifest, projectDir, projectLink.projectId, options);
5223
5328
  }
5224
5329
  if (!pushResult.failed) {
5225
- pullResult = await pullPhase(client, config.deviceId, cwd, projectDir, projectLink.projectId, options);
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.uploaded > 0 || pullResult.downloaded > 0) {
5338
+ if (pushResult.fileCount > 0 || pullResult.fileCount > 0) {
5234
5339
  const parts = [];
5235
- if (pushResult.uploaded > 0) parts.push(`${success(`+${pushResult.uploaded}`)} pushed`);
5236
- if (pullResult.downloaded > 0) parts.push(`${success(`+${pullResult.downloaded}`)} pulled`);
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 result = await pushPhase(client, config.deviceId, cwd, manifest, projectDir, void 0, options);
5286
- if (result.failed) {
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: result.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 downloaded = await crossMachinePull(
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 (downloaded === 0 && !options.dryRun) {
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 = join3(projectsDir, localEncoded);
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 = join3(projectsDir, localEncoded);
5402
- await mkdir2(projectsDir, { recursive: true });
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,188 +5510,177 @@ async function createProjectSymlink(projectsDir, localEncoded, foreignEncoded) {
5409
5510
  }
5410
5511
  await symlink(foreignEncoded, symlinkPath);
5411
5512
  }
5412
- async function crossMachinePull(client, deviceId, projectPath, fromDeviceId, projectId, foreignEncodedDir, projectsDir, options) {
5413
- const spinner = createSpinner("Preparing pull from remote device...").start();
5414
- let prepareResponse;
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
- spinner.stop();
5429
- const filesToDownload = prepareResponse.filesToDownload;
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 filesToDownload) {
5435
- console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5521
+ for (const entry of manifest) {
5522
+ console.log(` ${success("+")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5436
5523
  }
5437
5524
  }
5438
- return filesToDownload.length;
5525
+ return { fileCount: manifest.length, projectId: projectId || "", failed: false };
5439
5526
  }
5440
- const targetDir = join3(projectsDir, foreignEncodedDir);
5441
- let downloaded = 0;
5442
- for (const entry of filesToDownload) {
5443
- const dlSpinner = createSpinner(
5444
- `${formatProgress(downloaded + 1, filesToDownload.length)} ${entry.path}`
5445
- ).start();
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
- const urlResponse = await client.post("/api/sync/pull/download-url", {
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
- key: entry.path
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
- dlSpinner.stop();
5463
- }
5464
- try {
5465
- await client.post("/api/sync/pull/complete", {
5466
- syncEventId: prepareResponse.syncEventId,
5467
- manifest: filesToDownload
5468
- });
5469
- } 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
+ }
5470
5570
  }
5471
- return downloaded;
5472
5571
  }
5473
- async function pushPhase(client, deviceId, projectPath, manifest, projectDir, projectId, options) {
5474
- const spinner = createSpinner("Preparing push...").start();
5572
+ async function bundlePullPhase(client, deviceId, projectPath, projectDir, projectId, options) {
5573
+ const spinner = createSpinner("Checking for updates...").start();
5475
5574
  let prepareResponse;
5476
5575
  try {
5477
- prepareResponse = await client.post("/api/sync/push/prepare", {
5576
+ prepareResponse = await client.post("/api/sync/pull/bundle/prepare", {
5478
5577
  deviceId,
5479
5578
  projectPath,
5480
- projectId,
5481
- manifest
5579
+ projectId
5482
5580
  });
5483
5581
  } catch (err) {
5484
5582
  spinner.stop();
5485
5583
  if (err instanceof AuthExpiredError) throw err;
5486
- printError(`Push failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5487
- return { uploaded: 0, projectId: projectId || "", failed: true };
5584
+ printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5585
+ return { fileCount: 0, failed: true };
5488
5586
  }
5489
- spinner.stop();
5490
- const resolvedProjectId = prepareResponse.projectId;
5491
- const filesToUpload = prepareResponse.filesToUpload;
5492
- if (filesToUpload.length === 0) return { uploaded: 0, projectId: resolvedProjectId, failed: false };
5493
- printInfo(`${brand(String(filesToUpload.length))} files to push`);
5587
+ if (!prepareResponse.downloadUrl || !prepareResponse.bundleSize) {
5588
+ spinner.stop();
5589
+ return { fileCount: 0, failed: false };
5590
+ }
5591
+ const fileCount = prepareResponse.manifest.length;
5494
5592
  if (options.dryRun) {
5593
+ spinner.stop();
5594
+ printInfo(`${brand(String(fileCount))} files to pull`);
5495
5595
  if (options.verbose) {
5496
- for (const entry of filesToUpload) {
5497
- console.log(` ${success("+")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5596
+ for (const entry of prepareResponse.manifest) {
5597
+ console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5498
5598
  }
5499
5599
  }
5500
- return { uploaded: filesToUpload.length, projectId: resolvedProjectId, failed: false };
5600
+ return { fileCount, failed: false };
5501
5601
  }
5502
- let uploaded = 0;
5503
- for (const entry of filesToUpload) {
5504
- const uploadSpinner = createSpinner(
5505
- `${formatProgress(uploaded + 1, filesToUpload.length)} ${entry.path}`
5506
- ).start();
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);
5507
5610
  try {
5508
- const urlResponse = await client.post("/api/sync/push/upload-url", {
5611
+ const completeManifest = await walkDirectory(projectDir);
5612
+ await client.post("/api/sync/pull/bundle/complete", {
5509
5613
  syncEventId: prepareResponse.syncEventId,
5510
- key: entry.path
5511
- });
5512
- const filePath = join3(projectDir, entry.path);
5513
- let body = await readFile2(filePath);
5514
- if (entry.isCompressed) {
5515
- body = await compressBuffer(body);
5516
- }
5517
- await fetch(urlResponse.url, {
5518
- method: "PUT",
5519
- body,
5520
- headers: { "Content-Type": "application/octet-stream" }
5614
+ manifest: completeManifest
5521
5615
  });
5522
- uploaded++;
5523
5616
  } catch {
5524
5617
  }
5525
- uploadSpinner.stop();
5526
- }
5527
- try {
5528
- await client.post("/api/sync/push/complete", {
5529
- syncEventId: prepareResponse.syncEventId,
5530
- manifest
5531
- });
5532
- } 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
+ }
5533
5625
  }
5534
- return { uploaded, projectId: resolvedProjectId, failed: false };
5535
5626
  }
5536
- async function pullPhase(client, deviceId, projectPath, projectDir, projectId, options) {
5537
- const spinner = createSpinner("Preparing pull...").start();
5627
+ async function crossMachineBundlePull(client, deviceId, projectPath, fromDeviceId, projectId, foreignEncodedDir, projectsDir, options) {
5628
+ const spinner = createSpinner("Preparing download...").start();
5538
5629
  let prepareResponse;
5539
5630
  try {
5540
- prepareResponse = await client.post("/api/sync/pull/prepare", {
5631
+ prepareResponse = await client.post("/api/sync/pull/bundle/prepare", {
5541
5632
  deviceId,
5542
5633
  projectPath,
5634
+ fromDeviceId,
5543
5635
  projectId
5544
5636
  });
5545
5637
  } catch (err) {
5546
5638
  spinner.stop();
5547
5639
  if (err instanceof AuthExpiredError) throw err;
5548
5640
  printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5549
- return { downloaded: 0, failed: true };
5641
+ return { fileCount: 0, failed: true };
5550
5642
  }
5551
- spinner.stop();
5552
- const filesToDownload = prepareResponse.filesToDownload;
5553
- if (filesToDownload.length === 0) return { downloaded: 0, failed: false };
5554
- printInfo(`${brand(String(filesToDownload.length))} files to pull`);
5643
+ if (!prepareResponse.downloadUrl || !prepareResponse.bundleSize) {
5644
+ spinner.stop();
5645
+ return { fileCount: 0, failed: false };
5646
+ }
5647
+ const fileCount = prepareResponse.manifest.length;
5555
5648
  if (options.dryRun) {
5649
+ spinner.stop();
5650
+ printInfo(`${brand(String(fileCount))} files to pull`);
5556
5651
  if (options.verbose) {
5557
- for (const entry of filesToDownload) {
5652
+ for (const entry of prepareResponse.manifest) {
5558
5653
  console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5559
5654
  }
5560
5655
  }
5561
- return { downloaded: filesToDownload.length, failed: false };
5656
+ return { fileCount, failed: false };
5562
5657
  }
5563
- let downloaded = 0;
5564
- for (const entry of filesToDownload) {
5565
- const dlSpinner = createSpinner(
5566
- `${formatProgress(downloaded + 1, filesToDownload.length)} ${entry.path}`
5567
- ).start();
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);
5568
5668
  try {
5569
- const urlResponse = await client.post("/api/sync/pull/download-url", {
5669
+ const completeManifest = await walkDirectory(targetDir);
5670
+ await client.post("/api/sync/pull/bundle/complete", {
5570
5671
  syncEventId: prepareResponse.syncEventId,
5571
- key: entry.path
5672
+ manifest: completeManifest
5572
5673
  });
5573
- const response = await fetch(urlResponse.url);
5574
- let data = Buffer.from(await response.arrayBuffer());
5575
- if (entry.isCompressed) {
5576
- data = await decompressBuffer(data);
5577
- }
5578
- const outputPath = join3(projectDir, entry.path);
5579
- await mkdir2(dirname2(outputPath), { recursive: true });
5580
- await writeFile2(outputPath, data);
5581
- downloaded++;
5582
5674
  } catch {
5583
5675
  }
5584
- dlSpinner.stop();
5585
- }
5586
- try {
5587
- await client.post("/api/sync/pull/complete", {
5588
- syncEventId: prepareResponse.syncEventId,
5589
- manifest: filesToDownload
5590
- });
5591
- } 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
+ }
5592
5683
  }
5593
- return { downloaded, failed: false };
5594
5684
  }
5595
5685
  function syncCommand() {
5596
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) => {
@@ -5606,7 +5696,7 @@ var init_sync = __esm({
5606
5696
  init_api_client();
5607
5697
  init_sync_engine();
5608
5698
  init_progress();
5609
- init_compress();
5699
+ init_bundle();
5610
5700
  import_utils3 = __toESM(require_dist(), 1);
5611
5701
  init_theme();
5612
5702
  }