@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/bin/claude-sync.js +1036 -370
- package/dist/bin/claude-sync.js.map +1 -1
- package/dist/src/index.js +1033 -367
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
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 =
|
|
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
|
|
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
|
|
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(
|
|
4697
|
-
return ora({ text: brandColor(
|
|
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/
|
|
5274
|
-
var
|
|
5275
|
-
__export(
|
|
5276
|
-
|
|
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 {
|
|
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
|
-
|
|
5285
|
-
|
|
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 (
|
|
5310
|
-
|
|
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(
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
|
5417
|
+
return;
|
|
5557
5418
|
}
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
5665
|
-
const tempBundle =
|
|
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
|
|
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
|
|
6375
|
+
await rm3(tempBundle, { force: true });
|
|
5687
6376
|
} catch {
|
|
5688
6377
|
}
|
|
5689
6378
|
}
|
|
5690
6379
|
}
|
|
5691
|
-
function
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
});
|
|
5759
|
-
|
|
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
|
-
|
|
5769
|
-
|
|
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
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
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
|
|
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: "
|
|
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 (
|
|
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 (
|
|
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 {
|
|
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
|
-
|
|
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
|
|
5934
|
-
if (
|
|
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
|
|
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());
|