@claude-sync/cli 0.1.16 → 0.1.18

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
@@ -54,12 +54,12 @@ var require_path_encoding = __commonJS({
54
54
  init_esm_shims();
55
55
  Object.defineProperty(exports, "__esModule", { value: true });
56
56
  exports.ROOT_PATTERN = void 0;
57
- exports.encodePath = encodePath3;
57
+ exports.encodePath = encodePath5;
58
58
  exports.decodeSuffix = decodeSuffix;
59
59
  exports.decodeRoot = decodeRoot;
60
60
  exports.extractCanonicalPath = extractCanonicalPath;
61
61
  exports.ROOT_PATTERN = /^-(home-[^-]+|Users-[^-]+)/;
62
- function encodePath3(p) {
62
+ function encodePath5(p) {
63
63
  return p.replace(/\//g, "-").replace(/\./g, "-");
64
64
  }
65
65
  function decodeSuffix(suffix, projectPath) {
@@ -79,7 +79,7 @@ var require_path_encoding = __commonJS({
79
79
  }
80
80
  function extractCanonicalPath(fullPath, homeDir) {
81
81
  const relative = fullPath.startsWith(homeDir) ? fullPath.slice(homeDir.length) : fullPath;
82
- return encodePath3(relative.startsWith("/") ? relative : "/" + relative);
82
+ return encodePath5(relative.startsWith("/") ? relative : "/" + relative);
83
83
  }
84
84
  }
85
85
  });
@@ -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.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;
4348
+ exports.updateCollaboratorRoleSchema = exports.inviteCollaboratorSchema = exports.editableRoleSchema = exports.projectRoleSchema = exports.createFeatureRequestCommentSchema = exports.updateFeatureRequestStatusSchema = exports.updateFeatureRequestSchema = exports.createFeatureRequestSchema = exports.featureRequestStatusSchema = exports.featureRequestTypeSchema = exports.updateProfileSchema = exports.resetPasswordSchema = exports.forgotPasswordSchema = exports.oauthExchangeSchema = exports.refreshTokenSchema = exports.pushBundleCompleteSchema = exports.pushBundlePrepareSchema = exports.manifestEntrySchema = 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(),
@@ -4404,6 +4404,27 @@ var require_validation = __commonJS({
4404
4404
  isCompressed: zod_1.z.boolean()
4405
4405
  }))
4406
4406
  });
4407
+ exports.manifestEntrySchema = zod_1.z.object({
4408
+ path: zod_1.z.string(),
4409
+ hash: zod_1.z.string(),
4410
+ size: zod_1.z.number(),
4411
+ modifiedAt: zod_1.z.string(),
4412
+ isCompressed: zod_1.z.boolean()
4413
+ });
4414
+ exports.pushBundlePrepareSchema = zod_1.z.object({
4415
+ deviceId: zod_1.z.string().uuid(),
4416
+ projectPath: zod_1.z.string().min(1),
4417
+ projectId: zod_1.z.string().uuid().optional(),
4418
+ manifest: zod_1.z.array(exports.manifestEntrySchema),
4419
+ bundleSize: zod_1.z.number().positive(),
4420
+ localVersion: zod_1.z.number().int().min(0),
4421
+ message: zod_1.z.string().max(500).optional()
4422
+ });
4423
+ exports.pushBundleCompleteSchema = zod_1.z.object({
4424
+ syncEventId: zod_1.z.string().uuid(),
4425
+ manifest: zod_1.z.array(exports.manifestEntrySchema),
4426
+ message: zod_1.z.string().min(1).max(500)
4427
+ });
4407
4428
  exports.refreshTokenSchema = zod_1.z.object({
4408
4429
  refreshToken: zod_1.z.string()
4409
4430
  });
@@ -4439,6 +4460,15 @@ var require_validation = __commonJS({
4439
4460
  exports.createFeatureRequestCommentSchema = zod_1.z.object({
4440
4461
  body: zod_1.z.string().min(1).max(2e3)
4441
4462
  });
4463
+ exports.projectRoleSchema = zod_1.z.enum(["owner", "editor", "viewer"]);
4464
+ exports.editableRoleSchema = zod_1.z.enum(["editor", "viewer"]);
4465
+ exports.inviteCollaboratorSchema = zod_1.z.object({
4466
+ email: zod_1.z.string().email(),
4467
+ role: exports.editableRoleSchema
4468
+ });
4469
+ exports.updateCollaboratorRoleSchema = zod_1.z.object({
4470
+ role: exports.editableRoleSchema
4471
+ });
4442
4472
  }
4443
4473
  });
4444
4474
 
@@ -4693,8 +4723,8 @@ var init_auth = __esm({
4693
4723
  // src/lib/progress.ts
4694
4724
  import ora from "ora";
4695
4725
  import chalk from "chalk";
4696
- function createSpinner(text4) {
4697
- return ora({ text: brandColor(text4), spinner: "dots", color: "magenta" });
4726
+ function createSpinner(text5) {
4727
+ return ora({ text: brandColor(text5), spinner: "dots", color: "magenta" });
4698
4728
  }
4699
4729
  function formatBytes(bytes) {
4700
4730
  if (bytes === 0) return "0 B";
@@ -4772,6 +4802,9 @@ function printSuccess(message) {
4772
4802
  function printError(message) {
4773
4803
  console.log(` ${error("\u2716")} ${message}`);
4774
4804
  }
4805
+ function printWarn(message) {
4806
+ console.log(` ${warn("\u25B2")} ${message}`);
4807
+ }
4775
4808
  function printInfo(message) {
4776
4809
  console.log(` ${brand("\u25CF")} ${message}`);
4777
4810
  }
@@ -5270,19 +5303,23 @@ var init_bundle = __esm({
5270
5303
  }
5271
5304
  });
5272
5305
 
5273
- // src/commands/sync.ts
5274
- var sync_exports = {};
5275
- __export(sync_exports, {
5276
- runSync: () => runSync,
5277
- syncCommand: () => syncCommand
5306
+ // src/commands/push.ts
5307
+ var push_exports = {};
5308
+ __export(push_exports, {
5309
+ pushCommand: () => pushCommand
5278
5310
  });
5279
5311
  import { Command as Command4 } from "commander";
5280
- import { select as select2, isCancel as isCancel3 } from "@clack/prompts";
5281
- import { mkdir as mkdir3, lstat as lstat2, symlink, unlink, rename, rm } from "fs/promises";
5312
+ import { text as text3, isCancel as isCancel3 } from "@clack/prompts";
5282
5313
  import { join as join4 } from "path";
5283
5314
  import { homedir as homedir4, tmpdir } from "os";
5284
- async function runSync(options) {
5285
- printIntro("Sync");
5315
+ import { rm } from "fs/promises";
5316
+ function pushCommand() {
5317
+ return new Command4("push").description("Push local changes to cloud").option("-m, --message <message>", "Commit message").option("--dry-run", "Show what would be pushed without pushing").option("--verbose", "Show detailed output").action(async (options) => {
5318
+ await runPush(options);
5319
+ });
5320
+ }
5321
+ async function runPush(options) {
5322
+ printIntro("Push");
5286
5323
  const config = await loadConfig();
5287
5324
  if (!isAuthenticated(config)) {
5288
5325
  printError("Not logged in. Run `claude-sync login` first.");
@@ -5302,275 +5339,927 @@ async function runSync(options) {
5302
5339
  projectDir = join4(projectsDir, ctx.symlinkTarget);
5303
5340
  }
5304
5341
  printInfo(`Project: ${brand(cwd)}`);
5305
- if (ctx.state === "symlink") {
5306
- printInfo(`Linked to: ${dim(ctx.symlinkTarget || "unknown")}`);
5307
- }
5308
5342
  const manifest = await buildProjectManifest(cwd);
5309
- if (!projectLink) {
5310
- const result = await handleFirstRun(client, config, cwd, ctx, manifest, projectDir, projectsDir, options);
5311
- if (result === "cancelled") {
5312
- printOutro("Cancelled.");
5313
- return;
5314
- }
5315
- if (result === "pulled") {
5316
- const finalManifest2 = await buildProjectManifest(cwd);
5317
- await saveProjectManifest(cwd, finalManifest2);
5318
- printOutro("Done");
5319
- return;
5320
- }
5321
- if (result === "pushed") {
5322
- const finalManifest2 = await buildProjectManifest(cwd);
5323
- await saveProjectManifest(cwd, finalManifest2);
5324
- printOutro("Done");
5325
- return;
5326
- }
5343
+ if (manifest.length === 0) {
5344
+ printInfo("No files to push.");
5327
5345
  printOutro("Done");
5328
5346
  return;
5329
5347
  }
5330
- let pushResult = { fileCount: 0, projectId: projectLink.projectId, failed: false };
5331
- let pullResult = { fileCount: 0, failed: false };
5332
- if (manifest.length > 0) {
5333
- pushResult = await bundlePushPhase(client, config.deviceId, cwd, manifest, projectDir, projectLink.projectId, options);
5334
- }
5335
- if (!pushResult.failed) {
5336
- pullResult = await bundlePullPhase(client, config.deviceId, cwd, projectDir, projectLink.projectId, options);
5337
- }
5338
- if (pushResult.failed && pullResult.failed) {
5339
- printOutro("Sync failed.");
5340
- return;
5341
- }
5342
- const finalManifest = await buildProjectManifest(cwd);
5343
- await saveProjectManifest(cwd, finalManifest);
5344
- if (pushResult.fileCount > 0 || pullResult.fileCount > 0) {
5345
- const parts = [];
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`);
5349
- } else if (!pushResult.failed && !pullResult.failed) {
5350
- printSuccess("Everything is up to date.");
5351
- }
5352
- printOutro("Done");
5353
- }
5354
- async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, projectsDir, options) {
5355
- printInfo(brand("First time syncing this project"));
5356
- const hasLocalFiles = ctx.state !== "none" && manifest.length > 0;
5357
- const devicesSpinner = createSpinner("Checking for other devices...").start();
5358
- let devices;
5359
- try {
5360
- devices = await client.get("/api/devices");
5361
- } catch (err) {
5362
- if (err instanceof AuthExpiredError) throw err;
5363
- devices = [];
5364
- }
5365
- devicesSpinner.stop();
5366
- const otherDevices = devices.filter((d) => d.id !== config.deviceId);
5367
- const menuOptions = [];
5368
- if (hasLocalFiles) {
5369
- menuOptions.push({
5370
- value: "push",
5371
- label: "Push to cloud",
5372
- hint: dim(`Upload ${manifest.length} local files`)
5373
- });
5374
- }
5375
- if (otherDevices.length > 0) {
5376
- menuOptions.push({
5377
- value: "continue",
5378
- label: "Continue from another machine",
5379
- hint: dim("Pull sessions from a device")
5380
- });
5381
- }
5382
- if (menuOptions.length === 0) {
5383
- printInfo("No local sessions and no other devices found.");
5384
- printInfo("Use Claude Code in this directory first, then run sync.");
5385
- return "cancelled";
5386
- }
5387
- if (menuOptions.length === 1 && menuOptions[0].value === "push") {
5388
- printInfo("No other devices found.");
5389
- }
5390
- const choice = await select2({
5391
- message: brand("What would you like to do?"),
5392
- options: menuOptions
5393
- });
5394
- if (isCancel3(choice)) return "cancelled";
5395
- if (choice === "push") {
5396
- const result2 = await bundlePushPhase(client, config.deviceId, cwd, manifest, projectDir, void 0, options);
5397
- if (result2.failed) {
5398
- return "cancelled";
5399
- }
5400
- await saveProjectLink(cwd, {
5401
- projectId: result2.projectId,
5402
- foreignEncodedDir: ctx.encodedPath,
5403
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
5404
- });
5405
- return "pushed";
5406
- }
5407
- const deviceChoice = await select2({
5408
- message: brand("Select device"),
5409
- options: otherDevices.map((d) => ({
5410
- value: d.id,
5411
- label: d.name,
5412
- hint: dim(`${d.hostname} \xB7 ${d.platform}`)
5413
- }))
5414
- });
5415
- if (isCancel3(deviceChoice)) return "cancelled";
5416
- const spinner = createSpinner("Loading projects...").start();
5417
- let projects;
5418
- try {
5419
- projects = await client.get(`/api/devices/${deviceChoice}/projects`);
5420
- } catch (err) {
5421
- spinner.stop();
5422
- if (err instanceof AuthExpiredError) throw err;
5423
- printError("Failed to load projects from device.");
5424
- return "cancelled";
5425
- }
5426
- spinner.stop();
5427
- if (projects.length === 0) {
5428
- printInfo("No projects found on that device.");
5429
- return "cancelled";
5430
- }
5431
- const projectChoice = await select2({
5432
- message: brand("Select project"),
5433
- options: projects.map((p) => ({
5434
- value: p.id,
5435
- label: p.displayName || p.originalPath,
5436
- hint: dim(p.localPath)
5437
- }))
5438
- });
5439
- if (isCancel3(projectChoice)) return "cancelled";
5440
- const selectedProject = projects.find((p) => p.id === projectChoice);
5441
- const existingCheck = await handleExistingSessionDir(projectsDir, ctx.encodedPath);
5442
- if (existingCheck === "cancelled") {
5443
- return "cancelled";
5444
- }
5445
- const result = await crossMachineBundlePull(
5446
- client,
5447
- config.deviceId,
5448
- cwd,
5449
- deviceChoice,
5450
- selectedProject.id,
5451
- selectedProject.encodedDir,
5452
- projectsDir,
5453
- options
5454
- );
5455
- if (result.fileCount === 0 && !options.dryRun) {
5456
- printInfo("No files to download.");
5457
- return "cancelled";
5458
- }
5459
- if (!options.dryRun) {
5460
- await createProjectSymlink(projectsDir, ctx.encodedPath, selectedProject.encodedDir);
5461
- await saveProjectLink(cwd, {
5462
- projectId: selectedProject.id,
5463
- foreignEncodedDir: selectedProject.encodedDir,
5464
- linkedAt: (/* @__PURE__ */ new Date()).toISOString()
5465
- });
5466
- printInfo(`Symlink: ${dim(ctx.encodedPath)} \u2192 ${dim(selectedProject.encodedDir)}`);
5467
- }
5468
- return "pulled";
5469
- }
5470
- async function handleExistingSessionDir(projectsDir, localEncoded) {
5471
- const symlinkPath = join4(projectsDir, localEncoded);
5472
- try {
5473
- const stats = await lstat2(symlinkPath);
5474
- if (stats.isSymbolicLink()) {
5475
- return "continue";
5476
- }
5477
- if (stats.isDirectory()) {
5478
- printInfo(brand("Local session directory already exists"));
5479
- printInfo(dim(symlinkPath));
5480
- const choice = await select2({
5481
- message: brand("What would you like to do?"),
5482
- options: [
5483
- {
5484
- value: "backup",
5485
- label: "Backup and continue",
5486
- hint: dim("Move existing to .bak and sync from remote")
5487
- },
5488
- {
5489
- value: "cancel",
5490
- label: "Stop sync",
5491
- hint: dim("Keep existing local sessions")
5492
- }
5493
- ]
5494
- });
5495
- if (isCancel3(choice) || choice === "cancel") {
5496
- return "cancelled";
5497
- }
5498
- const backupPath = `${symlinkPath}.bak.${Date.now()}`;
5499
- await rename(symlinkPath, backupPath);
5500
- printSuccess(`Backed up to: ${dim(backupPath)}`);
5501
- return "continue";
5502
- }
5503
- } catch {
5504
- }
5505
- return "continue";
5506
- }
5507
- async function createProjectSymlink(projectsDir, localEncoded, foreignEncoded) {
5508
- const symlinkPath = join4(projectsDir, localEncoded);
5509
- await mkdir3(projectsDir, { recursive: true });
5510
- try {
5511
- const stats = await lstat2(symlinkPath);
5512
- if (stats.isSymbolicLink()) {
5513
- await unlink(symlinkPath);
5514
- }
5515
- } catch {
5516
- }
5517
- await symlink(foreignEncoded, symlinkPath);
5518
- }
5519
- async function bundlePushPhase(client, deviceId, projectPath, manifest, projectDir, projectId, options) {
5520
- if (manifest.length === 0) {
5521
- return { fileCount: 0, projectId: projectId || "", failed: false };
5522
- }
5523
5348
  const totalBytes = manifest.reduce((sum, f) => sum + f.size, 0);
5524
- printInfo(`${brand(String(manifest.length))} files ${dim(`(${formatBytes(totalBytes)})`)} to push`);
5349
+ printInfo(`Files: ${brand(String(manifest.length))} ${dim(`(${formatBytes(totalBytes)})`)}`);
5525
5350
  if (options.dryRun) {
5351
+ console.log();
5352
+ printInfo("Changes to push:");
5526
5353
  if (options.verbose) {
5527
5354
  for (const entry of manifest) {
5528
5355
  console.log(` ${success("+")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5529
5356
  }
5357
+ } else {
5358
+ console.log(` ${brand(String(manifest.length))} files would be uploaded`);
5359
+ }
5360
+ printOutro("Dry run complete");
5361
+ return;
5362
+ }
5363
+ if (projectLink?.projectId) {
5364
+ const permission = await checkPushPermission(client, projectLink.projectId);
5365
+ if (!permission.allowed) {
5366
+ printError(`Cannot push: You have ${brand(permission.role)} access to this project.`);
5367
+ printInfo("Only project editors and owners can push changes.");
5368
+ return;
5530
5369
  }
5531
- return { fileCount: manifest.length, projectId: projectId || "", failed: false };
5370
+ }
5371
+ const localVersion = projectLink?.localVersion ?? 0;
5372
+ let commitMessage = options.message;
5373
+ if (!commitMessage) {
5374
+ const result = await text3({
5375
+ message: brand("Commit message:"),
5376
+ placeholder: "Describe your changes...",
5377
+ validate: (value) => {
5378
+ if (!value || value.trim().length === 0) {
5379
+ return "Please enter a commit message";
5380
+ }
5381
+ if (value.length > 500) {
5382
+ return "Message too long (max 500 characters)";
5383
+ }
5384
+ }
5385
+ });
5386
+ if (isCancel3(result)) {
5387
+ printInfo("Push cancelled.");
5388
+ return;
5389
+ }
5390
+ commitMessage = result;
5532
5391
  }
5533
5392
  const tempBundle = join4(tmpdir(), `claude-sync-bundle-${Date.now()}.tar.gz`);
5534
5393
  try {
5535
- const spinner = createSpinner("Syncing...").start();
5394
+ const spinner = createSpinner("Preparing...").start();
5536
5395
  const bundleResult = await createBundle(projectDir, manifest, tempBundle, (bytes, total, phase) => {
5537
5396
  if (phase === "compressing") {
5538
5397
  const percent = Math.round(bytes / total * 100);
5539
5398
  spinner.text = `Compressing files... ${dim(`${percent}%`)}`;
5540
5399
  }
5541
5400
  });
5542
- const progress = createTransferProgress("Uploading", bundleResult.size, spinner);
5401
+ spinner.text = "Checking remote...";
5543
5402
  let prepareResponse;
5544
5403
  try {
5545
5404
  prepareResponse = await client.post("/api/sync/push/bundle/prepare", {
5546
- deviceId,
5547
- projectPath,
5548
- projectId,
5405
+ deviceId: config.deviceId,
5406
+ projectPath: cwd,
5407
+ projectId: projectLink?.projectId,
5549
5408
  manifest,
5550
- bundleSize: bundleResult.size
5409
+ bundleSize: bundleResult.size,
5410
+ localVersion,
5411
+ message: commitMessage
5551
5412
  });
5552
5413
  } catch (err) {
5553
5414
  spinner.stop();
5554
5415
  if (err instanceof AuthExpiredError) throw err;
5555
5416
  printError(`Push failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5556
- return { fileCount: 0, projectId: projectId || "", failed: true };
5417
+ return;
5557
5418
  }
5558
- await streamUpload(prepareResponse.uploadUrl, tempBundle, (bytes, total) => {
5559
- progress.update(bytes);
5560
- });
5561
- try {
5562
- await client.post("/api/sync/push/bundle/complete", {
5563
- syncEventId: prepareResponse.syncEventId,
5564
- manifest
5565
- });
5566
- } catch {
5419
+ if (!prepareResponse.canProceed) {
5420
+ spinner.stop();
5421
+ console.log();
5422
+ printError(`Cannot push: You are ${brand(String(prepareResponse.behindBy))} version(s) behind.`);
5423
+ console.log();
5424
+ if (prepareResponse.recentCommits && prepareResponse.recentCommits.length > 0) {
5425
+ printInfo("Recent commits on remote:");
5426
+ for (const commit of prepareResponse.recentCommits) {
5427
+ const deviceName = commit.device?.name || "Unknown";
5428
+ const timeAgo = formatTimeAgo(commit.createdAt);
5429
+ console.log(` v${commit.version} "${commit.message}" ${dim(`(${deviceName}, ${timeAgo})`)}`);
5430
+ }
5431
+ console.log();
5432
+ }
5433
+ printInfo(`Run ${brand("claude-sync pull")} first to get these changes.`);
5434
+ return;
5435
+ }
5436
+ const progress = createTransferProgress("Uploading", bundleResult.size, spinner);
5437
+ await streamUpload(prepareResponse.uploadUrl, tempBundle, (bytes) => {
5438
+ progress.update(bytes);
5439
+ });
5440
+ let completeResponse;
5441
+ try {
5442
+ completeResponse = await client.post("/api/sync/push/bundle/complete", {
5443
+ syncEventId: prepareResponse.syncEventId,
5444
+ manifest,
5445
+ message: commitMessage
5446
+ });
5447
+ } catch (err) {
5448
+ spinner.stop();
5449
+ if (err instanceof AuthExpiredError) throw err;
5450
+ printError(`Push failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5451
+ return;
5452
+ }
5453
+ const newVersion = completeResponse.commit.version;
5454
+ const compressionRatio = Math.round((1 - bundleResult.size / bundleResult.originalSize) * 100);
5455
+ progress.finish(`Pushed ${manifest.length} files ${dim(`(${formatBytes(bundleResult.size)}, ${compressionRatio}% smaller)`)}`);
5456
+ await saveProjectLink(cwd, {
5457
+ projectId: prepareResponse.projectId,
5458
+ foreignEncodedDir: (0, import_utils3.encodePath)(cwd),
5459
+ linkedAt: projectLink?.linkedAt || (/* @__PURE__ */ new Date()).toISOString(),
5460
+ localVersion: newVersion
5461
+ });
5462
+ console.log();
5463
+ printSuccess(`Commit: "${commitMessage}"`);
5464
+ printInfo(`Version: ${localVersion} \u2192 ${newVersion}`);
5465
+ printOutro("Done");
5466
+ } finally {
5467
+ try {
5468
+ await rm(tempBundle, { force: true });
5469
+ } catch {
5470
+ }
5471
+ }
5472
+ }
5473
+ async function checkPushPermission(client, projectId) {
5474
+ try {
5475
+ const roleInfo = await client.get(`/api/projects/${projectId}/my-role`);
5476
+ return { allowed: roleInfo.canPush, role: roleInfo.role };
5477
+ } catch {
5478
+ return { allowed: true, role: "owner" };
5479
+ }
5480
+ }
5481
+ function formatTimeAgo(dateStr) {
5482
+ const date = new Date(dateStr);
5483
+ const now = /* @__PURE__ */ new Date();
5484
+ const diffMs = now.getTime() - date.getTime();
5485
+ const diffMins = Math.floor(diffMs / 6e4);
5486
+ const diffHours = Math.floor(diffMs / 36e5);
5487
+ const diffDays = Math.floor(diffMs / 864e5);
5488
+ if (diffMins < 1) return "just now";
5489
+ if (diffMins < 60) return `${diffMins}m ago`;
5490
+ if (diffHours < 24) return `${diffHours}h ago`;
5491
+ if (diffDays < 7) return `${diffDays}d ago`;
5492
+ return date.toLocaleDateString();
5493
+ }
5494
+ var import_utils3;
5495
+ var init_push = __esm({
5496
+ "src/commands/push.ts"() {
5497
+ "use strict";
5498
+ init_esm_shims();
5499
+ init_config();
5500
+ init_api_client();
5501
+ init_sync_engine();
5502
+ init_progress();
5503
+ init_bundle();
5504
+ import_utils3 = __toESM(require_dist(), 1);
5505
+ init_theme();
5506
+ }
5507
+ });
5508
+
5509
+ // src/commands/pull.ts
5510
+ var pull_exports = {};
5511
+ __export(pull_exports, {
5512
+ pullCommand: () => pullCommand
5513
+ });
5514
+ import { Command as Command5 } from "commander";
5515
+ import { confirm, isCancel as isCancel4 } from "@clack/prompts";
5516
+ import { join as join5 } from "path";
5517
+ import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
5518
+ import { rm as rm2, mkdir as mkdir3 } from "fs/promises";
5519
+ function pullCommand() {
5520
+ return new Command5("pull").description("Pull latest changes from cloud").option("--dry-run", "Show what would be pulled without pulling").option("--verbose", "Show detailed output").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
5521
+ await runPull(options);
5522
+ });
5523
+ }
5524
+ async function runPull(options) {
5525
+ printIntro("Pull");
5526
+ const config = await loadConfig();
5527
+ if (!isAuthenticated(config)) {
5528
+ printError("Not logged in. Run `claude-sync login` first.");
5529
+ return;
5530
+ }
5531
+ if (!config.deviceId) {
5532
+ printError("No device registered. Run `claude-sync device add` first.");
5533
+ return;
5534
+ }
5535
+ const cwd = process.cwd();
5536
+ const ctx = await resolveProjectState(cwd);
5537
+ const client = new ApiClient(config.apiUrl);
5538
+ const projectsDir = join5(homedir5(), import_utils4.CLAUDE_DIR, import_utils4.PROJECTS_DIR);
5539
+ const projectLink = await loadProjectLink(cwd);
5540
+ let projectDir = ctx.projectDir;
5541
+ if (ctx.state === "symlink" && ctx.symlinkTarget) {
5542
+ projectDir = join5(projectsDir, ctx.symlinkTarget);
5543
+ }
5544
+ printInfo(`Project: ${brand(cwd)}`);
5545
+ if (!projectLink?.projectId) {
5546
+ printInfo("No project linked yet. Run `claude-sync push` first to create one.");
5547
+ printOutro("Done");
5548
+ return;
5549
+ }
5550
+ const localVersion = projectLink.localVersion ?? 0;
5551
+ const spinner = createSpinner("Checking for updates...").start();
5552
+ let syncState;
5553
+ try {
5554
+ syncState = await client.get(`/api/projects/${projectLink.projectId}/sync-state?localVersion=${localVersion}`);
5555
+ } catch (err) {
5556
+ spinner.stop();
5557
+ if (err instanceof AuthExpiredError) throw err;
5558
+ printError(`Failed to check sync state: ${err instanceof Error ? err.message : "Unknown error"}`);
5559
+ return;
5560
+ }
5561
+ if (!syncState.isBehind) {
5562
+ spinner.stop();
5563
+ console.log();
5564
+ printSuccess("Already up to date.");
5565
+ printInfo(`Version: ${localVersion} (latest)`);
5566
+ printOutro("Done");
5567
+ return;
5568
+ }
5569
+ spinner.stop();
5570
+ console.log();
5571
+ printInfo(`You are ${brand(String(syncState.behindBy))} version(s) behind.`);
5572
+ printInfo(`Local: v${localVersion} Remote: v${syncState.currentVersion}`);
5573
+ console.log();
5574
+ if (syncState.recentCommits.length > 0) {
5575
+ printInfo("Commits to pull:");
5576
+ for (const commit of syncState.recentCommits) {
5577
+ const deviceName = commit.device?.name || "Unknown";
5578
+ const timeAgo = formatTimeAgo2(commit.createdAt);
5579
+ console.log(` v${commit.version} "${commit.message}" ${dim(`(${deviceName}, ${timeAgo})`)}`);
5580
+ }
5581
+ console.log();
5582
+ }
5583
+ if (options.dryRun) {
5584
+ printOutro("Dry run complete");
5585
+ return;
5586
+ }
5587
+ if (!options.yes) {
5588
+ const proceed = await confirm({
5589
+ message: brand(`Pull ${syncState.behindBy} version(s)?`),
5590
+ initialValue: true
5591
+ });
5592
+ if (isCancel4(proceed) || !proceed) {
5593
+ printInfo("Pull cancelled.");
5594
+ return;
5595
+ }
5596
+ }
5597
+ const pullSpinner = createSpinner("Downloading...").start();
5598
+ let prepareResponse;
5599
+ try {
5600
+ prepareResponse = await client.post("/api/sync/pull/bundle/prepare", {
5601
+ deviceId: config.deviceId,
5602
+ projectPath: cwd,
5603
+ projectId: projectLink.projectId
5604
+ });
5605
+ } catch (err) {
5606
+ pullSpinner.stop();
5607
+ if (err instanceof AuthExpiredError) throw err;
5608
+ printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5609
+ return;
5610
+ }
5611
+ if (!prepareResponse.downloadUrl || !prepareResponse.bundleSize) {
5612
+ pullSpinner.stop();
5613
+ printInfo("No files to download.");
5614
+ printOutro("Done");
5615
+ return;
5616
+ }
5617
+ const tempBundle = join5(tmpdir2(), `claude-sync-pull-${Date.now()}.tar.gz`);
5618
+ try {
5619
+ const progress = createTransferProgress("Downloading", prepareResponse.bundleSize, pullSpinner);
5620
+ await streamDownload(prepareResponse.downloadUrl, tempBundle, (bytes) => {
5621
+ progress.update(bytes);
5622
+ });
5623
+ progress.finish("Downloaded");
5624
+ const extractSpinner = createSpinner("Extracting...").start();
5625
+ await mkdir3(projectDir, { recursive: true });
5626
+ const extractedCount = await extractBundle(tempBundle, projectDir);
5627
+ extractSpinner.succeed(`Extracted ${extractedCount} files`);
5628
+ try {
5629
+ await client.post("/api/sync/pull/bundle/complete", {
5630
+ syncEventId: prepareResponse.syncEventId,
5631
+ manifest: prepareResponse.manifest
5632
+ });
5633
+ } catch {
5634
+ }
5635
+ await saveProjectLink(cwd, {
5636
+ ...projectLink,
5637
+ localVersion: syncState.currentVersion
5638
+ });
5639
+ console.log();
5640
+ printSuccess(`Pulled ${prepareResponse.manifest.length} files`);
5641
+ printInfo(`Version: ${localVersion} \u2192 ${syncState.currentVersion}`);
5642
+ printOutro("Done");
5643
+ } finally {
5644
+ try {
5645
+ await rm2(tempBundle, { force: true });
5646
+ } catch {
5647
+ }
5648
+ }
5649
+ }
5650
+ function formatTimeAgo2(dateStr) {
5651
+ const date = new Date(dateStr);
5652
+ const now = /* @__PURE__ */ new Date();
5653
+ const diffMs = now.getTime() - date.getTime();
5654
+ const diffMins = Math.floor(diffMs / 6e4);
5655
+ const diffHours = Math.floor(diffMs / 36e5);
5656
+ const diffDays = Math.floor(diffMs / 864e5);
5657
+ if (diffMins < 1) return "just now";
5658
+ if (diffMins < 60) return `${diffMins}m ago`;
5659
+ if (diffHours < 24) return `${diffHours}h ago`;
5660
+ if (diffDays < 7) return `${diffDays}d ago`;
5661
+ return date.toLocaleDateString();
5662
+ }
5663
+ var import_utils4;
5664
+ var init_pull = __esm({
5665
+ "src/commands/pull.ts"() {
5666
+ "use strict";
5667
+ init_esm_shims();
5668
+ init_config();
5669
+ init_api_client();
5670
+ init_sync_engine();
5671
+ init_progress();
5672
+ init_bundle();
5673
+ import_utils4 = __toESM(require_dist(), 1);
5674
+ init_theme();
5675
+ }
5676
+ });
5677
+
5678
+ // src/commands/log.ts
5679
+ var log_exports = {};
5680
+ __export(log_exports, {
5681
+ logCommand: () => logCommand
5682
+ });
5683
+ import { Command as Command6 } from "commander";
5684
+ function logCommand() {
5685
+ return new Command6("log").description("Show commit history for current project").option("-n, --limit <number>", "Number of commits to show", "10").action(async (options) => {
5686
+ await runLog(options);
5687
+ });
5688
+ }
5689
+ async function runLog(options) {
5690
+ printIntro("Log");
5691
+ const config = await loadConfig();
5692
+ if (!isAuthenticated(config)) {
5693
+ printError("Not logged in. Run `claude-sync login` first.");
5694
+ return;
5695
+ }
5696
+ const cwd = process.cwd();
5697
+ const projectLink = await loadProjectLink(cwd);
5698
+ printInfo(`Project: ${brand(cwd)}`);
5699
+ if (!projectLink?.projectId) {
5700
+ printInfo("No project linked yet. Run `claude-sync push` first.");
5701
+ printOutro("Done");
5702
+ return;
5703
+ }
5704
+ const client = new ApiClient(config.apiUrl);
5705
+ const limit = parseInt(options.limit, 10) || 10;
5706
+ let commits;
5707
+ try {
5708
+ commits = await client.get(`/api/projects/${projectLink.projectId}/commits?limit=${limit}`);
5709
+ } catch (err) {
5710
+ if (err instanceof AuthExpiredError) throw err;
5711
+ printError(`Failed to fetch commits: ${err instanceof Error ? err.message : "Unknown error"}`);
5712
+ return;
5713
+ }
5714
+ if (commits.length === 0) {
5715
+ printInfo("No commits yet.");
5716
+ printOutro("Done");
5717
+ return;
5718
+ }
5719
+ const localVersion = projectLink.localVersion ?? 0;
5720
+ console.log();
5721
+ for (const commit of commits) {
5722
+ const deviceName = commit.device?.name || "Unknown device";
5723
+ const timeAgo = formatTimeAgo3(commit.createdAt);
5724
+ const isHead = commit.version === localVersion;
5725
+ const versionStr = isHead ? `${brand(`v${commit.version}`)} ${dim("(HEAD)")}` : `v${commit.version}`;
5726
+ console.log(` ${versionStr} "${commit.message}"`);
5727
+ console.log(` ${dim(`${deviceName} \u2022 ${timeAgo}`)}`);
5728
+ console.log();
5729
+ }
5730
+ printOutro("");
5731
+ }
5732
+ function formatTimeAgo3(dateStr) {
5733
+ const date = new Date(dateStr);
5734
+ const now = /* @__PURE__ */ new Date();
5735
+ const diffMs = now.getTime() - date.getTime();
5736
+ const diffMins = Math.floor(diffMs / 6e4);
5737
+ const diffHours = Math.floor(diffMs / 36e5);
5738
+ const diffDays = Math.floor(diffMs / 864e5);
5739
+ if (diffMins < 1) return "just now";
5740
+ if (diffMins < 60) return `${diffMins}m ago`;
5741
+ if (diffHours < 24) return `${diffHours}h ago`;
5742
+ if (diffDays < 7) return `${diffDays}d ago`;
5743
+ return date.toLocaleDateString();
5744
+ }
5745
+ var init_log = __esm({
5746
+ "src/commands/log.ts"() {
5747
+ "use strict";
5748
+ init_esm_shims();
5749
+ init_config();
5750
+ init_api_client();
5751
+ init_theme();
5752
+ }
5753
+ });
5754
+
5755
+ // src/commands/status.ts
5756
+ var status_exports = {};
5757
+ __export(status_exports, {
5758
+ statusCommand: () => statusCommand
5759
+ });
5760
+ import { Command as Command8 } from "commander";
5761
+ function statusCommand() {
5762
+ return new Command8("status").description("Show sync status for current project").action(async () => {
5763
+ printIntro("Status");
5764
+ const config = await loadConfig();
5765
+ const cwd = process.cwd();
5766
+ printLabel("API", config.apiUrl);
5767
+ printLabel("Authenticated", isAuthenticated(config) ? success("yes") : error("no"));
5768
+ printLabel("Device", config.deviceName || dim("none"));
5769
+ printLabel("Project", brand(cwd));
5770
+ const ctx = await resolveProjectState(cwd);
5771
+ printLabel("State", ctx.state === "none" ? dim("no session data") : ctx.state === "symlink" ? `symlink \u2192 ${dim(ctx.symlinkTarget || "unknown")}` : "local");
5772
+ if (!isAuthenticated(config) || ctx.state === "none") return;
5773
+ const spinner = createSpinner("Scanning files...").start();
5774
+ const savedManifest = await loadProjectManifest(cwd);
5775
+ const currentManifest = await buildProjectManifest(cwd);
5776
+ const diff = computeDiff(currentManifest, savedManifest);
5777
+ spinner.stop();
5778
+ const totalSize = currentManifest.reduce((sum, e) => sum + e.size, 0);
5779
+ printDivider();
5780
+ printLabel("Local files", `${brand(String(currentManifest.length))} ${dim(`(${formatBytes(totalSize)})`)}`);
5781
+ console.log();
5782
+ printInfo("Changes since last sync:");
5783
+ console.log(` ${success(`+${diff.added.length}`)} added ${warn(`~${diff.modified.length}`)} modified ${error(`-${diff.deleted.length}`)} deleted`);
5784
+ console.log();
5785
+ const projectLink = await loadProjectLink(cwd);
5786
+ if (projectLink?.projectId) {
5787
+ const client = new ApiClient(config.apiUrl);
5788
+ const localVersion = projectLink.localVersion ?? 0;
5789
+ try {
5790
+ const syncState = await client.get(`/api/projects/${projectLink.projectId}/sync-state?localVersion=${localVersion}`);
5791
+ if (syncState.isBehind) {
5792
+ printWarn(`You are ${brand(String(syncState.behindBy))} version(s) behind.`);
5793
+ printInfo(`Local: v${localVersion} Remote: v${syncState.currentVersion}`);
5794
+ console.log();
5795
+ if (syncState.recentCommits.length > 0) {
5796
+ printInfo("Recent commits on remote:");
5797
+ for (const commit of syncState.recentCommits) {
5798
+ const deviceName = commit.device?.name || "Unknown";
5799
+ console.log(` v${commit.version} "${commit.message}" ${dim(`(${deviceName})`)}`);
5800
+ }
5801
+ console.log();
5802
+ }
5803
+ printInfo(`Run ${brand("claude-sync pull")} to get latest changes.`);
5804
+ } else {
5805
+ printSuccess("Up to date");
5806
+ printInfo(`Version: ${localVersion}`);
5807
+ }
5808
+ } catch {
5809
+ printInfo(`Local version: ${localVersion}`);
5810
+ }
5811
+ }
5812
+ });
5813
+ }
5814
+ var init_status = __esm({
5815
+ "src/commands/status.ts"() {
5816
+ "use strict";
5817
+ init_esm_shims();
5818
+ init_config();
5819
+ init_sync_engine();
5820
+ init_api_client();
5821
+ init_progress();
5822
+ init_theme();
5823
+ }
5824
+ });
5825
+
5826
+ // src/commands/whoami.ts
5827
+ var whoami_exports = {};
5828
+ __export(whoami_exports, {
5829
+ whoamiCommand: () => whoamiCommand
5830
+ });
5831
+ import { Command as Command9 } from "commander";
5832
+ function whoamiCommand() {
5833
+ return new Command9("whoami").description("Show current user and device info").action(async () => {
5834
+ printIntro("Who Am I");
5835
+ const config = await loadConfig();
5836
+ if (!isAuthenticated(config)) {
5837
+ printError("Not logged in. Run `claude-sync login` first.");
5838
+ return;
5839
+ }
5840
+ const client = new ApiClient(config.apiUrl);
5841
+ const spinner = createSpinner("Fetching user info...").start();
5842
+ try {
5843
+ const user = await client.get("/api/auth/me");
5844
+ spinner.stop();
5845
+ printLabel("User", `${user.name} ${dim(`(${user.email})`)}`);
5846
+ printLabel("Plan", brand(user.plan));
5847
+ printDivider();
5848
+ printLabel("Device", config.deviceName || dim("none"));
5849
+ printLabel("Device ID", config.deviceId || dim("not registered"));
5850
+ } catch (err) {
5851
+ spinner.stop();
5852
+ if (err instanceof AuthExpiredError) throw err;
5853
+ printLabel("Email", config.email || dim("unknown"));
5854
+ printLabel("Device", config.deviceName || dim("none"));
5855
+ }
5856
+ console.log();
5857
+ });
5858
+ }
5859
+ var init_whoami = __esm({
5860
+ "src/commands/whoami.ts"() {
5861
+ "use strict";
5862
+ init_esm_shims();
5863
+ init_config();
5864
+ init_api_client();
5865
+ init_progress();
5866
+ init_theme();
5867
+ }
5868
+ });
5869
+
5870
+ // src/index.ts
5871
+ init_esm_shims();
5872
+ init_login();
5873
+ init_logout();
5874
+ init_device();
5875
+ init_push();
5876
+ init_pull();
5877
+ init_log();
5878
+ import { Command as Command10 } from "commander";
5879
+
5880
+ // src/commands/sync.ts
5881
+ init_esm_shims();
5882
+ init_config();
5883
+ init_api_client();
5884
+ init_sync_engine();
5885
+ init_progress();
5886
+ init_bundle();
5887
+ var import_utils5 = __toESM(require_dist(), 1);
5888
+ init_theme();
5889
+ import { Command as Command7 } from "commander";
5890
+ import { select as select2, isCancel as isCancel5 } from "@clack/prompts";
5891
+ import { mkdir as mkdir4, lstat as lstat2, symlink, unlink, rename, rm as rm3 } from "fs/promises";
5892
+ import { join as join6 } from "path";
5893
+ import { homedir as homedir6, tmpdir as tmpdir3 } from "os";
5894
+ async function runSync(options) {
5895
+ printIntro("Sync");
5896
+ const config = await loadConfig();
5897
+ if (!isAuthenticated(config)) {
5898
+ printError("Not logged in. Run `claude-sync login` first.");
5899
+ return;
5900
+ }
5901
+ if (!config.deviceId) {
5902
+ printError("No device registered. Run `claude-sync device add` first.");
5903
+ return;
5904
+ }
5905
+ const cwd = process.cwd();
5906
+ const ctx = await resolveProjectState(cwd);
5907
+ const client = new ApiClient(config.apiUrl);
5908
+ const projectsDir = join6(homedir6(), import_utils5.CLAUDE_DIR, import_utils5.PROJECTS_DIR);
5909
+ const projectLink = await loadProjectLink(cwd);
5910
+ let projectDir = ctx.projectDir;
5911
+ if (ctx.state === "symlink" && ctx.symlinkTarget) {
5912
+ projectDir = join6(projectsDir, ctx.symlinkTarget);
5913
+ }
5914
+ printInfo(`Project: ${brand(cwd)}`);
5915
+ if (ctx.state === "symlink") {
5916
+ printInfo(`Linked to: ${dim(ctx.symlinkTarget || "unknown")}`);
5917
+ }
5918
+ const manifest = await buildProjectManifest(cwd);
5919
+ if (!projectLink) {
5920
+ const result = await handleFirstRun(client, config, cwd, ctx, manifest, projectDir, projectsDir, options);
5921
+ if (result === "cancelled") {
5922
+ printOutro("Cancelled.");
5923
+ return;
5924
+ }
5925
+ if (result === "pulled") {
5926
+ const finalManifest2 = await buildProjectManifest(cwd);
5927
+ await saveProjectManifest(cwd, finalManifest2);
5928
+ printOutro("Done");
5929
+ return;
5930
+ }
5931
+ if (result === "pushed") {
5932
+ const finalManifest2 = await buildProjectManifest(cwd);
5933
+ await saveProjectManifest(cwd, finalManifest2);
5934
+ printOutro("Done");
5935
+ return;
5936
+ }
5937
+ printOutro("Done");
5938
+ return;
5939
+ }
5940
+ let pushResult = { fileCount: 0, projectId: projectLink.projectId, failed: false };
5941
+ let pullResult = { fileCount: 0, failed: false };
5942
+ if (manifest.length > 0) {
5943
+ pushResult = await bundlePushPhase(client, config.deviceId, cwd, manifest, projectDir, projectLink.projectId, options);
5944
+ }
5945
+ if (!pushResult.failed) {
5946
+ pullResult = await bundlePullPhase(client, config.deviceId, cwd, projectDir, projectLink.projectId, options);
5947
+ }
5948
+ if (pushResult.failed && pullResult.failed) {
5949
+ printOutro("Sync failed.");
5950
+ return;
5951
+ }
5952
+ const finalManifest = await buildProjectManifest(cwd);
5953
+ await saveProjectManifest(cwd, finalManifest);
5954
+ if (pushResult.fileCount > 0 || pullResult.fileCount > 0) {
5955
+ const parts = [];
5956
+ if (pushResult.fileCount > 0) parts.push(`${success(`${pushResult.fileCount}`)} pushed`);
5957
+ if (pullResult.fileCount > 0) parts.push(`${success(`${pullResult.fileCount}`)} pulled`);
5958
+ printSuccess(`Synced: ${parts.join(", ")} files`);
5959
+ } else if (!pushResult.failed && !pullResult.failed) {
5960
+ printSuccess("Everything is up to date.");
5961
+ }
5962
+ printOutro("Done");
5963
+ }
5964
+ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, projectsDir, options) {
5965
+ printInfo(brand("First time syncing this project"));
5966
+ const hasLocalFiles = ctx.state !== "none" && manifest.length > 0;
5967
+ const devicesSpinner = createSpinner("Checking for other devices...").start();
5968
+ let devices;
5969
+ try {
5970
+ devices = await client.get("/api/devices");
5971
+ } catch (err) {
5972
+ if (err instanceof AuthExpiredError) throw err;
5973
+ devices = [];
5974
+ }
5975
+ devicesSpinner.stop();
5976
+ const otherDevices = devices.filter((d) => d.id !== config.deviceId);
5977
+ let sharedProjects = [];
5978
+ try {
5979
+ sharedProjects = await client.get("/api/projects/shared");
5980
+ } catch {
5981
+ }
5982
+ const menuOptions = [];
5983
+ if (hasLocalFiles) {
5984
+ menuOptions.push({
5985
+ value: "push",
5986
+ label: "Push to cloud",
5987
+ hint: dim(`Upload ${manifest.length} local files`)
5988
+ });
5989
+ }
5990
+ if (otherDevices.length > 0) {
5991
+ menuOptions.push({
5992
+ value: "continue",
5993
+ label: "Continue from another machine",
5994
+ hint: dim("Pull sessions from a device")
5995
+ });
5996
+ }
5997
+ if (sharedProjects.length > 0) {
5998
+ menuOptions.push({
5999
+ value: "collaborator",
6000
+ label: "Sync from collaborator",
6001
+ hint: dim(`${sharedProjects.length} shared project(s) available`)
6002
+ });
6003
+ }
6004
+ if (menuOptions.length === 0) {
6005
+ printInfo("No local sessions and no other devices found.");
6006
+ printInfo("Use Claude Code in this directory first, then run sync.");
6007
+ return "cancelled";
6008
+ }
6009
+ if (menuOptions.length === 1 && menuOptions[0].value === "push") {
6010
+ printInfo("No other devices found.");
6011
+ }
6012
+ const choice = await select2({
6013
+ message: brand("What would you like to do?"),
6014
+ options: menuOptions
6015
+ });
6016
+ if (isCancel5(choice)) return "cancelled";
6017
+ if (choice === "push") {
6018
+ const result2 = await bundlePushPhase(client, config.deviceId, cwd, manifest, projectDir, void 0, options);
6019
+ if (result2.failed) {
6020
+ return "cancelled";
6021
+ }
6022
+ await saveProjectLink(cwd, {
6023
+ projectId: result2.projectId,
6024
+ foreignEncodedDir: ctx.encodedPath,
6025
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
6026
+ });
6027
+ return "pushed";
6028
+ }
6029
+ if (choice === "collaborator") {
6030
+ const sharedChoice = await select2({
6031
+ message: brand("Select shared project"),
6032
+ options: sharedProjects.map((p) => ({
6033
+ value: p.id,
6034
+ label: p.displayName || p.originalPath,
6035
+ hint: dim(`${p.owner.name} \xB7 ${p.role}`)
6036
+ }))
6037
+ });
6038
+ if (isCancel5(sharedChoice)) return "cancelled";
6039
+ const selectedShared = sharedProjects.find((p) => p.id === sharedChoice);
6040
+ const syncStatusSpinner = createSpinner("Checking sync status...").start();
6041
+ let syncStatus = null;
6042
+ try {
6043
+ syncStatus = await client.get(`/api/projects/${selectedShared.id}/sync-status`);
6044
+ } catch {
6045
+ }
6046
+ syncStatusSpinner.stop();
6047
+ if (syncStatus?.hasRecentSync && syncStatus.lastSyncedBy) {
6048
+ printInfo(brand("Recent sync detected"));
6049
+ printInfo(dim(`${syncStatus.lastSyncedBy.name} synced from ${syncStatus.lastSyncedBy.deviceName} recently`));
6050
+ const confirmChoice = await select2({
6051
+ message: brand("Continue anyway?"),
6052
+ options: [
6053
+ { value: "yes", label: "Yes, continue", hint: dim("Pull the latest version") },
6054
+ { value: "no", label: "Cancel", hint: dim("Wait for them to finish") }
6055
+ ]
6056
+ });
6057
+ if (isCancel5(confirmChoice) || confirmChoice === "no") return "cancelled";
6058
+ }
6059
+ const existingCheck2 = await handleExistingSessionDir(projectsDir, ctx.encodedPath);
6060
+ if (existingCheck2 === "cancelled") {
6061
+ return "cancelled";
6062
+ }
6063
+ const result2 = await collaboratorBundlePull(
6064
+ client,
6065
+ config.deviceId,
6066
+ cwd,
6067
+ selectedShared.id,
6068
+ selectedShared.canonicalPath,
6069
+ projectsDir,
6070
+ options
6071
+ );
6072
+ if (result2.fileCount === 0 && !options.dryRun) {
6073
+ printInfo("No files to download.");
6074
+ return "cancelled";
6075
+ }
6076
+ if (!options.dryRun) {
6077
+ const foreignEncoded = selectedShared.canonicalPath.replace(/\//g, "%2F");
6078
+ await createProjectSymlink(projectsDir, ctx.encodedPath, foreignEncoded);
6079
+ await saveProjectLink(cwd, {
6080
+ projectId: selectedShared.id,
6081
+ foreignEncodedDir: foreignEncoded,
6082
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
6083
+ });
6084
+ printInfo(`Synced from: ${dim(`${selectedShared.owner.name}'s ${selectedShared.displayName}`)}`);
6085
+ }
6086
+ return "pulled";
6087
+ }
6088
+ const deviceChoice = await select2({
6089
+ message: brand("Select device"),
6090
+ options: otherDevices.map((d) => ({
6091
+ value: d.id,
6092
+ label: d.name,
6093
+ hint: dim(`${d.hostname} \xB7 ${d.platform}`)
6094
+ }))
6095
+ });
6096
+ if (isCancel5(deviceChoice)) return "cancelled";
6097
+ const spinner = createSpinner("Loading projects...").start();
6098
+ let projects;
6099
+ try {
6100
+ projects = await client.get(`/api/devices/${deviceChoice}/projects`);
6101
+ } catch (err) {
6102
+ spinner.stop();
6103
+ if (err instanceof AuthExpiredError) throw err;
6104
+ printError("Failed to load projects from device.");
6105
+ return "cancelled";
6106
+ }
6107
+ spinner.stop();
6108
+ if (projects.length === 0) {
6109
+ printInfo("No projects found on that device.");
6110
+ return "cancelled";
6111
+ }
6112
+ const projectChoice = await select2({
6113
+ message: brand("Select project"),
6114
+ options: projects.map((p) => ({
6115
+ value: p.id,
6116
+ label: p.displayName || p.originalPath,
6117
+ hint: dim(p.localPath)
6118
+ }))
6119
+ });
6120
+ if (isCancel5(projectChoice)) return "cancelled";
6121
+ const selectedProject = projects.find((p) => p.id === projectChoice);
6122
+ const existingCheck = await handleExistingSessionDir(projectsDir, ctx.encodedPath);
6123
+ if (existingCheck === "cancelled") {
6124
+ return "cancelled";
6125
+ }
6126
+ const result = await crossMachineBundlePull(
6127
+ client,
6128
+ config.deviceId,
6129
+ cwd,
6130
+ deviceChoice,
6131
+ selectedProject.id,
6132
+ selectedProject.encodedDir,
6133
+ projectsDir,
6134
+ options
6135
+ );
6136
+ if (result.fileCount === 0 && !options.dryRun) {
6137
+ printInfo("No files to download.");
6138
+ return "cancelled";
6139
+ }
6140
+ if (!options.dryRun) {
6141
+ await createProjectSymlink(projectsDir, ctx.encodedPath, selectedProject.encodedDir);
6142
+ await saveProjectLink(cwd, {
6143
+ projectId: selectedProject.id,
6144
+ foreignEncodedDir: selectedProject.encodedDir,
6145
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
6146
+ });
6147
+ printInfo(`Symlink: ${dim(ctx.encodedPath)} \u2192 ${dim(selectedProject.encodedDir)}`);
6148
+ }
6149
+ return "pulled";
6150
+ }
6151
+ async function handleExistingSessionDir(projectsDir, localEncoded) {
6152
+ const symlinkPath = join6(projectsDir, localEncoded);
6153
+ try {
6154
+ const stats = await lstat2(symlinkPath);
6155
+ if (stats.isSymbolicLink()) {
6156
+ return "continue";
6157
+ }
6158
+ if (stats.isDirectory()) {
6159
+ printInfo(brand("Local session directory already exists"));
6160
+ printInfo(dim(symlinkPath));
6161
+ const choice = await select2({
6162
+ message: brand("What would you like to do?"),
6163
+ options: [
6164
+ {
6165
+ value: "backup",
6166
+ label: "Backup and continue",
6167
+ hint: dim("Move existing to .bak and sync from remote")
6168
+ },
6169
+ {
6170
+ value: "cancel",
6171
+ label: "Stop sync",
6172
+ hint: dim("Keep existing local sessions")
6173
+ }
6174
+ ]
6175
+ });
6176
+ if (isCancel5(choice) || choice === "cancel") {
6177
+ return "cancelled";
6178
+ }
6179
+ const backupPath = `${symlinkPath}.bak.${Date.now()}`;
6180
+ await rename(symlinkPath, backupPath);
6181
+ printSuccess(`Backed up to: ${dim(backupPath)}`);
6182
+ return "continue";
6183
+ }
6184
+ } catch {
6185
+ }
6186
+ return "continue";
6187
+ }
6188
+ async function createProjectSymlink(projectsDir, localEncoded, foreignEncoded) {
6189
+ const symlinkPath = join6(projectsDir, localEncoded);
6190
+ await mkdir4(projectsDir, { recursive: true });
6191
+ try {
6192
+ const stats = await lstat2(symlinkPath);
6193
+ if (stats.isSymbolicLink()) {
6194
+ await unlink(symlinkPath);
6195
+ }
6196
+ } catch {
6197
+ }
6198
+ await symlink(foreignEncoded, symlinkPath);
6199
+ }
6200
+ async function bundlePushPhase(client, deviceId, projectPath, manifest, projectDir, projectId, options) {
6201
+ if (manifest.length === 0) {
6202
+ return { fileCount: 0, projectId: projectId || "", failed: false };
6203
+ }
6204
+ if (projectId) {
6205
+ const permission = await checkPushPermission2(client, projectId);
6206
+ if (!permission.allowed) {
6207
+ printError(`Cannot push: You have ${brand(permission.role)} access to this project.`);
6208
+ printInfo("Only project editors and owners can push changes.");
6209
+ return { fileCount: 0, projectId, failed: true };
6210
+ }
6211
+ }
6212
+ const totalBytes = manifest.reduce((sum, f) => sum + f.size, 0);
6213
+ printInfo(`${brand(String(manifest.length))} files ${dim(`(${formatBytes(totalBytes)})`)} to push`);
6214
+ if (options.dryRun) {
6215
+ if (options.verbose) {
6216
+ for (const entry of manifest) {
6217
+ console.log(` ${success("+")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
6218
+ }
6219
+ }
6220
+ return { fileCount: manifest.length, projectId: projectId || "", failed: false };
6221
+ }
6222
+ const tempBundle = join6(tmpdir3(), `claude-sync-bundle-${Date.now()}.tar.gz`);
6223
+ try {
6224
+ const spinner = createSpinner("Syncing...").start();
6225
+ const bundleResult = await createBundle(projectDir, manifest, tempBundle, (bytes, total, phase) => {
6226
+ if (phase === "compressing") {
6227
+ const percent = Math.round(bytes / total * 100);
6228
+ spinner.text = `Compressing files... ${dim(`${percent}%`)}`;
6229
+ }
6230
+ });
6231
+ const progress = createTransferProgress("Uploading", bundleResult.size, spinner);
6232
+ let prepareResponse;
6233
+ try {
6234
+ prepareResponse = await client.post("/api/sync/push/bundle/prepare", {
6235
+ deviceId,
6236
+ projectPath,
6237
+ projectId,
6238
+ manifest,
6239
+ bundleSize: bundleResult.size
6240
+ });
6241
+ } catch (err) {
6242
+ spinner.stop();
6243
+ if (err instanceof AuthExpiredError) throw err;
6244
+ printError(`Push failed: ${err instanceof Error ? err.message : "Unknown error"}`);
6245
+ return { fileCount: 0, projectId: projectId || "", failed: true };
6246
+ }
6247
+ await streamUpload(prepareResponse.uploadUrl, tempBundle, (bytes, total) => {
6248
+ progress.update(bytes);
6249
+ });
6250
+ try {
6251
+ await client.post("/api/sync/push/bundle/complete", {
6252
+ syncEventId: prepareResponse.syncEventId,
6253
+ manifest
6254
+ });
6255
+ } catch {
5567
6256
  }
5568
6257
  const compressionRatio = Math.round((1 - bundleResult.size / bundleResult.originalSize) * 100);
5569
6258
  progress.finish(`Pushed ${manifest.length} files ${dim(`(${formatBytes(bundleResult.size)}, ${compressionRatio}% smaller)`)}`);
5570
6259
  return { fileCount: manifest.length, projectId: prepareResponse.projectId, failed: false };
5571
6260
  } finally {
5572
6261
  try {
5573
- await rm(tempBundle, { force: true });
6262
+ await rm3(tempBundle, { force: true });
5574
6263
  } catch {
5575
6264
  }
5576
6265
  }
@@ -5605,7 +6294,7 @@ async function bundlePullPhase(client, deviceId, projectPath, projectDir, projec
5605
6294
  }
5606
6295
  return { fileCount, failed: false };
5607
6296
  }
5608
- const tempBundle = join4(tmpdir(), `claude-sync-bundle-${Date.now()}.tar.gz`);
6297
+ const tempBundle = join6(tmpdir3(), `claude-sync-bundle-${Date.now()}.tar.gz`);
5609
6298
  try {
5610
6299
  const progress = createTransferProgress("Downloading", prepareResponse.bundleSize, spinner);
5611
6300
  await streamDownload(prepareResponse.downloadUrl, tempBundle, prepareResponse.bundleSize, (bytes) => {
@@ -5625,7 +6314,7 @@ async function bundlePullPhase(client, deviceId, projectPath, projectDir, projec
5625
6314
  return { fileCount: extractedCount, failed: false };
5626
6315
  } finally {
5627
6316
  try {
5628
- await rm(tempBundle, { force: true });
6317
+ await rm3(tempBundle, { force: true });
5629
6318
  } catch {
5630
6319
  }
5631
6320
  }
@@ -5661,15 +6350,15 @@ async function crossMachineBundlePull(client, deviceId, projectPath, fromDeviceI
5661
6350
  }
5662
6351
  return { fileCount, failed: false };
5663
6352
  }
5664
- const targetDir = join4(projectsDir, foreignEncodedDir);
5665
- const tempBundle = join4(tmpdir(), `claude-sync-bundle-${Date.now()}.tar.gz`);
6353
+ const targetDir = join6(projectsDir, foreignEncodedDir);
6354
+ const tempBundle = join6(tmpdir3(), `claude-sync-bundle-${Date.now()}.tar.gz`);
5666
6355
  try {
5667
6356
  const progress = createTransferProgress("Downloading", prepareResponse.bundleSize, spinner);
5668
6357
  await streamDownload(prepareResponse.downloadUrl, tempBundle, prepareResponse.bundleSize, (bytes) => {
5669
6358
  progress.update(bytes);
5670
6359
  });
5671
6360
  spinner.text = "Extracting files...";
5672
- await mkdir3(targetDir, { recursive: true });
6361
+ await mkdir4(targetDir, { recursive: true });
5673
6362
  const extractedCount = await extractBundle(tempBundle, targetDir);
5674
6363
  try {
5675
6364
  const completeManifest = await walkDirectory(targetDir);
@@ -5683,134 +6372,93 @@ async function crossMachineBundlePull(client, deviceId, projectPath, fromDeviceI
5683
6372
  return { fileCount: extractedCount, failed: false };
5684
6373
  } finally {
5685
6374
  try {
5686
- await rm(tempBundle, { force: true });
6375
+ await rm3(tempBundle, { force: true });
5687
6376
  } catch {
5688
6377
  }
5689
6378
  }
5690
6379
  }
5691
- function syncCommand() {
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) => {
5693
- await runSync(options);
5694
- });
5695
- }
5696
- var import_utils3;
5697
- var init_sync = __esm({
5698
- "src/commands/sync.ts"() {
5699
- "use strict";
5700
- init_esm_shims();
5701
- init_config();
5702
- init_api_client();
5703
- init_sync_engine();
5704
- init_progress();
5705
- init_bundle();
5706
- import_utils3 = __toESM(require_dist(), 1);
5707
- init_theme();
6380
+ async function collaboratorBundlePull(client, deviceId, projectPath, projectId, canonicalPath, projectsDir, options) {
6381
+ const spinner = createSpinner("Preparing download...").start();
6382
+ let prepareResponse;
6383
+ try {
6384
+ prepareResponse = await client.post("/api/sync/pull/bundle/prepare", {
6385
+ deviceId,
6386
+ projectPath,
6387
+ projectId
6388
+ });
6389
+ } catch (err) {
6390
+ spinner.stop();
6391
+ if (err instanceof AuthExpiredError) throw err;
6392
+ printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
6393
+ return { fileCount: 0, failed: true };
5708
6394
  }
5709
- });
5710
-
5711
- // src/commands/status.ts
5712
- var status_exports = {};
5713
- __export(status_exports, {
5714
- statusCommand: () => statusCommand
5715
- });
5716
- import { Command as Command5 } from "commander";
5717
- function statusCommand() {
5718
- return new Command5("status").description("Show sync status for current project").action(async () => {
5719
- printIntro("Status");
5720
- const config = await loadConfig();
5721
- const cwd = process.cwd();
5722
- printLabel("API", config.apiUrl);
5723
- printLabel("Authenticated", isAuthenticated(config) ? success("yes") : error("no"));
5724
- printLabel("Device", config.deviceName || dim("none"));
5725
- printLabel("Project", brand(cwd));
5726
- const ctx = await resolveProjectState(cwd);
5727
- printLabel("State", ctx.state === "none" ? dim("no session data") : ctx.state === "symlink" ? `symlink \u2192 ${dim(ctx.symlinkTarget || "unknown")}` : "local");
5728
- if (!isAuthenticated(config) || ctx.state === "none") return;
5729
- const spinner = createSpinner("Scanning files...").start();
5730
- const savedManifest = await loadProjectManifest(cwd);
5731
- const currentManifest = await buildProjectManifest(cwd);
5732
- const diff = computeDiff(currentManifest, savedManifest);
6395
+ if (!prepareResponse.downloadUrl || !prepareResponse.bundleSize) {
5733
6396
  spinner.stop();
5734
- const totalSize = currentManifest.reduce((sum, e) => sum + e.size, 0);
5735
- printDivider();
5736
- printLabel("Local files", `${brand(String(currentManifest.length))} ${dim(`(${formatBytes(totalSize)})`)}`);
5737
- console.log();
5738
- printInfo("Changes since last sync:");
5739
- console.log(` ${success(`+${diff.added.length}`)} added ${warn(`~${diff.modified.length}`)} modified ${error(`-${diff.deleted.length}`)} deleted`);
5740
- console.log();
5741
- });
5742
- }
5743
- var init_status = __esm({
5744
- "src/commands/status.ts"() {
5745
- "use strict";
5746
- init_esm_shims();
5747
- init_config();
5748
- init_sync_engine();
5749
- init_progress();
5750
- init_theme();
6397
+ return { fileCount: 0, failed: false };
5751
6398
  }
5752
- });
5753
-
5754
- // src/commands/whoami.ts
5755
- var whoami_exports = {};
5756
- __export(whoami_exports, {
5757
- whoamiCommand: () => whoamiCommand
5758
- });
5759
- import { Command as Command6 } from "commander";
5760
- function whoamiCommand() {
5761
- return new Command6("whoami").description("Show current user and device info").action(async () => {
5762
- printIntro("Who Am I");
5763
- const config = await loadConfig();
5764
- if (!isAuthenticated(config)) {
5765
- printError("Not logged in. Run `claude-sync login` first.");
5766
- return;
6399
+ const fileCount = prepareResponse.manifest.length;
6400
+ if (options.dryRun) {
6401
+ spinner.stop();
6402
+ printInfo(`${brand(String(fileCount))} files to pull`);
6403
+ if (options.verbose) {
6404
+ for (const entry of prepareResponse.manifest) {
6405
+ console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
6406
+ }
5767
6407
  }
5768
- const client = new ApiClient(config.apiUrl);
5769
- const spinner = createSpinner("Fetching user info...").start();
6408
+ return { fileCount, failed: false };
6409
+ }
6410
+ const foreignEncoded = canonicalPath.replace(/\//g, "%2F");
6411
+ const targetDir = join6(projectsDir, foreignEncoded);
6412
+ const tempBundle = join6(tmpdir3(), `claude-sync-bundle-${Date.now()}.tar.gz`);
6413
+ try {
6414
+ const progress = createTransferProgress("Downloading", prepareResponse.bundleSize, spinner);
6415
+ await streamDownload(prepareResponse.downloadUrl, tempBundle, prepareResponse.bundleSize, (bytes) => {
6416
+ progress.update(bytes);
6417
+ });
6418
+ spinner.text = "Extracting files...";
6419
+ await mkdir4(targetDir, { recursive: true });
6420
+ const extractedCount = await extractBundle(tempBundle, targetDir);
5770
6421
  try {
5771
- const user = await client.get("/api/auth/me");
5772
- spinner.stop();
5773
- printLabel("User", `${user.name} ${dim(`(${user.email})`)}`);
5774
- printLabel("Plan", brand(user.plan));
5775
- printDivider();
5776
- printLabel("Device", config.deviceName || dim("none"));
5777
- printLabel("Device ID", config.deviceId || dim("not registered"));
5778
- } catch (err) {
5779
- spinner.stop();
5780
- if (err instanceof AuthExpiredError) throw err;
5781
- printLabel("Email", config.email || dim("unknown"));
5782
- printLabel("Device", config.deviceName || dim("none"));
6422
+ const completeManifest = await walkDirectory(targetDir);
6423
+ await client.post("/api/sync/pull/bundle/complete", {
6424
+ syncEventId: prepareResponse.syncEventId,
6425
+ manifest: completeManifest
6426
+ });
6427
+ } catch {
5783
6428
  }
5784
- console.log();
5785
- });
6429
+ progress.finish(`Pulled ${extractedCount} files ${dim(`(${formatBytes(prepareResponse.bundleSize)})`)}`);
6430
+ return { fileCount: extractedCount, failed: false };
6431
+ } finally {
6432
+ try {
6433
+ await rm3(tempBundle, { force: true });
6434
+ } catch {
6435
+ }
6436
+ }
5786
6437
  }
5787
- var init_whoami = __esm({
5788
- "src/commands/whoami.ts"() {
5789
- "use strict";
5790
- init_esm_shims();
5791
- init_config();
5792
- init_api_client();
5793
- init_progress();
5794
- init_theme();
6438
+ async function checkPushPermission2(client, projectId) {
6439
+ try {
6440
+ const myRole = await client.get(`/api/projects/${projectId}/my-role`);
6441
+ return { allowed: myRole.canPush, role: myRole.role };
6442
+ } catch {
6443
+ return { allowed: true, role: "owner" };
5795
6444
  }
5796
- });
6445
+ }
6446
+ function syncCommand() {
6447
+ return new Command7("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) => {
6448
+ await runSync(options);
6449
+ });
6450
+ }
5797
6451
 
5798
6452
  // src/index.ts
5799
- init_esm_shims();
5800
- init_login();
5801
- init_logout();
5802
- init_device();
5803
- init_sync();
5804
6453
  init_status();
5805
6454
  init_whoami();
5806
- import { Command as Command7 } from "commander";
5807
6455
 
5808
6456
  // src/commands/tui.ts
5809
6457
  init_esm_shims();
5810
6458
  init_theme();
5811
6459
  init_config();
5812
6460
  init_api_client();
5813
- import { select as select3, text as text3, isCancel as isCancel4 } from "@clack/prompts";
6461
+ import { select as select3, text as text4, isCancel as isCancel6 } from "@clack/prompts";
5814
6462
  import chalk3 from "chalk";
5815
6463
  function buildMenu(loggedIn) {
5816
6464
  if (!loggedIn) {
@@ -5819,7 +6467,10 @@ function buildMenu(loggedIn) {
5819
6467
  ];
5820
6468
  }
5821
6469
  return [
5822
- { value: "sync", label: "Sync", hint: "Sync current project" },
6470
+ { value: "quick-push", label: "Quick Push", hint: "Push without message" },
6471
+ { value: "push-message", label: "Push with Message", hint: "Add a commit message" },
6472
+ { value: "pull", label: "Pull", hint: "Get latest changes" },
6473
+ { value: "log", label: "View History", hint: "Show commit log" },
5823
6474
  { value: "status", label: "Status", hint: "Show sync state" },
5824
6475
  { value: "device:add", label: "Add Device", hint: "Register this machine" },
5825
6476
  { value: "device:list", label: "List Devices", hint: "Show registered devices" },
@@ -5861,7 +6512,7 @@ async function runTui() {
5861
6512
  message: brand("What would you like to do?"),
5862
6513
  options
5863
6514
  });
5864
- if (isCancel4(choice) || choice === "quit") {
6515
+ if (isCancel6(choice) || choice === "quit") {
5865
6516
  printOutro("Goodbye!");
5866
6517
  return;
5867
6518
  }
@@ -5905,21 +6556,33 @@ async function promptDeviceSelection(config) {
5905
6556
  hint: d.id === config.deviceId ? dim("current") : void 0
5906
6557
  }))
5907
6558
  });
5908
- if (isCancel4(choice)) return null;
6559
+ if (isCancel6(choice)) return null;
5909
6560
  return choice;
5910
6561
  }
5911
6562
  async function executeCommand(command) {
5912
6563
  const { loginCommand: loginCommand2 } = await Promise.resolve().then(() => (init_login(), login_exports));
5913
6564
  const { logoutCommand: logoutCommand2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
5914
6565
  const { deviceCommand: deviceCommand2 } = await Promise.resolve().then(() => (init_device(), device_exports));
5915
- const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
6566
+ const { pushCommand: pushCommand2 } = await Promise.resolve().then(() => (init_push(), push_exports));
6567
+ const { pullCommand: pullCommand2 } = await Promise.resolve().then(() => (init_pull(), pull_exports));
6568
+ const { logCommand: logCommand2 } = await Promise.resolve().then(() => (init_log(), log_exports));
5916
6569
  const { statusCommand: statusCommand2 } = await Promise.resolve().then(() => (init_status(), status_exports));
5917
6570
  const { whoamiCommand: whoamiCommand2 } = await Promise.resolve().then(() => (init_whoami(), whoami_exports));
5918
6571
  const config = await loadConfig();
5919
6572
  const handlers = {
5920
6573
  login: () => loginCommand2().parseAsync(["", ""]),
5921
6574
  logout: () => logoutCommand2().parseAsync(["", ""]),
5922
- sync: () => syncCommand2().parseAsync(["", ""]),
6575
+ "quick-push": () => pushCommand2().parseAsync(["", "", "-m", "Quick sync"]),
6576
+ "push-message": async () => {
6577
+ const message = await text4({
6578
+ message: brand("Commit message:"),
6579
+ placeholder: "Describe your changes..."
6580
+ });
6581
+ if (isCancel6(message)) return;
6582
+ await pushCommand2().parseAsync(["", "", "-m", message]);
6583
+ },
6584
+ pull: () => pullCommand2().parseAsync(["", "", "-y"]),
6585
+ log: () => logCommand2().parseAsync(["", ""]),
5923
6586
  status: () => statusCommand2().parseAsync(["", ""]),
5924
6587
  whoami: () => whoamiCommand2().parseAsync(["", ""]),
5925
6588
  "device:add": () => deviceCommand2().parseAsync(["", "", "add"]),
@@ -5930,8 +6593,8 @@ async function executeCommand(command) {
5930
6593
  await deviceCommand2().parseAsync(["", "", "remove", deviceId]);
5931
6594
  },
5932
6595
  "device:rename": async () => {
5933
- const name = await text3({ message: `${brand("New device name")}:` });
5934
- if (isCancel4(name)) return;
6596
+ const name = await text4({ message: `${brand("New device name")}:` });
6597
+ if (isCancel6(name)) return;
5935
6598
  await deviceCommand2().parseAsync(["", "", "rename", name]);
5936
6599
  }
5937
6600
  };
@@ -5944,7 +6607,7 @@ async function executeCommand(command) {
5944
6607
  // src/index.ts
5945
6608
  init_config();
5946
6609
  function run() {
5947
- const program = new Command7();
6610
+ const program = new Command10();
5948
6611
  program.name("claude-sync").description("Sync Claude Code sessions across machines \u2014 claude-sync.com").version("0.1.0").option("--menu", "Open interactive menu").action(async (options) => {
5949
6612
  if (options.menu) {
5950
6613
  await runTui();
@@ -5960,6 +6623,9 @@ function run() {
5960
6623
  program.addCommand(loginCommand());
5961
6624
  program.addCommand(logoutCommand());
5962
6625
  program.addCommand(deviceCommand());
6626
+ program.addCommand(pushCommand());
6627
+ program.addCommand(pullCommand());
6628
+ program.addCommand(logCommand());
5963
6629
  program.addCommand(syncCommand());
5964
6630
  program.addCommand(statusCommand());
5965
6631
  program.addCommand(whoamiCommand());