@claude-sync/cli 0.1.16 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4346,7 +4346,7 @@ var require_validation = __commonJS({
4346
4346
  "use strict";
4347
4347
  init_esm_shims();
4348
4348
  Object.defineProperty(exports, "__esModule", { value: true });
4349
- exports.createFeatureRequestCommentSchema = exports.updateFeatureRequestStatusSchema = exports.updateFeatureRequestSchema = exports.createFeatureRequestSchema = exports.featureRequestStatusSchema = exports.featureRequestTypeSchema = exports.updateProfileSchema = exports.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
+ 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.syncCompleteSchema = exports.downloadUrlSchema = exports.uploadUrlSchema = exports.pullRequestSchema = exports.pushManifestSchema = exports.updateDeviceSchema = exports.createDeviceSchema = exports.registerSchema = exports.loginSchema = void 0;
4350
4350
  var zod_1 = require_zod();
4351
4351
  exports.loginSchema = zod_1.z.object({
4352
4352
  email: zod_1.z.string().email(),
@@ -4440,6 +4440,15 @@ var require_validation = __commonJS({
4440
4440
  exports.createFeatureRequestCommentSchema = zod_1.z.object({
4441
4441
  body: zod_1.z.string().min(1).max(2e3)
4442
4442
  });
4443
+ exports.projectRoleSchema = zod_1.z.enum(["owner", "editor", "viewer"]);
4444
+ exports.editableRoleSchema = zod_1.z.enum(["editor", "viewer"]);
4445
+ exports.inviteCollaboratorSchema = zod_1.z.object({
4446
+ email: zod_1.z.string().email(),
4447
+ role: exports.editableRoleSchema
4448
+ });
4449
+ exports.updateCollaboratorRoleSchema = zod_1.z.object({
4450
+ role: exports.editableRoleSchema
4451
+ });
4443
4452
  }
4444
4453
  });
4445
4454
 
@@ -5365,6 +5374,11 @@ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, pr
5365
5374
  }
5366
5375
  devicesSpinner.stop();
5367
5376
  const otherDevices = devices.filter((d) => d.id !== config.deviceId);
5377
+ let sharedProjects = [];
5378
+ try {
5379
+ sharedProjects = await client.get("/api/projects/shared");
5380
+ } catch {
5381
+ }
5368
5382
  const menuOptions = [];
5369
5383
  if (hasLocalFiles) {
5370
5384
  menuOptions.push({
@@ -5380,6 +5394,13 @@ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, pr
5380
5394
  hint: dim("Pull sessions from a device")
5381
5395
  });
5382
5396
  }
5397
+ if (sharedProjects.length > 0) {
5398
+ menuOptions.push({
5399
+ value: "collaborator",
5400
+ label: "Sync from collaborator",
5401
+ hint: dim(`${sharedProjects.length} shared project(s) available`)
5402
+ });
5403
+ }
5383
5404
  if (menuOptions.length === 0) {
5384
5405
  printInfo("No local sessions and no other devices found.");
5385
5406
  printInfo("Use Claude Code in this directory first, then run sync.");
@@ -5405,6 +5426,65 @@ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, pr
5405
5426
  });
5406
5427
  return "pushed";
5407
5428
  }
5429
+ if (choice === "collaborator") {
5430
+ const sharedChoice = await select2({
5431
+ message: brand("Select shared project"),
5432
+ options: sharedProjects.map((p) => ({
5433
+ value: p.id,
5434
+ label: p.displayName || p.originalPath,
5435
+ hint: dim(`${p.owner.name} \xB7 ${p.role}`)
5436
+ }))
5437
+ });
5438
+ if (isCancel3(sharedChoice)) return "cancelled";
5439
+ const selectedShared = sharedProjects.find((p) => p.id === sharedChoice);
5440
+ const syncStatusSpinner = createSpinner("Checking sync status...").start();
5441
+ let syncStatus = null;
5442
+ try {
5443
+ syncStatus = await client.get(`/api/projects/${selectedShared.id}/sync-status`);
5444
+ } catch {
5445
+ }
5446
+ syncStatusSpinner.stop();
5447
+ if (syncStatus?.hasRecentSync && syncStatus.lastSyncedBy) {
5448
+ printInfo(brand("Recent sync detected"));
5449
+ printInfo(dim(`${syncStatus.lastSyncedBy.name} synced from ${syncStatus.lastSyncedBy.deviceName} recently`));
5450
+ const confirmChoice = await select2({
5451
+ message: brand("Continue anyway?"),
5452
+ options: [
5453
+ { value: "yes", label: "Yes, continue", hint: dim("Pull the latest version") },
5454
+ { value: "no", label: "Cancel", hint: dim("Wait for them to finish") }
5455
+ ]
5456
+ });
5457
+ if (isCancel3(confirmChoice) || confirmChoice === "no") return "cancelled";
5458
+ }
5459
+ const existingCheck2 = await handleExistingSessionDir(projectsDir, ctx.encodedPath);
5460
+ if (existingCheck2 === "cancelled") {
5461
+ return "cancelled";
5462
+ }
5463
+ const result2 = await collaboratorBundlePull(
5464
+ client,
5465
+ config.deviceId,
5466
+ cwd,
5467
+ selectedShared.id,
5468
+ selectedShared.canonicalPath,
5469
+ projectsDir,
5470
+ options
5471
+ );
5472
+ if (result2.fileCount === 0 && !options.dryRun) {
5473
+ printInfo("No files to download.");
5474
+ return "cancelled";
5475
+ }
5476
+ if (!options.dryRun) {
5477
+ const foreignEncoded = selectedShared.canonicalPath.replace(/\//g, "%2F");
5478
+ await createProjectSymlink(projectsDir, ctx.encodedPath, foreignEncoded);
5479
+ await saveProjectLink(cwd, {
5480
+ projectId: selectedShared.id,
5481
+ foreignEncodedDir: foreignEncoded,
5482
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
5483
+ });
5484
+ printInfo(`Synced from: ${dim(`${selectedShared.owner.name}'s ${selectedShared.displayName}`)}`);
5485
+ }
5486
+ return "pulled";
5487
+ }
5408
5488
  const deviceChoice = await select2({
5409
5489
  message: brand("Select device"),
5410
5490
  options: otherDevices.map((d) => ({
@@ -5521,6 +5601,14 @@ async function bundlePushPhase(client, deviceId, projectPath, manifest, projectD
5521
5601
  if (manifest.length === 0) {
5522
5602
  return { fileCount: 0, projectId: projectId || "", failed: false };
5523
5603
  }
5604
+ if (projectId) {
5605
+ const permission = await checkPushPermission(client, projectId);
5606
+ if (!permission.allowed) {
5607
+ printError(`Cannot push: You have ${brand(permission.role)} access to this project.`);
5608
+ printInfo("Only project editors and owners can push changes.");
5609
+ return { fileCount: 0, projectId, failed: true };
5610
+ }
5611
+ }
5524
5612
  const totalBytes = manifest.reduce((sum, f) => sum + f.size, 0);
5525
5613
  printInfo(`${brand(String(manifest.length))} files ${dim(`(${formatBytes(totalBytes)})`)} to push`);
5526
5614
  if (options.dryRun) {
@@ -5689,6 +5777,72 @@ async function crossMachineBundlePull(client, deviceId, projectPath, fromDeviceI
5689
5777
  }
5690
5778
  }
5691
5779
  }
5780
+ async function collaboratorBundlePull(client, deviceId, projectPath, projectId, canonicalPath, projectsDir, options) {
5781
+ const spinner = createSpinner("Preparing download...").start();
5782
+ let prepareResponse;
5783
+ try {
5784
+ prepareResponse = await client.post("/api/sync/pull/bundle/prepare", {
5785
+ deviceId,
5786
+ projectPath,
5787
+ projectId
5788
+ });
5789
+ } catch (err) {
5790
+ spinner.stop();
5791
+ if (err instanceof AuthExpiredError) throw err;
5792
+ printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
5793
+ return { fileCount: 0, failed: true };
5794
+ }
5795
+ if (!prepareResponse.downloadUrl || !prepareResponse.bundleSize) {
5796
+ spinner.stop();
5797
+ return { fileCount: 0, failed: false };
5798
+ }
5799
+ const fileCount = prepareResponse.manifest.length;
5800
+ if (options.dryRun) {
5801
+ spinner.stop();
5802
+ printInfo(`${brand(String(fileCount))} files to pull`);
5803
+ if (options.verbose) {
5804
+ for (const entry of prepareResponse.manifest) {
5805
+ console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
5806
+ }
5807
+ }
5808
+ return { fileCount, failed: false };
5809
+ }
5810
+ const foreignEncoded = canonicalPath.replace(/\//g, "%2F");
5811
+ const targetDir = join4(projectsDir, foreignEncoded);
5812
+ const tempBundle = join4(tmpdir(), `claude-sync-bundle-${Date.now()}.tar.gz`);
5813
+ try {
5814
+ const progress = createTransferProgress("Downloading", prepareResponse.bundleSize, spinner);
5815
+ await streamDownload(prepareResponse.downloadUrl, tempBundle, prepareResponse.bundleSize, (bytes) => {
5816
+ progress.update(bytes);
5817
+ });
5818
+ spinner.text = "Extracting files...";
5819
+ await mkdir3(targetDir, { recursive: true });
5820
+ const extractedCount = await extractBundle(tempBundle, targetDir);
5821
+ try {
5822
+ const completeManifest = await walkDirectory(targetDir);
5823
+ await client.post("/api/sync/pull/bundle/complete", {
5824
+ syncEventId: prepareResponse.syncEventId,
5825
+ manifest: completeManifest
5826
+ });
5827
+ } catch {
5828
+ }
5829
+ progress.finish(`Pulled ${extractedCount} files ${dim(`(${formatBytes(prepareResponse.bundleSize)})`)}`);
5830
+ return { fileCount: extractedCount, failed: false };
5831
+ } finally {
5832
+ try {
5833
+ await rm(tempBundle, { force: true });
5834
+ } catch {
5835
+ }
5836
+ }
5837
+ }
5838
+ async function checkPushPermission(client, projectId) {
5839
+ try {
5840
+ const myRole = await client.get(`/api/projects/${projectId}/my-role`);
5841
+ return { allowed: myRole.canPush, role: myRole.role };
5842
+ } catch {
5843
+ return { allowed: true, role: "owner" };
5844
+ }
5845
+ }
5692
5846
  function syncCommand() {
5693
5847
  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) => {
5694
5848
  await runSync(options);