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