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