@claude-sync/cli 0.1.0

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.
@@ -0,0 +1,1298 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/lib/config.ts
12
+ import { join, dirname } from "path";
13
+ import { homedir } from "os";
14
+ import { readFile, writeFile, mkdir } from "fs/promises";
15
+ import { CONFIG_DIR, CONFIG_FILE, MANIFEST_FILE, encodePath } from "@claude-sync/utils";
16
+ function getConfigDir() {
17
+ return join(homedir(), CONFIG_DIR);
18
+ }
19
+ function getConfigPath() {
20
+ return join(getConfigDir(), CONFIG_FILE);
21
+ }
22
+ async function loadConfig() {
23
+ try {
24
+ const content = await readFile(getConfigPath(), "utf-8");
25
+ return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
26
+ } catch {
27
+ return { ...DEFAULT_CONFIG };
28
+ }
29
+ }
30
+ async function saveConfig(config) {
31
+ const existing = await loadConfig();
32
+ const merged = { ...existing, ...config };
33
+ await mkdir(getConfigDir(), { recursive: true });
34
+ await writeFile(getConfigPath(), JSON.stringify(merged, null, 2));
35
+ }
36
+ async function clearConfig() {
37
+ await saveConfig(DEFAULT_CONFIG);
38
+ }
39
+ async function loadProjectManifest(projectPath) {
40
+ try {
41
+ const encoded = encodePath(projectPath);
42
+ const manifestPath = join(getConfigDir(), "manifests", `${encoded}.json`);
43
+ const content = await readFile(manifestPath, "utf-8");
44
+ return JSON.parse(content);
45
+ } catch {
46
+ return [];
47
+ }
48
+ }
49
+ async function saveProjectManifest(projectPath, entries) {
50
+ const encoded = encodePath(projectPath);
51
+ const manifestPath = join(getConfigDir(), "manifests", `${encoded}.json`);
52
+ await mkdir(dirname(manifestPath), { recursive: true });
53
+ await writeFile(manifestPath, JSON.stringify(entries, null, 2));
54
+ }
55
+ function isAuthenticated(config) {
56
+ return config.accessToken !== null;
57
+ }
58
+ async function loadProjectLink(projectPath) {
59
+ try {
60
+ const encoded = encodePath(projectPath);
61
+ const linkPath = join(getConfigDir(), "project-links", `${encoded}.json`);
62
+ const content = await readFile(linkPath, "utf-8");
63
+ return JSON.parse(content);
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ async function saveProjectLink(projectPath, link) {
69
+ const encoded = encodePath(projectPath);
70
+ const linkPath = join(getConfigDir(), "project-links", `${encoded}.json`);
71
+ await mkdir(dirname(linkPath), { recursive: true });
72
+ await writeFile(linkPath, JSON.stringify(link, null, 2));
73
+ }
74
+ var DEFAULT_CONFIG;
75
+ var init_config = __esm({
76
+ "src/lib/config.ts"() {
77
+ "use strict";
78
+ DEFAULT_CONFIG = {
79
+ apiUrl: "https://api.claude-sync.com",
80
+ accessToken: null,
81
+ refreshToken: null,
82
+ deviceId: null,
83
+ deviceName: null,
84
+ userId: null,
85
+ email: null
86
+ };
87
+ }
88
+ });
89
+
90
+ // src/lib/api-client.ts
91
+ var ApiClient;
92
+ var init_api_client = __esm({
93
+ "src/lib/api-client.ts"() {
94
+ "use strict";
95
+ init_auth();
96
+ ApiClient = class {
97
+ baseUrl;
98
+ constructor(baseUrl) {
99
+ this.baseUrl = baseUrl.replace(/\/$/, "");
100
+ }
101
+ async rawRequest(path, options = {}) {
102
+ const controller = new AbortController();
103
+ const timeout = setTimeout(() => controller.abort(), 15e3);
104
+ let response;
105
+ try {
106
+ response = await fetch(`${this.baseUrl}${path}`, {
107
+ ...options,
108
+ signal: controller.signal
109
+ });
110
+ } catch (err) {
111
+ clearTimeout(timeout);
112
+ if (err instanceof Error && err.name === "AbortError") {
113
+ throw new Error("Request timed out. Is the API server running?");
114
+ }
115
+ throw err;
116
+ }
117
+ clearTimeout(timeout);
118
+ if (!response.ok) {
119
+ if (response.status === 401) {
120
+ throw new Error("Session expired. Run `claude-sync login` to re-authenticate.");
121
+ }
122
+ const error2 = await response.json().catch(() => ({
123
+ statusCode: response.status,
124
+ message: response.statusText,
125
+ error: "Request failed"
126
+ }));
127
+ throw new Error(error2.message);
128
+ }
129
+ const json = await response.json();
130
+ return json.data !== void 0 ? json.data : json;
131
+ }
132
+ async request(path, options = {}) {
133
+ const token = await getValidToken();
134
+ const headers = {
135
+ "Content-Type": "application/json",
136
+ ...options.headers
137
+ };
138
+ if (token) {
139
+ headers["Authorization"] = `Bearer ${token}`;
140
+ }
141
+ return this.rawRequest(path, { ...options, headers });
142
+ }
143
+ async requestNoAuth(path, options = {}) {
144
+ const headers = {
145
+ "Content-Type": "application/json",
146
+ ...options.headers
147
+ };
148
+ return this.rawRequest(path, { ...options, headers });
149
+ }
150
+ async get(path) {
151
+ return this.request(path, { method: "GET" });
152
+ }
153
+ async post(path, body) {
154
+ return this.request(path, {
155
+ method: "POST",
156
+ body: body ? JSON.stringify(body) : void 0
157
+ });
158
+ }
159
+ async patch(path, body) {
160
+ return this.request(path, {
161
+ method: "PATCH",
162
+ body: body ? JSON.stringify(body) : void 0
163
+ });
164
+ }
165
+ async delete(path) {
166
+ return this.request(path, { method: "DELETE" });
167
+ }
168
+ };
169
+ }
170
+ });
171
+
172
+ // src/lib/auth.ts
173
+ async function storeTokens(tokens, userId, email) {
174
+ await saveConfig({
175
+ accessToken: tokens.accessToken,
176
+ refreshToken: tokens.refreshToken,
177
+ userId,
178
+ email
179
+ });
180
+ }
181
+ async function refreshAccessToken() {
182
+ const config = await loadConfig();
183
+ if (!config.refreshToken) return null;
184
+ const client = new ApiClient(config.apiUrl);
185
+ try {
186
+ const response = await client.requestNoAuth("/api/auth/refresh", {
187
+ method: "POST",
188
+ body: JSON.stringify({ refreshToken: config.refreshToken })
189
+ });
190
+ await saveConfig({
191
+ accessToken: response.accessToken,
192
+ refreshToken: response.refreshToken
193
+ });
194
+ return response.accessToken;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+ async function getValidToken() {
200
+ const config = await loadConfig();
201
+ if (!config.accessToken) return null;
202
+ try {
203
+ const payload = JSON.parse(
204
+ Buffer.from(config.accessToken.split(".")[1], "base64").toString()
205
+ );
206
+ if (payload.exp * 1e3 > Date.now()) {
207
+ return config.accessToken;
208
+ }
209
+ } catch {
210
+ }
211
+ return refreshAccessToken();
212
+ }
213
+ var init_auth = __esm({
214
+ "src/lib/auth.ts"() {
215
+ "use strict";
216
+ init_config();
217
+ init_api_client();
218
+ }
219
+ });
220
+
221
+ // src/lib/progress.ts
222
+ import ora from "ora";
223
+ import chalk from "chalk";
224
+ function createSpinner(text3) {
225
+ return ora({ text: brandColor(text3), spinner: "dots", color: "magenta" });
226
+ }
227
+ function formatBytes(bytes) {
228
+ if (bytes === 0) return "0 B";
229
+ const k = 1024;
230
+ const sizes = ["B", "KB", "MB", "GB"];
231
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
232
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
233
+ }
234
+ function formatProgress(current, total) {
235
+ const percentage = total > 0 ? Math.round(current / total * 100) : 0;
236
+ return `${current}/${total} (${percentage}%)`;
237
+ }
238
+ var brandColor;
239
+ var init_progress = __esm({
240
+ "src/lib/progress.ts"() {
241
+ "use strict";
242
+ brandColor = chalk.hex("#8a8cdd");
243
+ }
244
+ });
245
+
246
+ // src/lib/theme.ts
247
+ import chalk2 from "chalk";
248
+ function printBanner() {
249
+ console.log();
250
+ console.log(` ${brandBold("claude-sync")} ${dim("v0.1.0")}`);
251
+ console.log();
252
+ }
253
+ function printIntro(title) {
254
+ console.log();
255
+ console.log(` ${brandBold(title)}`);
256
+ console.log();
257
+ }
258
+ function printOutro(message) {
259
+ console.log();
260
+ console.log(` ${dim(message)}`);
261
+ console.log();
262
+ }
263
+ function printSuccess(message) {
264
+ console.log(` ${success("\u2714")} ${message}`);
265
+ }
266
+ function printError(message) {
267
+ console.log(` ${error("\u2716")} ${message}`);
268
+ }
269
+ function printInfo(message) {
270
+ console.log(` ${brand("\u25CF")} ${message}`);
271
+ }
272
+ function printLabel(label, value) {
273
+ console.log(` ${dim(label.padEnd(14))} ${white(value)}`);
274
+ }
275
+ function printDivider() {
276
+ console.log();
277
+ }
278
+ var BRAND_HEX, BRAND_DIM_HEX, SUCCESS_HEX, WARN_HEX, ERROR_HEX, brand, brandBold, brandDim, success, successBold, warn, error, errorBold, dim, bold, white;
279
+ var init_theme = __esm({
280
+ "src/lib/theme.ts"() {
281
+ "use strict";
282
+ BRAND_HEX = "#8a8cdd";
283
+ BRAND_DIM_HEX = "#6b6db8";
284
+ SUCCESS_HEX = "#8adda0";
285
+ WARN_HEX = "#ddcc8a";
286
+ ERROR_HEX = "#dd8a8a";
287
+ brand = chalk2.hex(BRAND_HEX);
288
+ brandBold = chalk2.hex(BRAND_HEX).bold;
289
+ brandDim = chalk2.hex(BRAND_DIM_HEX);
290
+ success = chalk2.hex(SUCCESS_HEX);
291
+ successBold = chalk2.hex(SUCCESS_HEX).bold;
292
+ warn = chalk2.hex(WARN_HEX);
293
+ error = chalk2.hex(ERROR_HEX);
294
+ errorBold = chalk2.hex(ERROR_HEX).bold;
295
+ dim = chalk2.dim;
296
+ bold = chalk2.bold;
297
+ white = chalk2.white;
298
+ }
299
+ });
300
+
301
+ // src/commands/login.ts
302
+ var login_exports = {};
303
+ __export(login_exports, {
304
+ loginCommand: () => loginCommand
305
+ });
306
+ import { Command } from "commander";
307
+ import { text, password as passwordPrompt, select, isCancel } from "@clack/prompts";
308
+ async function loginWithBrowser(client) {
309
+ const response = await client.requestNoAuth("/api/auth/cli/initiate", {
310
+ method: "POST",
311
+ headers: { "Content-Type": "application/json" }
312
+ });
313
+ printInfo(`Open this URL in your browser:
314
+ ${brandBold(response.verifyUrl)}`);
315
+ printInfo(`Code: ${brandBold(response.code)}`);
316
+ const spinner = createSpinner("Waiting for browser authorization...").start();
317
+ const pollInterval = 2e3;
318
+ const maxAttempts = 150;
319
+ let attempts = 0;
320
+ while (attempts < maxAttempts) {
321
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
322
+ attempts++;
323
+ try {
324
+ const pollResponse = await client.requestNoAuth("/api/auth/cli/poll", {
325
+ method: "POST",
326
+ headers: { "Content-Type": "application/json" },
327
+ body: JSON.stringify({ code: response.code })
328
+ });
329
+ if (pollResponse.status === "authorized" && pollResponse.tokenPair && pollResponse.user) {
330
+ spinner.stop();
331
+ await storeTokens(pollResponse.tokenPair, pollResponse.user.id, pollResponse.user.email);
332
+ printSuccess(`Logged in as ${brandBold(pollResponse.user.email)}`);
333
+ return;
334
+ }
335
+ if (pollResponse.status === "expired") {
336
+ spinner.stop();
337
+ printError("Authorization code expired. Please try again.");
338
+ process.exit(1);
339
+ }
340
+ } catch {
341
+ }
342
+ }
343
+ spinner.stop();
344
+ printError("Authorization timed out. Please try again.");
345
+ process.exit(1);
346
+ }
347
+ async function loginWithEmail(client) {
348
+ const email = await text({ message: `${brand("Email")}:` });
349
+ if (isCancel(email)) process.exit(0);
350
+ const password = await passwordPrompt({ message: `${brand("Password")}:` });
351
+ if (isCancel(password)) process.exit(0);
352
+ const spinner = createSpinner("Logging in...").start();
353
+ try {
354
+ const response = await client.requestNoAuth(
355
+ "/api/auth/login",
356
+ {
357
+ method: "POST",
358
+ headers: { "Content-Type": "application/json" },
359
+ body: JSON.stringify({ email, password })
360
+ }
361
+ );
362
+ spinner.stop();
363
+ await storeTokens(
364
+ { accessToken: response.accessToken, refreshToken: response.refreshToken, expiresIn: 900 },
365
+ response.user.id,
366
+ response.user.email
367
+ );
368
+ printSuccess(`Logged in as ${brandBold(response.user.email)}`);
369
+ } catch (err) {
370
+ spinner.stop();
371
+ printError(`Login failed: ${err instanceof Error ? err.message : "Unknown error"}`);
372
+ process.exit(1);
373
+ }
374
+ }
375
+ function loginCommand() {
376
+ return new Command("login").description("Authenticate with claude-sync").action(async () => {
377
+ printIntro("Login");
378
+ const config = await loadConfig();
379
+ const client = new ApiClient(config.apiUrl);
380
+ const method = await select({
381
+ message: `${brand("How would you like to log in?")}`,
382
+ options: [
383
+ { value: "browser", label: `${brand("\u25CB")} Browser ${dim("(recommended)")}` },
384
+ { value: "email", label: `${brand("\u25CB")} Email & password` }
385
+ ]
386
+ });
387
+ if (isCancel(method)) process.exit(0);
388
+ if (method === "browser") {
389
+ await loginWithBrowser(client);
390
+ } else {
391
+ await loginWithEmail(client);
392
+ }
393
+ printOutro("Authentication complete");
394
+ });
395
+ }
396
+ var init_login = __esm({
397
+ "src/commands/login.ts"() {
398
+ "use strict";
399
+ init_config();
400
+ init_auth();
401
+ init_api_client();
402
+ init_progress();
403
+ init_theme();
404
+ }
405
+ });
406
+
407
+ // src/commands/logout.ts
408
+ var logout_exports = {};
409
+ __export(logout_exports, {
410
+ logoutCommand: () => logoutCommand
411
+ });
412
+ import { Command as Command2 } from "commander";
413
+ function logoutCommand() {
414
+ return new Command2("logout").description("Log out and revoke tokens").action(async () => {
415
+ printIntro("Logout");
416
+ const config = await loadConfig();
417
+ if (!config.accessToken) {
418
+ printInfo("Not currently logged in.");
419
+ printOutro("Done");
420
+ return;
421
+ }
422
+ try {
423
+ const client = new ApiClient(config.apiUrl);
424
+ await client.post("/api/auth/logout", {
425
+ refreshToken: config.refreshToken
426
+ });
427
+ } catch {
428
+ }
429
+ await clearConfig();
430
+ printSuccess("Logged out successfully.");
431
+ printOutro("Done");
432
+ });
433
+ }
434
+ var init_logout = __esm({
435
+ "src/commands/logout.ts"() {
436
+ "use strict";
437
+ init_config();
438
+ init_api_client();
439
+ init_theme();
440
+ }
441
+ });
442
+
443
+ // src/commands/device.ts
444
+ var device_exports = {};
445
+ __export(device_exports, {
446
+ deviceCommand: () => deviceCommand
447
+ });
448
+ import { Command as Command3 } from "commander";
449
+ import { hostname, platform, arch, homedir as homedir2 } from "os";
450
+ import { text as text2, isCancel as isCancel2 } from "@clack/prompts";
451
+ function requireAuth(config) {
452
+ if (!isAuthenticated(config)) {
453
+ printError("Not logged in. Run `claude-sync login` first.");
454
+ process.exit(1);
455
+ }
456
+ }
457
+ function deviceCommand() {
458
+ const device = new Command3("device").description("Manage devices");
459
+ device.command("add").argument("[name]", "Device name").description("Register this machine as a device").action(async (name) => {
460
+ printIntro("Device Add");
461
+ const config = await loadConfig();
462
+ requireAuth(config);
463
+ const deviceName = name || await (async () => {
464
+ const input = await text2({ message: `${brand("Device name")}:`, initialValue: hostname() });
465
+ if (isCancel2(input)) process.exit(0);
466
+ return input;
467
+ })();
468
+ const spinner = createSpinner("Registering device...").start();
469
+ const client = new ApiClient(config.apiUrl);
470
+ try {
471
+ const response = await client.post("/api/devices", {
472
+ name: deviceName,
473
+ hostname: hostname(),
474
+ platform: platform(),
475
+ homeDir: homedir2(),
476
+ arch: arch()
477
+ });
478
+ await saveConfig({
479
+ deviceId: response.id,
480
+ deviceName: response.name
481
+ });
482
+ spinner.stop();
483
+ printSuccess(`Device ${brandBold(`"${response.name}"`)} registered`);
484
+ printLabel("ID", response.id);
485
+ } catch (err) {
486
+ spinner.stop();
487
+ printError(`Failed: ${err instanceof Error ? err.message : "Unknown error"}`);
488
+ process.exit(1);
489
+ }
490
+ printOutro("Done");
491
+ });
492
+ device.command("list").description("List all registered devices").action(async () => {
493
+ printIntro("Devices");
494
+ const config = await loadConfig();
495
+ requireAuth(config);
496
+ const client = new ApiClient(config.apiUrl);
497
+ const spinner = createSpinner("Fetching devices...").start();
498
+ try {
499
+ const devices = await client.get("/api/devices");
500
+ spinner.stop();
501
+ if (devices.length === 0) {
502
+ printInfo("No devices registered.");
503
+ return;
504
+ }
505
+ console.log();
506
+ for (const d of devices) {
507
+ const current = d.id === config.deviceId ? ` ${brandBold("(current)")}` : "";
508
+ const active = d.isActive ? success("active") : error("inactive");
509
+ console.log(` ${brand("\u25CB")} ${white(d.name)}${current}`);
510
+ console.log(` ${dim(d.platform)} ${dim("\xB7")} ${active} ${dim("\xB7")} ${dim(d.id)}`);
511
+ }
512
+ console.log();
513
+ } catch (err) {
514
+ spinner.stop();
515
+ printError(`Failed: ${err instanceof Error ? err.message : "Unknown error"}`);
516
+ process.exit(1);
517
+ }
518
+ });
519
+ device.command("remove").argument("<nameOrId>", "Device name or ID").description("Remove a device").action(async (nameOrId) => {
520
+ const config = await loadConfig();
521
+ requireAuth(config);
522
+ const client = new ApiClient(config.apiUrl);
523
+ const spinner = createSpinner("Removing device...").start();
524
+ try {
525
+ await client.delete(`/api/devices/${nameOrId}`);
526
+ spinner.stop();
527
+ if (config.deviceId === nameOrId) {
528
+ await saveConfig({ deviceId: null, deviceName: null });
529
+ }
530
+ printSuccess("Device removed.");
531
+ } catch (err) {
532
+ spinner.stop();
533
+ printError(`Failed: ${err instanceof Error ? err.message : "Unknown error"}`);
534
+ process.exit(1);
535
+ }
536
+ });
537
+ device.command("rename").argument("<name>", "New name").description("Rename the current device").action(async (name) => {
538
+ const config = await loadConfig();
539
+ requireAuth(config);
540
+ if (!config.deviceId) {
541
+ printError("No device registered. Run `claude-sync device add` first.");
542
+ process.exit(1);
543
+ }
544
+ const client = new ApiClient(config.apiUrl);
545
+ const spinner = createSpinner("Renaming device...").start();
546
+ try {
547
+ const response = await client.patch(`/api/devices/${config.deviceId}`, { name });
548
+ await saveConfig({ deviceName: response.name });
549
+ spinner.stop();
550
+ printSuccess(`Device renamed to ${brandBold(`"${response.name}"`)}`);
551
+ } catch (err) {
552
+ spinner.stop();
553
+ printError(`Failed: ${err instanceof Error ? err.message : "Unknown error"}`);
554
+ process.exit(1);
555
+ }
556
+ });
557
+ return device;
558
+ }
559
+ var init_device = __esm({
560
+ "src/commands/device.ts"() {
561
+ "use strict";
562
+ init_config();
563
+ init_api_client();
564
+ init_progress();
565
+ init_theme();
566
+ }
567
+ });
568
+
569
+ // src/lib/sync-engine.ts
570
+ import { join as join2 } from "path";
571
+ import { homedir as homedir3 } from "os";
572
+ import { readdir, stat, lstat, readlink } from "fs/promises";
573
+ import { hashFile, diffManifests, encodePath as encodePath2, CLAUDE_DIR, PROJECTS_DIR, SYNC_EXCLUDE_PATTERNS } from "@claude-sync/utils";
574
+ function shouldExclude(name) {
575
+ return SYNC_EXCLUDE_PATTERNS.some((pattern) => {
576
+ if (pattern.startsWith("*")) {
577
+ return name.endsWith(pattern.slice(1));
578
+ }
579
+ return name === pattern;
580
+ });
581
+ }
582
+ async function walkDirectory(dir, basePath = "") {
583
+ const entries = [];
584
+ const items = await readdir(dir, { withFileTypes: true });
585
+ for (const item of items) {
586
+ if (shouldExclude(item.name)) continue;
587
+ const fullPath = join2(dir, item.name);
588
+ const relativePath = basePath ? `${basePath}/${item.name}` : item.name;
589
+ if (item.isDirectory()) {
590
+ const subEntries = await walkDirectory(fullPath, relativePath);
591
+ entries.push(...subEntries);
592
+ } else if (item.isFile()) {
593
+ const fileStat = await stat(fullPath);
594
+ const hash = await hashFile(fullPath);
595
+ const isText = !isBinaryPath(item.name);
596
+ entries.push({
597
+ path: relativePath,
598
+ hash,
599
+ size: fileStat.size,
600
+ modifiedAt: fileStat.mtime.toISOString(),
601
+ isCompressed: isText
602
+ });
603
+ }
604
+ }
605
+ return entries;
606
+ }
607
+ function isBinaryPath(name) {
608
+ const binaryExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".pdf", ".zip", ".gz", ".tar"];
609
+ return binaryExtensions.some((ext) => name.endsWith(ext));
610
+ }
611
+ async function resolveProjectState(cwd) {
612
+ const projectsDir = join2(homedir3(), CLAUDE_DIR, PROJECTS_DIR);
613
+ const encoded = encodePath2(cwd);
614
+ const projectDir = join2(projectsDir, encoded);
615
+ let state = "none";
616
+ let symlinkTarget;
617
+ try {
618
+ const stats = await lstat(projectDir);
619
+ if (stats.isSymbolicLink()) {
620
+ state = "symlink";
621
+ symlinkTarget = await readlink(projectDir);
622
+ } else if (stats.isDirectory()) {
623
+ state = "real";
624
+ }
625
+ } catch {
626
+ state = "none";
627
+ }
628
+ return { cwd, encodedPath: encoded, projectDir, state, symlinkTarget };
629
+ }
630
+ async function buildProjectManifest(cwd) {
631
+ const ctx = await resolveProjectState(cwd);
632
+ if (ctx.state === "none") return [];
633
+ const projectsDir = join2(homedir3(), CLAUDE_DIR, PROJECTS_DIR);
634
+ let targetDir = ctx.projectDir;
635
+ if (ctx.state === "symlink" && ctx.symlinkTarget) {
636
+ targetDir = join2(projectsDir, ctx.symlinkTarget);
637
+ }
638
+ try {
639
+ return await walkDirectory(targetDir);
640
+ } catch {
641
+ return [];
642
+ }
643
+ }
644
+ function computeDiff(local, remote) {
645
+ return diffManifests(local, remote);
646
+ }
647
+ var init_sync_engine = __esm({
648
+ "src/lib/sync-engine.ts"() {
649
+ "use strict";
650
+ }
651
+ });
652
+
653
+ // src/lib/compress.ts
654
+ import { createGzip, createGunzip } from "zlib";
655
+ import { createReadStream, createWriteStream } from "fs";
656
+ import { pipeline } from "stream/promises";
657
+ function compressBuffer(data) {
658
+ return new Promise((resolve, reject) => {
659
+ const gzip = createGzip();
660
+ const chunks = [];
661
+ gzip.on("data", (chunk) => chunks.push(chunk));
662
+ gzip.on("end", () => resolve(Buffer.concat(chunks)));
663
+ gzip.on("error", reject);
664
+ gzip.end(data);
665
+ });
666
+ }
667
+ function decompressBuffer(data) {
668
+ return new Promise((resolve, reject) => {
669
+ const gunzip = createGunzip();
670
+ const chunks = [];
671
+ gunzip.on("data", (chunk) => chunks.push(chunk));
672
+ gunzip.on("end", () => resolve(Buffer.concat(chunks)));
673
+ gunzip.on("error", reject);
674
+ gunzip.end(data);
675
+ });
676
+ }
677
+ var init_compress = __esm({
678
+ "src/lib/compress.ts"() {
679
+ "use strict";
680
+ }
681
+ });
682
+
683
+ // src/commands/sync.ts
684
+ var sync_exports = {};
685
+ __export(sync_exports, {
686
+ runSync: () => runSync,
687
+ syncCommand: () => syncCommand
688
+ });
689
+ import { Command as Command4 } from "commander";
690
+ import { select as select2, isCancel as isCancel3 } from "@clack/prompts";
691
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, lstat as lstat2, symlink, unlink } from "fs/promises";
692
+ import { join as join3, dirname as dirname2 } from "path";
693
+ import { homedir as homedir4 } from "os";
694
+ import { CLAUDE_DIR as CLAUDE_DIR2, PROJECTS_DIR as PROJECTS_DIR2 } from "@claude-sync/utils";
695
+ async function runSync(options) {
696
+ printIntro("Sync");
697
+ const config = await loadConfig();
698
+ if (!isAuthenticated(config)) {
699
+ printError("Not logged in. Run `claude-sync login` first.");
700
+ return;
701
+ }
702
+ if (!config.deviceId) {
703
+ printError("No device registered. Run `claude-sync device add` first.");
704
+ return;
705
+ }
706
+ const cwd = process.cwd();
707
+ const ctx = await resolveProjectState(cwd);
708
+ const client = new ApiClient(config.apiUrl);
709
+ const projectsDir = join3(homedir4(), CLAUDE_DIR2, PROJECTS_DIR2);
710
+ const projectLink = await loadProjectLink(cwd);
711
+ let projectDir = ctx.projectDir;
712
+ if (ctx.state === "symlink" && ctx.symlinkTarget) {
713
+ projectDir = join3(projectsDir, ctx.symlinkTarget);
714
+ }
715
+ printInfo(`Project: ${brand(cwd)}`);
716
+ if (ctx.state === "symlink") {
717
+ printInfo(`Linked to: ${dim(ctx.symlinkTarget || "unknown")}`);
718
+ }
719
+ const manifest = await buildProjectManifest(cwd);
720
+ if (!projectLink) {
721
+ const result = await handleFirstRun(client, config, cwd, ctx, manifest, projectDir, projectsDir, options);
722
+ if (result === "cancelled") {
723
+ printOutro("Cancelled.");
724
+ return;
725
+ }
726
+ if (result === "pulled") {
727
+ const finalManifest2 = await buildProjectManifest(cwd);
728
+ await saveProjectManifest(cwd, finalManifest2);
729
+ printSuccess("Sessions synced from remote device.");
730
+ printOutro("Done");
731
+ return;
732
+ }
733
+ if (result === "pushed") {
734
+ const finalManifest2 = await buildProjectManifest(cwd);
735
+ await saveProjectManifest(cwd, finalManifest2);
736
+ printOutro("Done");
737
+ return;
738
+ }
739
+ printOutro("Done");
740
+ return;
741
+ }
742
+ let pushResult = { uploaded: 0, projectId: projectLink.projectId, failed: false };
743
+ let pullResult = { downloaded: 0, failed: false };
744
+ if (manifest.length > 0) {
745
+ pushResult = await pushPhase(client, config.deviceId, cwd, manifest, projectDir, projectLink.projectId, options);
746
+ }
747
+ if (!pushResult.failed) {
748
+ pullResult = await pullPhase(client, config.deviceId, cwd, projectDir, projectLink.projectId, options);
749
+ }
750
+ if (pushResult.failed && pullResult.failed) {
751
+ printOutro("Sync failed.");
752
+ return;
753
+ }
754
+ const finalManifest = await buildProjectManifest(cwd);
755
+ await saveProjectManifest(cwd, finalManifest);
756
+ if (pushResult.uploaded > 0 || pullResult.downloaded > 0) {
757
+ const parts = [];
758
+ if (pushResult.uploaded > 0) parts.push(`${success(`+${pushResult.uploaded}`)} pushed`);
759
+ if (pullResult.downloaded > 0) parts.push(`${success(`+${pullResult.downloaded}`)} pulled`);
760
+ printSuccess(`Synced: ${parts.join(", ")}`);
761
+ } else if (!pushResult.failed && !pullResult.failed) {
762
+ printSuccess("Everything is up to date.");
763
+ }
764
+ printOutro("Done");
765
+ }
766
+ async function handleFirstRun(client, config, cwd, ctx, manifest, projectDir, projectsDir, options) {
767
+ printInfo(brand("First time syncing this project"));
768
+ const hasLocalFiles = ctx.state !== "none" && manifest.length > 0;
769
+ const devicesSpinner = createSpinner("Checking for other devices...").start();
770
+ let devices;
771
+ try {
772
+ devices = await client.get("/api/devices");
773
+ } catch {
774
+ devices = [];
775
+ }
776
+ devicesSpinner.stop();
777
+ const otherDevices = devices.filter((d) => d.id !== config.deviceId);
778
+ const menuOptions = [];
779
+ if (hasLocalFiles) {
780
+ menuOptions.push({
781
+ value: "push",
782
+ label: "Push to cloud",
783
+ hint: dim(`Upload ${manifest.length} local files`)
784
+ });
785
+ }
786
+ if (otherDevices.length > 0) {
787
+ menuOptions.push({
788
+ value: "continue",
789
+ label: "Continue from another machine",
790
+ hint: dim("Pull sessions from a device")
791
+ });
792
+ }
793
+ if (menuOptions.length === 0) {
794
+ printInfo("No local sessions and no other devices found.");
795
+ printInfo("Use Claude Code in this directory first, then run sync.");
796
+ return "cancelled";
797
+ }
798
+ if (menuOptions.length === 1 && menuOptions[0].value === "push") {
799
+ printInfo("No other devices found.");
800
+ }
801
+ const choice = await select2({
802
+ message: brand("What would you like to do?"),
803
+ options: menuOptions
804
+ });
805
+ if (isCancel3(choice)) return "cancelled";
806
+ if (choice === "push") {
807
+ const result = await pushPhase(client, config.deviceId, cwd, manifest, projectDir, void 0, options);
808
+ if (result.failed) {
809
+ return "cancelled";
810
+ }
811
+ if (result.uploaded > 0) {
812
+ printSuccess(`${success(`+${result.uploaded}`)} files pushed`);
813
+ }
814
+ await saveProjectLink(cwd, {
815
+ projectId: result.projectId,
816
+ foreignEncodedDir: ctx.encodedPath,
817
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
818
+ });
819
+ return "pushed";
820
+ }
821
+ const deviceChoice = await select2({
822
+ message: brand("Select device"),
823
+ options: otherDevices.map((d) => ({
824
+ value: d.id,
825
+ label: d.name,
826
+ hint: dim(`${d.hostname} \xB7 ${d.platform}`)
827
+ }))
828
+ });
829
+ if (isCancel3(deviceChoice)) return "cancelled";
830
+ const spinner = createSpinner("Loading projects...").start();
831
+ let projects;
832
+ try {
833
+ projects = await client.get(`/api/devices/${deviceChoice}/projects`);
834
+ } catch {
835
+ spinner.stop();
836
+ printError("Failed to load projects from device.");
837
+ return "cancelled";
838
+ }
839
+ spinner.stop();
840
+ if (projects.length === 0) {
841
+ printInfo("No projects found on that device.");
842
+ return "cancelled";
843
+ }
844
+ const projectChoice = await select2({
845
+ message: brand("Select project"),
846
+ options: projects.map((p) => ({
847
+ value: p.id,
848
+ label: p.displayName || p.originalPath,
849
+ hint: dim(p.localPath)
850
+ }))
851
+ });
852
+ if (isCancel3(projectChoice)) return "cancelled";
853
+ const selectedProject = projects.find((p) => p.id === projectChoice);
854
+ const downloaded = await crossMachinePull(
855
+ client,
856
+ config.deviceId,
857
+ cwd,
858
+ deviceChoice,
859
+ selectedProject.id,
860
+ selectedProject.encodedDir,
861
+ projectsDir,
862
+ options
863
+ );
864
+ if (downloaded === 0 && !options.dryRun) {
865
+ printInfo("No files to download.");
866
+ return "cancelled";
867
+ }
868
+ if (!options.dryRun) {
869
+ await createProjectSymlink(projectsDir, ctx.encodedPath, selectedProject.encodedDir);
870
+ await saveProjectLink(cwd, {
871
+ projectId: selectedProject.id,
872
+ foreignEncodedDir: selectedProject.encodedDir,
873
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString()
874
+ });
875
+ printSuccess(`${success(`+${downloaded}`)} files pulled`);
876
+ printInfo(`Symlink: ${dim(ctx.encodedPath)} \u2192 ${dim(selectedProject.encodedDir)}`);
877
+ }
878
+ return "pulled";
879
+ }
880
+ async function createProjectSymlink(projectsDir, localEncoded, foreignEncoded) {
881
+ const symlinkPath = join3(projectsDir, localEncoded);
882
+ await mkdir2(projectsDir, { recursive: true });
883
+ try {
884
+ const stats = await lstat2(symlinkPath);
885
+ if (stats.isSymbolicLink()) {
886
+ await unlink(symlinkPath);
887
+ } else if (stats.isDirectory()) {
888
+ printError(`Local session directory already exists. Back it up first:
889
+ mv "${symlinkPath}" "${symlinkPath}.bak"`);
890
+ return;
891
+ }
892
+ } catch {
893
+ }
894
+ await symlink(foreignEncoded, symlinkPath);
895
+ }
896
+ async function crossMachinePull(client, deviceId, projectPath, fromDeviceId, projectId, foreignEncodedDir, projectsDir, options) {
897
+ const spinner = createSpinner("Preparing pull from remote device...").start();
898
+ let prepareResponse;
899
+ try {
900
+ prepareResponse = await client.post("/api/sync/pull/prepare", {
901
+ deviceId,
902
+ projectPath,
903
+ fromDeviceId,
904
+ projectId
905
+ });
906
+ } catch (err) {
907
+ spinner.stop();
908
+ printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
909
+ return 0;
910
+ }
911
+ spinner.stop();
912
+ const filesToDownload = prepareResponse.filesToDownload;
913
+ if (filesToDownload.length === 0) return 0;
914
+ printInfo(`${brand(String(filesToDownload.length))} files to pull`);
915
+ if (options.dryRun) {
916
+ if (options.verbose) {
917
+ for (const entry of filesToDownload) {
918
+ console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
919
+ }
920
+ }
921
+ return filesToDownload.length;
922
+ }
923
+ const targetDir = join3(projectsDir, foreignEncodedDir);
924
+ let downloaded = 0;
925
+ for (const entry of filesToDownload) {
926
+ const dlSpinner = createSpinner(
927
+ `${formatProgress(downloaded + 1, filesToDownload.length)} ${entry.path}`
928
+ ).start();
929
+ try {
930
+ const urlResponse = await client.post("/api/sync/pull/download-url", {
931
+ syncEventId: prepareResponse.syncEventId,
932
+ key: entry.path
933
+ });
934
+ const response = await fetch(urlResponse.url);
935
+ let data = Buffer.from(await response.arrayBuffer());
936
+ if (entry.isCompressed) {
937
+ data = await decompressBuffer(data);
938
+ }
939
+ const outputPath = join3(targetDir, entry.path);
940
+ await mkdir2(dirname2(outputPath), { recursive: true });
941
+ await writeFile2(outputPath, data);
942
+ downloaded++;
943
+ } catch {
944
+ }
945
+ dlSpinner.stop();
946
+ }
947
+ try {
948
+ await client.post("/api/sync/pull/complete", {
949
+ syncEventId: prepareResponse.syncEventId,
950
+ manifest: filesToDownload
951
+ });
952
+ } catch {
953
+ }
954
+ return downloaded;
955
+ }
956
+ async function pushPhase(client, deviceId, projectPath, manifest, projectDir, projectId, options) {
957
+ const spinner = createSpinner("Preparing push...").start();
958
+ let prepareResponse;
959
+ try {
960
+ prepareResponse = await client.post("/api/sync/push/prepare", {
961
+ deviceId,
962
+ projectPath,
963
+ projectId,
964
+ manifest
965
+ });
966
+ } catch (err) {
967
+ spinner.stop();
968
+ printError(`Push failed: ${err instanceof Error ? err.message : "Unknown error"}`);
969
+ return { uploaded: 0, projectId: projectId || "", failed: true };
970
+ }
971
+ spinner.stop();
972
+ const resolvedProjectId = prepareResponse.projectId;
973
+ const filesToUpload = prepareResponse.filesToUpload;
974
+ if (filesToUpload.length === 0) return { uploaded: 0, projectId: resolvedProjectId, failed: false };
975
+ printInfo(`${brand(String(filesToUpload.length))} files to push`);
976
+ if (options.dryRun) {
977
+ if (options.verbose) {
978
+ for (const entry of filesToUpload) {
979
+ console.log(` ${success("+")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
980
+ }
981
+ }
982
+ return { uploaded: filesToUpload.length, projectId: resolvedProjectId, failed: false };
983
+ }
984
+ let uploaded = 0;
985
+ for (const entry of filesToUpload) {
986
+ const uploadSpinner = createSpinner(
987
+ `${formatProgress(uploaded + 1, filesToUpload.length)} ${entry.path}`
988
+ ).start();
989
+ try {
990
+ const urlResponse = await client.post("/api/sync/push/upload-url", {
991
+ syncEventId: prepareResponse.syncEventId,
992
+ key: entry.path
993
+ });
994
+ const filePath = join3(projectDir, entry.path);
995
+ let body = await readFile2(filePath);
996
+ if (entry.isCompressed) {
997
+ body = await compressBuffer(body);
998
+ }
999
+ await fetch(urlResponse.url, {
1000
+ method: "PUT",
1001
+ body,
1002
+ headers: { "Content-Type": "application/octet-stream" }
1003
+ });
1004
+ uploaded++;
1005
+ } catch {
1006
+ }
1007
+ uploadSpinner.stop();
1008
+ }
1009
+ try {
1010
+ await client.post("/api/sync/push/complete", {
1011
+ syncEventId: prepareResponse.syncEventId,
1012
+ manifest
1013
+ });
1014
+ } catch {
1015
+ }
1016
+ return { uploaded, projectId: resolvedProjectId, failed: false };
1017
+ }
1018
+ async function pullPhase(client, deviceId, projectPath, projectDir, projectId, options) {
1019
+ const spinner = createSpinner("Preparing pull...").start();
1020
+ let prepareResponse;
1021
+ try {
1022
+ prepareResponse = await client.post("/api/sync/pull/prepare", {
1023
+ deviceId,
1024
+ projectPath,
1025
+ projectId
1026
+ });
1027
+ } catch (err) {
1028
+ spinner.stop();
1029
+ printError(`Pull failed: ${err instanceof Error ? err.message : "Unknown error"}`);
1030
+ return { downloaded: 0, failed: true };
1031
+ }
1032
+ spinner.stop();
1033
+ const filesToDownload = prepareResponse.filesToDownload;
1034
+ if (filesToDownload.length === 0) return { downloaded: 0, failed: false };
1035
+ printInfo(`${brand(String(filesToDownload.length))} files to pull`);
1036
+ if (options.dryRun) {
1037
+ if (options.verbose) {
1038
+ for (const entry of filesToDownload) {
1039
+ console.log(` ${brand("\u25CB")} ${entry.path} ${dim(`(${formatBytes(entry.size)})`)}`);
1040
+ }
1041
+ }
1042
+ return { downloaded: filesToDownload.length, failed: false };
1043
+ }
1044
+ let downloaded = 0;
1045
+ for (const entry of filesToDownload) {
1046
+ const dlSpinner = createSpinner(
1047
+ `${formatProgress(downloaded + 1, filesToDownload.length)} ${entry.path}`
1048
+ ).start();
1049
+ try {
1050
+ const urlResponse = await client.post("/api/sync/pull/download-url", {
1051
+ syncEventId: prepareResponse.syncEventId,
1052
+ key: entry.path
1053
+ });
1054
+ const response = await fetch(urlResponse.url);
1055
+ let data = Buffer.from(await response.arrayBuffer());
1056
+ if (entry.isCompressed) {
1057
+ data = await decompressBuffer(data);
1058
+ }
1059
+ const outputPath = join3(projectDir, entry.path);
1060
+ await mkdir2(dirname2(outputPath), { recursive: true });
1061
+ await writeFile2(outputPath, data);
1062
+ downloaded++;
1063
+ } catch {
1064
+ }
1065
+ dlSpinner.stop();
1066
+ }
1067
+ try {
1068
+ await client.post("/api/sync/pull/complete", {
1069
+ syncEventId: prepareResponse.syncEventId,
1070
+ manifest: filesToDownload
1071
+ });
1072
+ } catch {
1073
+ }
1074
+ return { downloaded, failed: false };
1075
+ }
1076
+ function syncCommand() {
1077
+ return new Command4("sync").description("Sync current project sessions with cloud").option("--dry-run", "Show what would be synced without syncing").option("--verbose", "Show detailed output").action(async (options) => {
1078
+ await runSync(options);
1079
+ });
1080
+ }
1081
+ var init_sync = __esm({
1082
+ "src/commands/sync.ts"() {
1083
+ "use strict";
1084
+ init_config();
1085
+ init_api_client();
1086
+ init_sync_engine();
1087
+ init_progress();
1088
+ init_compress();
1089
+ init_theme();
1090
+ }
1091
+ });
1092
+
1093
+ // src/commands/status.ts
1094
+ var status_exports = {};
1095
+ __export(status_exports, {
1096
+ statusCommand: () => statusCommand
1097
+ });
1098
+ import { Command as Command5 } from "commander";
1099
+ function statusCommand() {
1100
+ return new Command5("status").description("Show sync status for current project").action(async () => {
1101
+ printIntro("Status");
1102
+ const config = await loadConfig();
1103
+ const cwd = process.cwd();
1104
+ printLabel("API", config.apiUrl);
1105
+ printLabel("Authenticated", isAuthenticated(config) ? success("yes") : error("no"));
1106
+ printLabel("Device", config.deviceName || dim("none"));
1107
+ printLabel("Project", brand(cwd));
1108
+ const ctx = await resolveProjectState(cwd);
1109
+ printLabel("State", ctx.state === "none" ? dim("no session data") : ctx.state === "symlink" ? `symlink \u2192 ${dim(ctx.symlinkTarget || "unknown")}` : "local");
1110
+ if (!isAuthenticated(config) || ctx.state === "none") return;
1111
+ const spinner = createSpinner("Scanning files...").start();
1112
+ const savedManifest = await loadProjectManifest(cwd);
1113
+ const currentManifest = await buildProjectManifest(cwd);
1114
+ const diff = computeDiff(currentManifest, savedManifest);
1115
+ spinner.stop();
1116
+ const totalSize = currentManifest.reduce((sum, e) => sum + e.size, 0);
1117
+ printDivider();
1118
+ printLabel("Local files", `${brand(String(currentManifest.length))} ${dim(`(${formatBytes(totalSize)})`)}`);
1119
+ console.log();
1120
+ printInfo("Changes since last sync:");
1121
+ console.log(` ${success(`+${diff.added.length}`)} added ${warn(`~${diff.modified.length}`)} modified ${error(`-${diff.deleted.length}`)} deleted`);
1122
+ console.log();
1123
+ });
1124
+ }
1125
+ var init_status = __esm({
1126
+ "src/commands/status.ts"() {
1127
+ "use strict";
1128
+ init_config();
1129
+ init_sync_engine();
1130
+ init_progress();
1131
+ init_theme();
1132
+ }
1133
+ });
1134
+
1135
+ // src/commands/whoami.ts
1136
+ var whoami_exports = {};
1137
+ __export(whoami_exports, {
1138
+ whoamiCommand: () => whoamiCommand
1139
+ });
1140
+ import { Command as Command6 } from "commander";
1141
+ function whoamiCommand() {
1142
+ return new Command6("whoami").description("Show current user and device info").action(async () => {
1143
+ printIntro("Who Am I");
1144
+ const config = await loadConfig();
1145
+ if (!isAuthenticated(config)) {
1146
+ printError("Not logged in. Run `claude-sync login` first.");
1147
+ return;
1148
+ }
1149
+ const client = new ApiClient(config.apiUrl);
1150
+ const spinner = createSpinner("Fetching user info...").start();
1151
+ try {
1152
+ const user = await client.get("/api/auth/me");
1153
+ spinner.stop();
1154
+ printLabel("User", `${user.name} ${dim(`(${user.email})`)}`);
1155
+ printLabel("Plan", brand(user.plan));
1156
+ printDivider();
1157
+ printLabel("Device", config.deviceName || dim("none"));
1158
+ printLabel("Device ID", config.deviceId || dim("not registered"));
1159
+ } catch {
1160
+ spinner.stop();
1161
+ printLabel("Email", config.email || dim("unknown"));
1162
+ printLabel("Device", config.deviceName || dim("none"));
1163
+ }
1164
+ console.log();
1165
+ });
1166
+ }
1167
+ var init_whoami = __esm({
1168
+ "src/commands/whoami.ts"() {
1169
+ "use strict";
1170
+ init_config();
1171
+ init_api_client();
1172
+ init_progress();
1173
+ init_theme();
1174
+ }
1175
+ });
1176
+
1177
+ // src/index.ts
1178
+ init_login();
1179
+ init_logout();
1180
+ init_device();
1181
+ init_sync();
1182
+ init_status();
1183
+ init_whoami();
1184
+ import { Command as Command7 } from "commander";
1185
+
1186
+ // src/commands/tui.ts
1187
+ init_theme();
1188
+ init_config();
1189
+ import { select as select3, isCancel as isCancel4 } from "@clack/prompts";
1190
+ import chalk3 from "chalk";
1191
+ function buildMenu(loggedIn) {
1192
+ if (!loggedIn) {
1193
+ return [
1194
+ { value: "login", label: "Login", hint: "Authenticate with claude-sync" }
1195
+ ];
1196
+ }
1197
+ return [
1198
+ { value: "sync", label: "Sync", hint: "Sync current project" },
1199
+ { value: "status", label: "Status", hint: "Show sync state" },
1200
+ { value: "device:add", label: "Add Device", hint: "Register this machine" },
1201
+ { value: "device:list", label: "List Devices", hint: "Show registered devices" },
1202
+ { value: "device:remove", label: "Remove Device" },
1203
+ { value: "device:rename", label: "Rename Device" },
1204
+ { value: "whoami", label: "Who Am I", hint: "Current user and device" },
1205
+ { value: "logout", label: "Logout", hint: "Sign out" }
1206
+ ];
1207
+ }
1208
+ async function runTui() {
1209
+ printBanner();
1210
+ const config = await loadConfig();
1211
+ const loggedIn = isAuthenticated(config);
1212
+ if (loggedIn) {
1213
+ console.log(` ${dim("user")} ${brand(config.email || "unknown")}`);
1214
+ if (config.deviceName) {
1215
+ console.log(` ${dim("device")} ${brand(config.deviceName)}`);
1216
+ }
1217
+ console.log();
1218
+ } else {
1219
+ console.log(` ${dim("Not logged in")}`);
1220
+ console.log();
1221
+ }
1222
+ while (true) {
1223
+ const items = buildMenu(loggedIn);
1224
+ const options = [
1225
+ ...items.map((o) => ({
1226
+ value: o.value,
1227
+ label: o.label,
1228
+ hint: o.hint ? dim(o.hint) : void 0
1229
+ })),
1230
+ { value: "quit", label: chalk3.red("Quit") }
1231
+ ];
1232
+ const choice = await select3({
1233
+ message: brand("What would you like to do?"),
1234
+ options
1235
+ });
1236
+ if (isCancel4(choice) || choice === "quit") {
1237
+ printOutro("Goodbye!");
1238
+ return;
1239
+ }
1240
+ await executeCommand(choice);
1241
+ console.log();
1242
+ }
1243
+ }
1244
+ async function executeCommand(command) {
1245
+ const { loginCommand: loginCommand2 } = await Promise.resolve().then(() => (init_login(), login_exports));
1246
+ const { logoutCommand: logoutCommand2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
1247
+ const { deviceCommand: deviceCommand2 } = await Promise.resolve().then(() => (init_device(), device_exports));
1248
+ const { syncCommand: syncCommand2 } = await Promise.resolve().then(() => (init_sync(), sync_exports));
1249
+ const { statusCommand: statusCommand2 } = await Promise.resolve().then(() => (init_status(), status_exports));
1250
+ const { whoamiCommand: whoamiCommand2 } = await Promise.resolve().then(() => (init_whoami(), whoami_exports));
1251
+ const handlers = {
1252
+ login: () => loginCommand2().parseAsync(["", "", "login"]),
1253
+ logout: () => logoutCommand2().parseAsync(["", "", "logout"]),
1254
+ sync: () => syncCommand2().parseAsync(["", "", "sync"]),
1255
+ status: () => statusCommand2().parseAsync(["", "", "status"]),
1256
+ whoami: () => whoamiCommand2().parseAsync(["", "", "whoami"]),
1257
+ "device:add": () => deviceCommand2().parseAsync(["", "", "device", "add"]),
1258
+ "device:list": () => deviceCommand2().parseAsync(["", "", "device", "list"]),
1259
+ "device:remove": () => deviceCommand2().parseAsync(["", "", "device", "remove"]),
1260
+ "device:rename": () => deviceCommand2().parseAsync(["", "", "device", "rename"])
1261
+ };
1262
+ const handler = handlers[command];
1263
+ if (handler) {
1264
+ try {
1265
+ await handler();
1266
+ } catch {
1267
+ }
1268
+ }
1269
+ }
1270
+
1271
+ // src/index.ts
1272
+ init_config();
1273
+ function run() {
1274
+ const program = new Command7();
1275
+ program.name("claude-sync").description("Sync Claude Code sessions across machines").version("0.1.0").option("--menu", "Open interactive menu").action(async (options) => {
1276
+ if (options.menu) {
1277
+ await runTui();
1278
+ return;
1279
+ }
1280
+ const config = await loadConfig();
1281
+ if (isAuthenticated(config) && config.deviceId) {
1282
+ await runSync({});
1283
+ } else {
1284
+ await runTui();
1285
+ }
1286
+ });
1287
+ program.addCommand(loginCommand());
1288
+ program.addCommand(logoutCommand());
1289
+ program.addCommand(deviceCommand());
1290
+ program.addCommand(syncCommand());
1291
+ program.addCommand(statusCommand());
1292
+ program.addCommand(whoamiCommand());
1293
+ program.parse();
1294
+ }
1295
+ export {
1296
+ run
1297
+ };
1298
+ //# sourceMappingURL=index.js.map