@claude-sync/cli 0.1.14 → 0.1.16

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.
@@ -4346,7 +4346,7 @@ var require_validation = __commonJS({
4346
4346
  "use strict";
4347
4347
  init_esm_shims();
4348
4348
  Object.defineProperty(exports, "__esModule", { value: true });
4349
- exports.createFeatureRequestCommentSchema = exports.updateFeatureRequestStatusSchema = exports.updateFeatureRequestSchema = exports.createFeatureRequestSchema = exports.featureRequestStatusSchema = exports.featureRequestTypeSchema = exports.updateProfileSchema = exports.oauthExchangeSchema = exports.refreshTokenSchema = exports.syncCompleteSchema = exports.downloadUrlSchema = exports.uploadUrlSchema = exports.pullRequestSchema = exports.pushManifestSchema = exports.updateDeviceSchema = exports.createDeviceSchema = exports.registerSchema = exports.loginSchema = void 0;
4349
+ exports.createFeatureRequestCommentSchema = exports.updateFeatureRequestStatusSchema = exports.updateFeatureRequestSchema = exports.createFeatureRequestSchema = exports.featureRequestStatusSchema = exports.featureRequestTypeSchema = exports.updateProfileSchema = exports.resetPasswordSchema = exports.forgotPasswordSchema = exports.oauthExchangeSchema = exports.refreshTokenSchema = exports.syncCompleteSchema = exports.downloadUrlSchema = exports.uploadUrlSchema = exports.pullRequestSchema = exports.pushManifestSchema = exports.updateDeviceSchema = exports.createDeviceSchema = exports.registerSchema = exports.loginSchema = void 0;
4350
4350
  var zod_1 = require_zod();
4351
4351
  exports.loginSchema = zod_1.z.object({
4352
4352
  email: zod_1.z.string().email(),
@@ -4412,6 +4412,13 @@ var require_validation = __commonJS({
4412
4412
  provider: zod_1.z.enum(["github", "google"]),
4413
4413
  accessToken: zod_1.z.string().min(1)
4414
4414
  });
4415
+ exports.forgotPasswordSchema = zod_1.z.object({
4416
+ email: zod_1.z.string().email()
4417
+ });
4418
+ exports.resetPasswordSchema = zod_1.z.object({
4419
+ token: zod_1.z.string().min(1),
4420
+ password: zod_1.z.string().min(8)
4421
+ });
4415
4422
  exports.updateProfileSchema = zod_1.z.object({
4416
4423
  name: zod_1.z.string().min(1).max(100).optional(),
4417
4424
  avatarUrl: zod_1.z.string().url().nullable().optional()
@@ -4697,16 +4704,49 @@ function formatBytes(bytes) {
4697
4704
  const i = Math.floor(Math.log(bytes) / Math.log(k));
4698
4705
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
4699
4706
  }
4700
- function formatProgress(current, total) {
4701
- const percentage = total > 0 ? Math.round(current / total * 100) : 0;
4702
- return `${current}/${total} (${percentage}%)`;
4707
+ function createBar(percent, width = 20) {
4708
+ const filled = Math.round(percent / 100 * width);
4709
+ const empty = width - filled;
4710
+ return dimColor("[") + successColor("\u2588".repeat(filled)) + dimColor("\u2591".repeat(empty) + "]");
4711
+ }
4712
+ function formatSpeed(bytesPerSecond) {
4713
+ return `${formatBytes(bytesPerSecond)}/s`;
4714
+ }
4715
+ function createTransferProgress(initialLabel, totalBytes, spinner) {
4716
+ const startTime = Date.now();
4717
+ let lastUpdate = 0;
4718
+ let currentLabel = initialLabel;
4719
+ return {
4720
+ update(bytesTransferred) {
4721
+ const now = Date.now();
4722
+ if (now - lastUpdate < 50) return;
4723
+ lastUpdate = now;
4724
+ const percent = Math.round(bytesTransferred / totalBytes * 100);
4725
+ const elapsed = (now - startTime) / 1e3;
4726
+ const speed = elapsed > 0 ? bytesTransferred / elapsed : 0;
4727
+ const bar = createBar(percent);
4728
+ spinner.text = `${currentLabel} ${bar} ${dimColor(`${percent}%`)} ${dimColor("\u2022")} ${formatBytes(bytesTransferred)}/${formatBytes(totalBytes)} ${dimColor("\u2022")} ${formatSpeed(speed)}`;
4729
+ },
4730
+ setLabel(label) {
4731
+ currentLabel = label;
4732
+ },
4733
+ finish(message) {
4734
+ if (message) {
4735
+ spinner.succeed(message);
4736
+ } else {
4737
+ spinner.stop();
4738
+ }
4739
+ }
4740
+ };
4703
4741
  }
4704
- var brandColor;
4742
+ var brandColor, dimColor, successColor;
4705
4743
  var init_progress = __esm({
4706
4744
  "src/lib/progress.ts"() {
4707
4745
  "use strict";
4708
4746
  init_esm_shims();
4709
4747
  brandColor = chalk.hex("#8a8cdd");
4748
+ dimColor = chalk.dim;
4749
+ successColor = chalk.hex("#22c55e");
4710
4750
  }
4711
4751
  });
4712
4752
 
@@ -5127,32 +5167,105 @@ var init_sync_engine = __esm({
5127
5167
  }
5128
5168
  });
5129
5169
 
5130
- // src/lib/compress.ts
5170
+ // src/lib/bundle.ts
5131
5171
  import { createGzip, createGunzip } from "zlib";
5132
5172
  import { createReadStream, createWriteStream } from "fs";
5173
+ import { readFile as readFile2, stat as stat2, mkdir as mkdir2 } from "fs/promises";
5174
+ import { join as join3, dirname as dirname2 } from "path";
5133
5175
  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);
5176
+ import { pack, extract } from "tar-stream";
5177
+ async function createBundle(sourceDir, files, outputPath, onProgress) {
5178
+ const totalBytes = files.reduce((sum, f) => sum + f.size, 0);
5179
+ let processedBytes = 0;
5180
+ const archive = pack();
5181
+ const gzip = createGzip({ level: 6 });
5182
+ const output = createWriteStream(outputPath);
5183
+ const pipelinePromise = pipeline(archive, gzip, output);
5184
+ for (const file of files) {
5185
+ const filePath = join3(sourceDir, file.path);
5186
+ const content = await readFile2(filePath);
5187
+ archive.entry({ name: file.path, size: content.length }, content);
5188
+ processedBytes += file.size;
5189
+ onProgress?.(processedBytes, totalBytes, "compressing");
5190
+ }
5191
+ archive.finalize();
5192
+ await pipelinePromise;
5193
+ const outputStat = await stat2(outputPath);
5194
+ return {
5195
+ size: outputStat.size,
5196
+ fileCount: files.length,
5197
+ originalSize: totalBytes
5198
+ };
5199
+ }
5200
+ async function extractBundle(bundlePath, targetDir, onProgress) {
5201
+ await mkdir2(targetDir, { recursive: true });
5202
+ const extractor = extract();
5203
+ const gunzip = createGunzip();
5204
+ const input = createReadStream(bundlePath);
5205
+ let count = 0;
5206
+ extractor.on("entry", (header, stream, next) => {
5207
+ count++;
5208
+ onProgress?.(count, header.name);
5209
+ const outputPath = join3(targetDir, header.name);
5210
+ const dir = dirname2(outputPath);
5211
+ mkdir2(dir, { recursive: true }).then(() => {
5212
+ const fileOutput = createWriteStream(outputPath);
5213
+ stream.pipe(fileOutput);
5214
+ fileOutput.on("finish", next);
5215
+ fileOutput.on("error", next);
5216
+ }).catch(next);
5142
5217
  });
5218
+ await pipeline(input, gunzip, extractor);
5219
+ return count;
5143
5220
  }
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);
5221
+ async function streamUpload(url, filePath, onProgress) {
5222
+ const fileStat = await stat2(filePath);
5223
+ const totalSize = fileStat.size;
5224
+ const fileStream = createReadStream(filePath);
5225
+ let uploaded = 0;
5226
+ const chunks = [];
5227
+ for await (const chunk of fileStream) {
5228
+ chunks.push(chunk);
5229
+ uploaded += chunk.length;
5230
+ onProgress?.(uploaded, totalSize);
5231
+ }
5232
+ const body = Buffer.concat(chunks);
5233
+ const response = await fetch(url, {
5234
+ method: "PUT",
5235
+ body,
5236
+ headers: {
5237
+ "Content-Type": "application/gzip",
5238
+ "Content-Length": String(totalSize)
5239
+ }
5152
5240
  });
5241
+ if (!response.ok) {
5242
+ throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
5243
+ }
5153
5244
  }
5154
- var init_compress = __esm({
5155
- "src/lib/compress.ts"() {
5245
+ async function streamDownload(url, outputPath, expectedSize, onProgress) {
5246
+ const response = await fetch(url);
5247
+ if (!response.ok) {
5248
+ throw new Error(`Download failed: ${response.status} ${response.statusText}`);
5249
+ }
5250
+ const reader = response.body?.getReader();
5251
+ if (!reader) throw new Error("No response body");
5252
+ const output = createWriteStream(outputPath);
5253
+ let downloaded = 0;
5254
+ while (true) {
5255
+ const { done, value } = await reader.read();
5256
+ if (done) break;
5257
+ output.write(value);
5258
+ downloaded += value.length;
5259
+ onProgress?.(downloaded, expectedSize);
5260
+ }
5261
+ await new Promise((resolve, reject) => {
5262
+ output.end();
5263
+ output.on("finish", resolve);
5264
+ output.on("error", reject);
5265
+ });
5266
+ }
5267
+ var init_bundle = __esm({
5268
+ "src/lib/bundle.ts"() {
5156
5269
  "use strict";
5157
5270
  init_esm_shims();
5158
5271
  }
@@ -5166,9 +5279,9 @@ __export(sync_exports, {
5166
5279
  });
5167
5280
  import { Command as Command4 } from "commander";
5168
5281
  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";
5282
+ import { mkdir as mkdir3, lstat as lstat2, symlink, unlink, rename, rm } from "fs/promises";
5283
+ import { join as join4 } from "path";
5284
+ import { homedir as homedir4, tmpdir } from "os";
5172
5285
  async function runSync(options) {
5173
5286
  printIntro("Sync");
5174
5287
  const config = await loadConfig();
@@ -5183,11 +5296,11 @@ async function runSync(options) {
5183
5296
  const cwd = process.cwd();
5184
5297
  const ctx = await resolveProjectState(cwd);
5185
5298
  const client = new ApiClient(config.apiUrl);
5186
- const projectsDir = join3(homedir4(), import_utils3.CLAUDE_DIR, import_utils3.PROJECTS_DIR);
5299
+ const projectsDir = join4(homedir4(), import_utils3.CLAUDE_DIR, import_utils3.PROJECTS_DIR);
5187
5300
  const projectLink = await loadProjectLink(cwd);
5188
5301
  let projectDir = ctx.projectDir;
5189
5302
  if (ctx.state === "symlink" && ctx.symlinkTarget) {
5190
- projectDir = join3(projectsDir, ctx.symlinkTarget);
5303
+ projectDir = join4(projectsDir, ctx.symlinkTarget);
5191
5304
  }
5192
5305
  printInfo(`Project: ${brand(cwd)}`);
5193
5306
  if (ctx.state === "symlink") {
@@ -5203,7 +5316,6 @@ async function runSync(options) {
5203
5316
  if (result === "pulled") {
5204
5317
  const finalManifest2 = await buildProjectManifest(cwd);
5205
5318
  await saveProjectManifest(cwd, finalManifest2);
5206
- printSuccess("Sessions synced from remote device.");
5207
5319
  printOutro("Done");
5208
5320
  return;
5209
5321
  }
@@ -5216,13 +5328,13 @@ async function runSync(options) {
5216
5328
  printOutro("Done");
5217
5329
  return;
5218
5330
  }
5219
- let pushResult = { uploaded: 0, projectId: projectLink.projectId, failed: false };
5220
- let pullResult = { downloaded: 0, failed: false };
5331
+ let pushResult = { fileCount: 0, projectId: projectLink.projectId, failed: false };
5332
+ let pullResult = { fileCount: 0, failed: false };
5221
5333
  if (manifest.length > 0) {
5222
- pushResult = await pushPhase(client, config.deviceId, cwd, manifest, projectDir, projectLink.projectId, options);
5334
+ pushResult = await bundlePushPhase(client, config.deviceId, cwd, manifest, projectDir, projectLink.projectId, options);
5223
5335
  }
5224
5336
  if (!pushResult.failed) {
5225
- pullResult = await pullPhase(client, config.deviceId, cwd, projectDir, projectLink.projectId, options);
5337
+ pullResult = await bundlePullPhase(client, config.deviceId, cwd, projectDir, projectLink.projectId, options);
5226
5338
  }
5227
5339
  if (pushResult.failed && pullResult.failed) {
5228
5340
  printOutro("Sync failed.");
@@ -5230,11 +5342,11 @@ async function runSync(options) {
5230
5342
  }
5231
5343
  const finalManifest = await buildProjectManifest(cwd);
5232
5344
  await saveProjectManifest(cwd, finalManifest);
5233
- if (pushResult.uploaded > 0 || pullResult.downloaded > 0) {
5345
+ if (pushResult.fileCount > 0 || pullResult.fileCount > 0) {
5234
5346
  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(", ")}`);
5347
+ if (pushResult.fileCount > 0) parts.push(`${success(`${pushResult.fileCount}`)} pushed`);
5348
+ if (pullResult.fileCount > 0) parts.push(`${success(`${pullResult.fileCount}`)} pulled`);
5349
+ printSuccess(`Synced: ${parts.join(", ")} files`);
5238
5350
  } else if (!pushResult.failed && !pullResult.failed) {
5239
5351
  printSuccess("Everything is up to date.");
5240
5352
  }
@@ -5282,15 +5394,12 @@ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, pr
5282
5394
  });
5283
5395
  if (isCancel3(choice)) return "cancelled";
5284
5396
  if (choice === "push") {
5285
- const result = await pushPhase(client, config.deviceId, cwd, manifest, projectDir, void 0, options);
5286
- if (result.failed) {
5397
+ const result2 = await bundlePushPhase(client, config.deviceId, cwd, manifest, projectDir, void 0, options);
5398
+ if (result2.failed) {
5287
5399
  return "cancelled";
5288
5400
  }
5289
- if (result.uploaded > 0) {
5290
- printSuccess(`${success(`+${result.uploaded}`)} files pushed`);
5291
- }
5292
5401
  await saveProjectLink(cwd, {
5293
- projectId: result.projectId,
5402
+ projectId: result2.projectId,
5294
5403
  foreignEncodedDir: ctx.encodedPath,
5295
5404
  linkedAt: (/* @__PURE__ */ new Date()).toISOString()
5296
5405
  });
@@ -5334,7 +5443,7 @@ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, pr
5334
5443
  if (existingCheck === "cancelled") {
5335
5444
  return "cancelled";
5336
5445
  }
5337
- const downloaded = await crossMachinePull(
5446
+ const result = await crossMachineBundlePull(
5338
5447
  client,
5339
5448
  config.deviceId,
5340
5449
  cwd,
@@ -5344,7 +5453,7 @@ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, pr
5344
5453
  projectsDir,
5345
5454
  options
5346
5455
  );
5347
- if (downloaded === 0 && !options.dryRun) {
5456
+ if (result.fileCount === 0 && !options.dryRun) {
5348
5457
  printInfo("No files to download.");
5349
5458
  return "cancelled";
5350
5459
  }
@@ -5355,13 +5464,12 @@ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, pr
5355
5464
  foreignEncodedDir: selectedProject.encodedDir,
5356
5465
  linkedAt: (/* @__PURE__ */ new Date()).toISOString()
5357
5466
  });
5358
- printSuccess(`${success(`+${downloaded}`)} files pulled`);
5359
5467
  printInfo(`Symlink: ${dim(ctx.encodedPath)} \u2192 ${dim(selectedProject.encodedDir)}`);
5360
5468
  }
5361
5469
  return "pulled";
5362
5470
  }
5363
5471
  async function handleExistingSessionDir(projectsDir, localEncoded) {
5364
- const symlinkPath = join3(projectsDir, localEncoded);
5472
+ const symlinkPath = join4(projectsDir, localEncoded);
5365
5473
  try {
5366
5474
  const stats = await lstat2(symlinkPath);
5367
5475
  if (stats.isSymbolicLink()) {
@@ -5398,8 +5506,8 @@ async function handleExistingSessionDir(projectsDir, localEncoded) {
5398
5506
  return "continue";
5399
5507
  }
5400
5508
  async function createProjectSymlink(projectsDir, localEncoded, foreignEncoded) {
5401
- const symlinkPath = join3(projectsDir, localEncoded);
5402
- await mkdir2(projectsDir, { recursive: true });
5509
+ const symlinkPath = join4(projectsDir, localEncoded);
5510
+ await mkdir3(projectsDir, { recursive: true });
5403
5511
  try {
5404
5512
  const stats = await lstat2(symlinkPath);
5405
5513
  if (stats.isSymbolicLink()) {
@@ -5409,190 +5517,177 @@ async function createProjectSymlink(projectsDir, localEncoded, foreignEncoded) {
5409
5517
  }
5410
5518
  await symlink(foreignEncoded, symlinkPath);
5411
5519
  }
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;
5520
+ async function bundlePushPhase(client, deviceId, projectPath, manifest, projectDir, projectId, options) {
5521
+ if (manifest.length === 0) {
5522
+ return { fileCount: 0, projectId: projectId || "", failed: false };
5427
5523
  }
5428
- spinner.stop();
5429
- const filesToDownload = prepareResponse.filesToDownload;
5430
- if (filesToDownload.length === 0) return 0;
5431
- printInfo(`${brand(String(filesToDownload.length))} files to pull`);
5524
+ const totalBytes = manifest.reduce((sum, f) => sum + f.size, 0);
5525
+ printInfo(`${brand(String(manifest.length))} files ${dim(`(${formatBytes(totalBytes)})`)} to push`);
5432
5526
  if (options.dryRun) {
5433
5527
  if (options.verbose) {
5434
- for (const entry of filesToDownload) {
5435
- console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5528
+ for (const entry of manifest) {
5529
+ console.log(` ${success("+")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5436
5530
  }
5437
5531
  }
5438
- return filesToDownload.length;
5532
+ return { fileCount: manifest.length, projectId: projectId || "", failed: false };
5439
5533
  }
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();
5534
+ const tempBundle = join4(tmpdir(), `claude-sync-bundle-${Date.now()}.tar.gz`);
5535
+ try {
5536
+ const spinner = createSpinner("Syncing...").start();
5537
+ const bundleResult = await createBundle(projectDir, manifest, tempBundle, (bytes, total, phase) => {
5538
+ if (phase === "compressing") {
5539
+ const percent = Math.round(bytes / total * 100);
5540
+ spinner.text = `Compressing files... ${dim(`${percent}%`)}`;
5541
+ }
5542
+ });
5543
+ const progress = createTransferProgress("Uploading", bundleResult.size, spinner);
5544
+ let prepareResponse;
5545
+ try {
5546
+ prepareResponse = await client.post("/api/sync/push/bundle/prepare", {
5547
+ deviceId,
5548
+ projectPath,
5549
+ projectId,
5550
+ manifest,
5551
+ bundleSize: bundleResult.size
5552
+ });
5553
+ } catch (err) {
5554
+ spinner.stop();
5555
+ if (err instanceof AuthExpiredError) throw err;
5556
+ printError(`Push failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5557
+ return { fileCount: 0, projectId: projectId || "", failed: true };
5558
+ }
5559
+ await streamUpload(prepareResponse.uploadUrl, tempBundle, (bytes, total) => {
5560
+ progress.update(bytes);
5561
+ });
5446
5562
  try {
5447
- const urlResponse = await client.post("/api/sync/pull/download-url", {
5563
+ await client.post("/api/sync/push/bundle/complete", {
5448
5564
  syncEventId: prepareResponse.syncEventId,
5449
- key: entry.path
5565
+ manifest
5450
5566
  });
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
5567
  } catch {
5461
5568
  }
5462
- dlSpinner.stop();
5463
- }
5464
- try {
5465
- const completeManifest = await walkDirectory(targetDir);
5466
- await client.post("/api/sync/pull/complete", {
5467
- syncEventId: prepareResponse.syncEventId,
5468
- manifest: completeManifest
5469
- });
5470
- } catch {
5569
+ const compressionRatio = Math.round((1 - bundleResult.size / bundleResult.originalSize) * 100);
5570
+ progress.finish(`Pushed ${manifest.length} files ${dim(`(${formatBytes(bundleResult.size)}, ${compressionRatio}% smaller)`)}`);
5571
+ return { fileCount: manifest.length, projectId: prepareResponse.projectId, failed: false };
5572
+ } finally {
5573
+ try {
5574
+ await rm(tempBundle, { force: true });
5575
+ } catch {
5576
+ }
5471
5577
  }
5472
- return downloaded;
5473
5578
  }
5474
- async function pushPhase(client, deviceId, projectPath, manifest, projectDir, projectId, options) {
5475
- const spinner = createSpinner("Preparing push...").start();
5579
+ async function bundlePullPhase(client, deviceId, projectPath, projectDir, projectId, options) {
5580
+ const spinner = createSpinner("Checking for updates...").start();
5476
5581
  let prepareResponse;
5477
5582
  try {
5478
- prepareResponse = await client.post("/api/sync/push/prepare", {
5583
+ prepareResponse = await client.post("/api/sync/pull/bundle/prepare", {
5479
5584
  deviceId,
5480
5585
  projectPath,
5481
- projectId,
5482
- manifest
5586
+ projectId
5483
5587
  });
5484
5588
  } catch (err) {
5485
5589
  spinner.stop();
5486
5590
  if (err instanceof AuthExpiredError) throw err;
5487
- printError(`Push failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5488
- return { uploaded: 0, projectId: projectId || "", failed: true };
5591
+ printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5592
+ return { fileCount: 0, failed: true };
5489
5593
  }
5490
- spinner.stop();
5491
- const resolvedProjectId = prepareResponse.projectId;
5492
- const filesToUpload = prepareResponse.filesToUpload;
5493
- if (filesToUpload.length === 0) return { uploaded: 0, projectId: resolvedProjectId, failed: false };
5494
- printInfo(`${brand(String(filesToUpload.length))} files to push`);
5594
+ if (!prepareResponse.downloadUrl || !prepareResponse.bundleSize) {
5595
+ spinner.stop();
5596
+ return { fileCount: 0, failed: false };
5597
+ }
5598
+ const fileCount = prepareResponse.manifest.length;
5495
5599
  if (options.dryRun) {
5600
+ spinner.stop();
5601
+ printInfo(`${brand(String(fileCount))} files to pull`);
5496
5602
  if (options.verbose) {
5497
- for (const entry of filesToUpload) {
5498
- console.log(` ${success("+")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5603
+ for (const entry of prepareResponse.manifest) {
5604
+ console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5499
5605
  }
5500
5606
  }
5501
- return { uploaded: filesToUpload.length, projectId: resolvedProjectId, failed: false };
5607
+ return { fileCount, failed: false };
5502
5608
  }
5503
- let uploaded = 0;
5504
- for (const entry of filesToUpload) {
5505
- const uploadSpinner = createSpinner(
5506
- `${formatProgress(uploaded + 1, filesToUpload.length)} ${entry.path}`
5507
- ).start();
5609
+ const tempBundle = join4(tmpdir(), `claude-sync-bundle-${Date.now()}.tar.gz`);
5610
+ try {
5611
+ const progress = createTransferProgress("Downloading", prepareResponse.bundleSize, spinner);
5612
+ await streamDownload(prepareResponse.downloadUrl, tempBundle, prepareResponse.bundleSize, (bytes) => {
5613
+ progress.update(bytes);
5614
+ });
5615
+ spinner.text = "Extracting files...";
5616
+ const extractedCount = await extractBundle(tempBundle, projectDir);
5508
5617
  try {
5509
- const urlResponse = await client.post("/api/sync/push/upload-url", {
5618
+ const completeManifest = await walkDirectory(projectDir);
5619
+ await client.post("/api/sync/pull/bundle/complete", {
5510
5620
  syncEventId: prepareResponse.syncEventId,
5511
- key: entry.path
5621
+ manifest: completeManifest
5512
5622
  });
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" }
5522
- });
5523
- uploaded++;
5524
5623
  } catch {
5525
5624
  }
5526
- uploadSpinner.stop();
5527
- }
5528
- try {
5529
- await client.post("/api/sync/push/complete", {
5530
- syncEventId: prepareResponse.syncEventId,
5531
- manifest
5532
- });
5533
- } catch {
5625
+ progress.finish(`Pulled ${extractedCount} files ${dim(`(${formatBytes(prepareResponse.bundleSize)})`)}`);
5626
+ return { fileCount: extractedCount, failed: false };
5627
+ } finally {
5628
+ try {
5629
+ await rm(tempBundle, { force: true });
5630
+ } catch {
5631
+ }
5534
5632
  }
5535
- return { uploaded, projectId: resolvedProjectId, failed: false };
5536
5633
  }
5537
- async function pullPhase(client, deviceId, projectPath, projectDir, projectId, options) {
5538
- const spinner = createSpinner("Preparing pull...").start();
5634
+ async function crossMachineBundlePull(client, deviceId, projectPath, fromDeviceId, projectId, foreignEncodedDir, projectsDir, options) {
5635
+ const spinner = createSpinner("Preparing download...").start();
5539
5636
  let prepareResponse;
5540
5637
  try {
5541
- prepareResponse = await client.post("/api/sync/pull/prepare", {
5638
+ prepareResponse = await client.post("/api/sync/pull/bundle/prepare", {
5542
5639
  deviceId,
5543
5640
  projectPath,
5641
+ fromDeviceId,
5544
5642
  projectId
5545
5643
  });
5546
5644
  } catch (err) {
5547
5645
  spinner.stop();
5548
5646
  if (err instanceof AuthExpiredError) throw err;
5549
5647
  printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5550
- return { downloaded: 0, failed: true };
5648
+ return { fileCount: 0, failed: true };
5551
5649
  }
5552
- spinner.stop();
5553
- const filesToDownload = prepareResponse.filesToDownload;
5554
- if (filesToDownload.length === 0) return { downloaded: 0, failed: false };
5555
- printInfo(`${brand(String(filesToDownload.length))} files to pull`);
5650
+ if (!prepareResponse.downloadUrl || !prepareResponse.bundleSize) {
5651
+ spinner.stop();
5652
+ return { fileCount: 0, failed: false };
5653
+ }
5654
+ const fileCount = prepareResponse.manifest.length;
5556
5655
  if (options.dryRun) {
5656
+ spinner.stop();
5657
+ printInfo(`${brand(String(fileCount))} files to pull`);
5557
5658
  if (options.verbose) {
5558
- for (const entry of filesToDownload) {
5659
+ for (const entry of prepareResponse.manifest) {
5559
5660
  console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5560
5661
  }
5561
5662
  }
5562
- return { downloaded: filesToDownload.length, failed: false };
5663
+ return { fileCount, failed: false };
5563
5664
  }
5564
- let downloaded = 0;
5565
- for (const entry of filesToDownload) {
5566
- const dlSpinner = createSpinner(
5567
- `${formatProgress(downloaded + 1, filesToDownload.length)} ${entry.path}`
5568
- ).start();
5665
+ const targetDir = join4(projectsDir, foreignEncodedDir);
5666
+ const tempBundle = join4(tmpdir(), `claude-sync-bundle-${Date.now()}.tar.gz`);
5667
+ try {
5668
+ const progress = createTransferProgress("Downloading", prepareResponse.bundleSize, spinner);
5669
+ await streamDownload(prepareResponse.downloadUrl, tempBundle, prepareResponse.bundleSize, (bytes) => {
5670
+ progress.update(bytes);
5671
+ });
5672
+ spinner.text = "Extracting files...";
5673
+ await mkdir3(targetDir, { recursive: true });
5674
+ const extractedCount = await extractBundle(tempBundle, targetDir);
5569
5675
  try {
5570
- const urlResponse = await client.post("/api/sync/pull/download-url", {
5676
+ const completeManifest = await walkDirectory(targetDir);
5677
+ await client.post("/api/sync/pull/bundle/complete", {
5571
5678
  syncEventId: prepareResponse.syncEventId,
5572
- key: entry.path
5679
+ manifest: completeManifest
5573
5680
  });
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
5681
  } catch {
5584
5682
  }
5585
- dlSpinner.stop();
5586
- }
5587
- try {
5588
- const completeManifest = await walkDirectory(projectDir);
5589
- await client.post("/api/sync/pull/complete", {
5590
- syncEventId: prepareResponse.syncEventId,
5591
- manifest: completeManifest
5592
- });
5593
- } catch {
5683
+ progress.finish(`Pulled ${extractedCount} files ${dim(`(${formatBytes(prepareResponse.bundleSize)})`)}`);
5684
+ return { fileCount: extractedCount, failed: false };
5685
+ } finally {
5686
+ try {
5687
+ await rm(tempBundle, { force: true });
5688
+ } catch {
5689
+ }
5594
5690
  }
5595
- return { downloaded, failed: false };
5596
5691
  }
5597
5692
  function syncCommand() {
5598
5693
  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 +5703,7 @@ var init_sync = __esm({
5608
5703
  init_api_client();
5609
5704
  init_sync_engine();
5610
5705
  init_progress();
5611
- init_compress();
5706
+ init_bundle();
5612
5707
  import_utils3 = __toESM(require_dist(), 1);
5613
5708
  init_theme();
5614
5709
  }