ziphro-cli 0.0.0-stage → 2.0.0-preview.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.
package/dist/index.js ADDED
@@ -0,0 +1,3719 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import React17 from "react";
5
+ import { render as render4 } from "ink";
6
+ import { program } from "commander";
7
+
8
+ // src/commands/config.tsx
9
+ import React3, { useState as useState3, useEffect as useEffect3 } from "react";
10
+ import { Box as Box4, Text as Text4, useInput, useApp } from "ink";
11
+ import { hostname as hostname2 } from "os";
12
+ import http from "http";
13
+ import { parse as parseUrl } from "url";
14
+ import { exec } from "child_process";
15
+
16
+ // src/lib/auth.ts
17
+ import { homedir, hostname, userInfo } from "os";
18
+ import { join } from "path";
19
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, statSync } from "fs";
20
+ import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync, hkdfSync } from "crypto";
21
+ var ZIPHRO_DIR = join(homedir(), ".ziphro");
22
+ var CREDENTIALS_FILE = join(ZIPHRO_DIR, "credentials");
23
+ var SEED_FILE = join(ZIPHRO_DIR, ".seed");
24
+ function ensureZiphroDir() {
25
+ if (existsSync(ZIPHRO_DIR)) {
26
+ try {
27
+ const stat = statSync(ZIPHRO_DIR);
28
+ if (!stat.isDirectory()) {
29
+ throw new Error(`Path ${ZIPHRO_DIR} exists but is not a directory.`);
30
+ }
31
+ } catch (err) {
32
+ if (err.code !== "ENOENT") {
33
+ throw err;
34
+ }
35
+ }
36
+ return;
37
+ }
38
+ try {
39
+ mkdirSync(ZIPHRO_DIR, { recursive: true, mode: 448 });
40
+ } catch (err) {
41
+ if (err.code !== "EEXIST") {
42
+ throw err;
43
+ }
44
+ }
45
+ }
46
+ function getMachineKey() {
47
+ ensureZiphroDir();
48
+ let seed;
49
+ if (existsSync(SEED_FILE)) {
50
+ try {
51
+ seed = readFileSync(SEED_FILE);
52
+ if (seed.length < 32) throw new Error("Invalid seed length");
53
+ } catch {
54
+ seed = randomBytes(32);
55
+ writeFileSync(SEED_FILE, seed, { mode: 384 });
56
+ }
57
+ } else {
58
+ seed = randomBytes(32);
59
+ try {
60
+ writeFileSync(SEED_FILE, seed, { mode: 384 });
61
+ } catch {
62
+ writeFileSync(SEED_FILE, seed);
63
+ }
64
+ }
65
+ const user = userInfo?.()?.username || "user";
66
+ const host = hostname() || "local";
67
+ const info = `ziphro-vault-v2-${user}-${host}-${process.platform}`;
68
+ return Buffer.from(hkdfSync("sha256", seed, Buffer.alloc(0), Buffer.from(info), 32));
69
+ }
70
+ function encryptSecret(plaintext) {
71
+ if (!plaintext) return "";
72
+ try {
73
+ const key = getMachineKey();
74
+ const iv = randomBytes(12);
75
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
76
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
77
+ const tag = cipher.getAuthTag();
78
+ return `enc:v1:${iv.toString("hex")}:${ciphertext.toString("hex")}:${tag.toString("hex")}`;
79
+ } catch {
80
+ return plaintext;
81
+ }
82
+ }
83
+ function decryptSecret(value) {
84
+ if (!value) return void 0;
85
+ if (!value.startsWith("enc:v1:")) {
86
+ return value;
87
+ }
88
+ try {
89
+ const parts = value.split(":");
90
+ if (parts.length !== 5) return void 0;
91
+ const iv = Buffer.from(parts[2], "hex");
92
+ const ciphertext = Buffer.from(parts[3], "hex");
93
+ const tag = Buffer.from(parts[4], "hex");
94
+ const key = getMachineKey();
95
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
96
+ decipher.setAuthTag(tag);
97
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
98
+ return decrypted.toString("utf8");
99
+ } catch {
100
+ return void 0;
101
+ }
102
+ }
103
+ function deriveVaultKeySync(passphrase, email) {
104
+ const normalizedSalt = Buffer.from(email.trim().toLowerCase(), "utf8");
105
+ const masterKey = pbkdf2Sync(passphrase, normalizedSalt, 6e5, 32, "sha256");
106
+ const encKey = hkdfSync("sha256", masterKey, Buffer.alloc(0), Buffer.from("ziphro-encryption-v2"), 32);
107
+ return Buffer.from(encKey).toString("base64");
108
+ }
109
+ function parseIni(content) {
110
+ const result = {};
111
+ for (const line of content.split(/\r?\n/)) {
112
+ const trimmed = line.trim();
113
+ if (trimmed.startsWith("#") || trimmed.startsWith(";") || trimmed.startsWith("[") || !trimmed) continue;
114
+ const eqIdx = trimmed.indexOf("=");
115
+ if (eqIdx === -1) continue;
116
+ const key = trimmed.slice(0, eqIdx).trim();
117
+ const value = trimmed.slice(eqIdx + 1).trim();
118
+ result[key] = value;
119
+ }
120
+ return result;
121
+ }
122
+ function toIni(data) {
123
+ const lines = ["[default]"];
124
+ for (const [key, value] of Object.entries(data)) {
125
+ if (value !== void 0 && value !== "") {
126
+ lines.push(`${key} = ${value}`);
127
+ }
128
+ }
129
+ return lines.join("\n") + "\n";
130
+ }
131
+ function readCredentials() {
132
+ ensureZiphroDir();
133
+ if (!existsSync(CREDENTIALS_FILE)) return null;
134
+ try {
135
+ const content = readFileSync(CREDENTIALS_FILE, "utf8");
136
+ const raw = parseIni(content);
137
+ const rawExpires = raw.expires_at ? parseInt(raw.expires_at, 10) : void 0;
138
+ const email = raw.email || "";
139
+ const accessToken = decryptSecret(raw.access_token);
140
+ const refreshToken = decryptSecret(raw.refresh_token);
141
+ let vaultKey = decryptSecret(raw.vault_key);
142
+ if (raw.password) {
143
+ if (!vaultKey && email) {
144
+ vaultKey = deriveVaultKeySync(raw.password, email);
145
+ }
146
+ writeCredentials({
147
+ api_url: raw.api_url || "https://api-ziphro.vercel.app",
148
+ email,
149
+ access_token: accessToken,
150
+ refresh_token: refreshToken,
151
+ expires_at: rawExpires,
152
+ device_id: raw.device_id,
153
+ vault_key: vaultKey
154
+ // Password is purposefully omitted so it is wiped from disk
155
+ });
156
+ }
157
+ return {
158
+ api_url: raw.api_url || "https://api-ziphro.vercel.app",
159
+ email,
160
+ access_token: accessToken,
161
+ refresh_token: refreshToken,
162
+ expires_at: rawExpires && !isNaN(rawExpires) ? rawExpires : void 0,
163
+ device_id: raw.device_id || void 0,
164
+ vault_key: vaultKey
165
+ };
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+ function writeCredentials(creds) {
171
+ ensureZiphroDir();
172
+ let vaultKeyToWrite = creds.vault_key;
173
+ if (creds.password && creds.email && !vaultKeyToWrite) {
174
+ vaultKeyToWrite = deriveVaultKeySync(creds.password, creds.email);
175
+ }
176
+ const encryptedAccessToken = creds.access_token ? encryptSecret(creds.access_token) : void 0;
177
+ const encryptedRefreshToken = creds.refresh_token ? encryptSecret(creds.refresh_token) : void 0;
178
+ const encryptedVaultKey = vaultKeyToWrite ? encryptSecret(vaultKeyToWrite) : void 0;
179
+ const data = {
180
+ api_url: creds.api_url,
181
+ email: creds.email,
182
+ device_id: creds.device_id,
183
+ expires_at: creds.expires_at,
184
+ access_token: encryptedAccessToken,
185
+ refresh_token: encryptedRefreshToken,
186
+ vault_key: encryptedVaultKey
187
+ // Note: 'password' is intentionally NOT in data — never stored in plain text
188
+ };
189
+ try {
190
+ writeFileSync(CREDENTIALS_FILE, toIni(data), { encoding: "utf8", mode: 384 });
191
+ } catch (err) {
192
+ writeFileSync(CREDENTIALS_FILE, toIni(data), { encoding: "utf8" });
193
+ }
194
+ try {
195
+ chmodSync(CREDENTIALS_FILE, 384);
196
+ } catch {
197
+ }
198
+ }
199
+ function clearTokens() {
200
+ const creds = readCredentials();
201
+ if (!creds) return;
202
+ writeCredentials({
203
+ ...creds,
204
+ access_token: void 0,
205
+ refresh_token: void 0,
206
+ expires_at: void 0,
207
+ device_id: void 0,
208
+ vault_key: void 0,
209
+ password: void 0
210
+ });
211
+ }
212
+ function requireAuth() {
213
+ const creds = readCredentials();
214
+ if (!creds || !creds.access_token) {
215
+ throw new Error("Not authenticated. Run `ziphro login` first.");
216
+ }
217
+ if (creds.expires_at && Date.now() / 1e3 > creds.expires_at) {
218
+ throw new Error("Session expired. Run `ziphro login` to re-authenticate.");
219
+ }
220
+ const vaultKeyBuf = creds.vault_key ? Buffer.from(creds.vault_key, "base64") : void 0;
221
+ return {
222
+ email: creds.email,
223
+ deviceId: creds.device_id ?? "default",
224
+ accessToken: creds.access_token,
225
+ apiUrl: creds.api_url,
226
+ expiresAt: creds.expires_at ?? 0,
227
+ password: vaultKeyBuf ? vaultKeyBuf.toString("hex") : void 0,
228
+ vaultKey: vaultKeyBuf ? new Uint8Array(vaultKeyBuf) : void 0
229
+ };
230
+ }
231
+ function readAuth() {
232
+ try {
233
+ return requireAuth();
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+
239
+ // src/lib/version.ts
240
+ var CLI_VERSION = "2.0.0-preview.0";
241
+ function parseSemver(version) {
242
+ const cleaned = version.trim().replace(/^v/i, "").split("+")[0] ?? "";
243
+ const dashIndex = cleaned.indexOf("-");
244
+ const core = dashIndex === -1 ? cleaned : cleaned.slice(0, dashIndex);
245
+ const pre = dashIndex === -1 ? "" : cleaned.slice(dashIndex + 1);
246
+ const [major = 0, minor = 0, patch = 0] = core.split(".").map((part) => {
247
+ const num = Number.parseInt(part, 10);
248
+ return Number.isNaN(num) ? 0 : num;
249
+ });
250
+ return {
251
+ major,
252
+ minor,
253
+ patch,
254
+ prerelease: pre ? pre.split(".").filter(Boolean) : []
255
+ };
256
+ }
257
+ function isOutdated(current, latest) {
258
+ if (!current) return true;
259
+ if (!latest) return false;
260
+ const c = parseSemver(current);
261
+ const l = parseSemver(latest);
262
+ if (l.major !== c.major) return l.major > c.major;
263
+ if (l.minor !== c.minor) return l.minor > c.minor;
264
+ if (l.patch !== c.patch) return l.patch > c.patch;
265
+ if (c.prerelease.length > 0 && l.prerelease.length === 0) return true;
266
+ if (c.prerelease.length === 0 && l.prerelease.length > 0) return false;
267
+ const maxLen = Math.max(c.prerelease.length, l.prerelease.length);
268
+ for (let i = 0; i < maxLen; i++) {
269
+ const cId = c.prerelease[i];
270
+ const lId = l.prerelease[i];
271
+ if (cId === void 0 && lId !== void 0) return true;
272
+ if (cId !== void 0 && lId === void 0) return false;
273
+ if (cId === lId) continue;
274
+ const cIsNum = /^\d+$/.test(cId);
275
+ const lIsNum = /^\d+$/.test(lId);
276
+ if (cIsNum && lIsNum) {
277
+ return Number(lId) > Number(cId);
278
+ }
279
+ if (cIsNum !== lIsNum) {
280
+ return !lIsNum;
281
+ }
282
+ return lId > cId;
283
+ }
284
+ return false;
285
+ }
286
+
287
+ // src/lib/api.ts
288
+ async function request(apiUrl, path, options = {}) {
289
+ const url = `${apiUrl.replace(/\/$/, "")}${path}`;
290
+ const headers = {
291
+ "Content-Type": "application/json",
292
+ "User-Agent": `ziphro-cli/${CLI_VERSION}`
293
+ };
294
+ if (options.token) {
295
+ headers["Authorization"] = `Bearer ${options.token}`;
296
+ }
297
+ try {
298
+ const res = await fetch(url, {
299
+ method: options.method ?? "GET",
300
+ headers,
301
+ body: options.body ? JSON.stringify(options.body) : void 0
302
+ });
303
+ const json = await res.json();
304
+ return json;
305
+ } catch (err) {
306
+ const message = err instanceof Error ? err.message : "Network error";
307
+ return {
308
+ success: false,
309
+ error: {
310
+ code: "NETWORK_ERROR",
311
+ message: `Cannot reach API: ${message}`
312
+ }
313
+ };
314
+ }
315
+ }
316
+ async function apiLogin(apiUrl, email, password) {
317
+ return request(apiUrl, "/auth/login", {
318
+ method: "POST",
319
+ body: { email, password }
320
+ });
321
+ }
322
+ async function apiLogout(apiUrl, token) {
323
+ return request(apiUrl, "/auth/logout", {
324
+ method: "POST",
325
+ token
326
+ });
327
+ }
328
+ async function apiRegisterDevice(apiUrl, token, deviceName, platform) {
329
+ return request(apiUrl, "/auth/device", {
330
+ method: "POST",
331
+ token,
332
+ body: { device_name: deviceName, platform }
333
+ });
334
+ }
335
+ async function apiUploadSync(apiUrl, token, encryptedBlob, chk, deviceId, expectedRevision) {
336
+ return request(apiUrl, "/api/v1/tasks", {
337
+ method: "POST",
338
+ token,
339
+ body: {
340
+ encrypted_blob: encryptedBlob,
341
+ checksum: chk,
342
+ device_id: deviceId,
343
+ expected_revision: expectedRevision
344
+ }
345
+ });
346
+ }
347
+ async function apiDownloadSync(apiUrl, token, revision) {
348
+ const path = revision !== void 0 ? `/api/v1/tasks?revision=${revision}` : "/api/v1/tasks";
349
+ return request(apiUrl, path, { token });
350
+ }
351
+
352
+ // src/components/StarBurst.tsx
353
+ import { useState, useEffect } from "react";
354
+ import { Box, Text } from "ink";
355
+
356
+ // src/lib/theme.ts
357
+ var BRAND = {
358
+ // Primary brand accents
359
+ orange: "#f54e00",
360
+ white: "#f5f4ef",
361
+ muted: "#73726c",
362
+ subtle: "#8e8e93",
363
+ border: "#3a3a3c",
364
+ borderDark: "#2a2a28",
365
+ // Semantic status colors
366
+ success: "#22c55e",
367
+ warning: "#eab308",
368
+ error: "#ef4444",
369
+ info: "#06b6d4",
370
+ tag: "#a78bfa"
371
+ };
372
+
373
+ // src/components/StarBurst.tsx
374
+ import { jsx, jsxs } from "react/jsx-runtime";
375
+ var STARS = ["\u2726", "\u2727", "\u2605", "\u2606", "\u2738", "\u2739", "\u273A", "\u273B", "\u273C", "\u273D"];
376
+ function randomStar() {
377
+ return STARS[Math.floor(Math.random() * STARS.length)] ?? "\u2726";
378
+ }
379
+ function randomStars(count) {
380
+ return Array.from({ length: count }, () => randomStar()).join(" ");
381
+ }
382
+ function StarBurst({
383
+ label,
384
+ color = BRAND.orange
385
+ }) {
386
+ const [frame, setFrame] = useState(0);
387
+ const [done, setDone] = useState(false);
388
+ useEffect(() => {
389
+ const timer = setInterval(() => {
390
+ setFrame((f) => {
391
+ if (f >= 5) {
392
+ clearInterval(timer);
393
+ setDone(true);
394
+ return f;
395
+ }
396
+ return f + 1;
397
+ });
398
+ }, 80);
399
+ return () => clearInterval(timer);
400
+ }, []);
401
+ if (done) {
402
+ return /* @__PURE__ */ jsxs(Box, { children: [
403
+ /* @__PURE__ */ jsxs(Text, { color: BRAND.success, bold: true, children: [
404
+ "\u2713",
405
+ " "
406
+ ] }),
407
+ /* @__PURE__ */ jsx(Text, { color: BRAND.white, bold: true, children: label })
408
+ ] });
409
+ }
410
+ const count = Math.min(frame + 1, 4);
411
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { children: [
412
+ /* @__PURE__ */ jsxs(Text, { color, children: [
413
+ randomStars(count),
414
+ " "
415
+ ] }),
416
+ /* @__PURE__ */ jsx(Text, { color: BRAND.white, bold: true, children: label }),
417
+ /* @__PURE__ */ jsxs(Text, { color, children: [
418
+ " ",
419
+ randomStars(count)
420
+ ] })
421
+ ] }) });
422
+ }
423
+ function StatusBadge({
424
+ type,
425
+ message
426
+ }) {
427
+ const config = {
428
+ success: { icon: "\u2713", color: BRAND.success },
429
+ error: { icon: "\u2717", color: BRAND.error },
430
+ info: { icon: "\u2192", color: BRAND.info },
431
+ warn: { icon: "\u26A0", color: BRAND.warning }
432
+ }[type];
433
+ return /* @__PURE__ */ jsxs(Box, { children: [
434
+ /* @__PURE__ */ jsxs(Text, { color: config.color, bold: true, children: [
435
+ config.icon,
436
+ " "
437
+ ] }),
438
+ /* @__PURE__ */ jsx(Text, { color: type === "error" ? BRAND.error : BRAND.white, children: message })
439
+ ] });
440
+ }
441
+
442
+ // src/components/Spinner.tsx
443
+ import { useState as useState2, useEffect as useEffect2 } from "react";
444
+ import { Box as Box2, Text as Text2 } from "ink";
445
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
446
+ var ORBIT_FRAMES = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
447
+ var STAR_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
448
+ function Spinner({
449
+ label = "Loading...",
450
+ type = "orbit",
451
+ color = BRAND.orange
452
+ }) {
453
+ const frames = type === "orbit" ? ORBIT_FRAMES : STAR_FRAMES;
454
+ const [frame, setFrame] = useState2(0);
455
+ useEffect2(() => {
456
+ const timer = setInterval(() => {
457
+ setFrame((f) => (f + 1) % frames.length);
458
+ }, 80);
459
+ return () => clearInterval(timer);
460
+ }, [frames.length]);
461
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
462
+ /* @__PURE__ */ jsxs2(Text2, { color, bold: true, children: [
463
+ frames[frame],
464
+ " "
465
+ ] }),
466
+ /* @__PURE__ */ jsx2(Text2, { color: BRAND.white, children: label })
467
+ ] });
468
+ }
469
+
470
+ // src/components/AsciiHeader.tsx
471
+ import { Box as Box3, Text as Text3 } from "ink";
472
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
473
+ var LOGO_LINES = [
474
+ { ziph: "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557", ro: "\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 " },
475
+ { ziph: "\u255A\u2550\u2550\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551", ro: "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557" },
476
+ { ziph: " \u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551", ro: "\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551" },
477
+ { ziph: " \u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551", ro: "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551" },
478
+ { ziph: "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551", ro: "\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D" },
479
+ { ziph: "\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D", ro: "\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D " }
480
+ ];
481
+ var TAGLINE = "\u2726 CLI \xB7 MCP \xB7 E2EE VAULT \u2726";
482
+ function AsciiHeader() {
483
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
484
+ LOGO_LINES.map((line, i) => /* @__PURE__ */ jsxs3(Box3, { children: [
485
+ /* @__PURE__ */ jsx3(Text3, { color: BRAND.white, bold: true, children: line.ziph }),
486
+ /* @__PURE__ */ jsx3(Text3, { color: BRAND.orange, bold: true, children: line.ro })
487
+ ] }, i)),
488
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 0, children: /* @__PURE__ */ jsxs3(Text3, { color: BRAND.muted, children: [
489
+ " ",
490
+ TAGLINE
491
+ ] }) }),
492
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 0, children: /* @__PURE__ */ jsx3(Text3, { color: BRAND.borderDark, children: "\u2500".repeat(54) }) })
493
+ ] });
494
+ }
495
+ function MiniHeader() {
496
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 0, children: [
497
+ LOGO_LINES.map((line, i) => /* @__PURE__ */ jsxs3(Box3, { children: [
498
+ /* @__PURE__ */ jsx3(Text3, { color: BRAND.white, bold: true, children: line.ziph }),
499
+ /* @__PURE__ */ jsx3(Text3, { color: BRAND.orange, bold: true, children: line.ro })
500
+ ] }, i)),
501
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 0, children: /* @__PURE__ */ jsxs3(Text3, { color: BRAND.muted, children: [
502
+ " ",
503
+ TAGLINE
504
+ ] }) })
505
+ ] });
506
+ }
507
+
508
+ // src/commands/config.tsx
509
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
510
+ function openBrowser(url) {
511
+ const start = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
512
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : `${start} "${url}"`;
513
+ exec(cmd);
514
+ }
515
+ function ConfigUI() {
516
+ const { exit } = useApp();
517
+ const [stage, setStage] = useState3("waiting_for_browser");
518
+ const apiUrl = "https://api-ziphro.vercel.app";
519
+ const [port, setPort] = useState3(0);
520
+ const [errorMsg, setErrorMsg] = useState3("");
521
+ const [authDetails, setAuthDetails] = useState3(null);
522
+ useInput((input, key) => {
523
+ if (key.escape) {
524
+ exit();
525
+ }
526
+ });
527
+ useEffect3(() => {
528
+ if (stage !== "waiting_for_browser") return;
529
+ let server = null;
530
+ let isActive = true;
531
+ async function runServer() {
532
+ try {
533
+ const srv = http.createServer((req, res) => {
534
+ const parsed = parseUrl(req.url || "", true);
535
+ if (parsed.pathname === "/callback") {
536
+ const q = parsed.query;
537
+ const accessToken = q.access_token;
538
+ const refreshToken = q.refresh_token;
539
+ const expiresAt = q.expires_at;
540
+ const email = q.email;
541
+ const password = q.password;
542
+ if (!accessToken || !email) {
543
+ res.writeHead(400, { "Content-Type": "text/html" });
544
+ res.end("<h1>Authentication Failed</h1><p>Missing credentials.</p>");
545
+ if (isActive) {
546
+ setErrorMsg("Authentication failed: Missing credentials from redirect.");
547
+ setStage("error");
548
+ setTimeout(() => exit(new Error("Missing credentials")), 1500);
549
+ }
550
+ return;
551
+ }
552
+ res.writeHead(200, { "Content-Type": "text/html" });
553
+ res.end(`
554
+ <!DOCTYPE html>
555
+ <html>
556
+ <head>
557
+ <title>Ziphro CLI Authenticated</title>
558
+ <style>
559
+ body {
560
+ background:
561
+ radial-gradient(ellipse 80% 60% at 50% -20%, rgba(123, 97, 255, 0.15) 0%, transparent 70%),
562
+ radial-gradient(ellipse 60% 50% at 80% 20%, rgba(0, 112, 243, 0.12) 0%, transparent 60%),
563
+ radial-gradient(ellipse 50% 40% at 20% 30%, rgba(0, 223, 216, 0.1) 0%, transparent 60%),
564
+ radial-gradient(ellipse 70% 40% at 60% 60%, rgba(235, 54, 127, 0.08) 0%, transparent 70%),
565
+ #0D0D11;
566
+ color: #FFFFFF;
567
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
568
+ display: flex;
569
+ align-items: center;
570
+ justify-content: center;
571
+ min-height: 100vh;
572
+ margin: 0;
573
+ }
574
+ .card {
575
+ background: rgba(19, 19, 26, 0.8);
576
+ backdrop-filter: blur(12px);
577
+ -webkit-backdrop-filter: blur(12px);
578
+ border: 1px solid rgba(255, 255, 255, 0.08);
579
+ border-radius: 16px;
580
+ padding: 40px;
581
+ text-align: center;
582
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
583
+ max-width: 400px;
584
+ width: 100%;
585
+ animation: fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
586
+ }
587
+ .logo-container {
588
+ background: #0D0D11;
589
+ border: 1px solid #1F1F2E;
590
+ border-radius: 8px;
591
+ padding: 8px 16px;
592
+ display: inline-flex;
593
+ align-items: center;
594
+ justify-content: center;
595
+ margin-bottom: 24px;
596
+ animation: pulseRing 2s infinite ease-in-out;
597
+ }
598
+ .text-logo {
599
+ display: inline-flex;
600
+ align-items: center;
601
+ gap: 6px;
602
+ text-decoration: none !important;
603
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
604
+ font-size: 16px;
605
+ font-weight: 600;
606
+ letter-spacing: -0.5px;
607
+ color: #FFFFFF;
608
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
609
+ flex-shrink: 0;
610
+ }
611
+ .text-logo-brand {
612
+ transition: letter-spacing 0.3s ease, color 0.3s ease;
613
+ }
614
+ .text-logo-accent {
615
+ background: linear-gradient(135deg, #7B61FF, #0070f3);
616
+ -webkit-background-clip: text;
617
+ -webkit-text-fill-color: transparent;
618
+ }
619
+ .text-logo-spark {
620
+ color: #7B61FF;
621
+ font-size: 11px;
622
+ opacity: 0.7;
623
+ transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease, color 0.3s ease;
624
+ }
625
+ .text-logo:hover {
626
+ opacity: 0.95;
627
+ }
628
+ .text-logo:hover .text-logo-brand {
629
+ letter-spacing: 0.3px;
630
+ }
631
+ .text-logo:hover .text-logo-spark {
632
+ transform: scale(1.3) rotate(180deg);
633
+ opacity: 1;
634
+ color: #00dfd8;
635
+ }
636
+ .success-badge {
637
+ display: inline-flex;
638
+ align-items: center;
639
+ gap: 6px;
640
+ background: rgba(16, 185, 129, 0.1);
641
+ color: #10B981;
642
+ border: 1px solid rgba(16, 185, 129, 0.2);
643
+ padding: 6px 14px;
644
+ border-radius: 99px;
645
+ font-size: 13px;
646
+ font-weight: 500;
647
+ margin-bottom: 20px;
648
+ }
649
+ h1 {
650
+ color: #FFFFFF;
651
+ margin: 0 0 12px 0;
652
+ font-size: 22px;
653
+ font-weight: 600;
654
+ letter-spacing: -0.5px;
655
+ }
656
+ p {
657
+ color: #A1A1B5;
658
+ font-size: 14px;
659
+ line-height: 1.6;
660
+ margin: 0;
661
+ }
662
+ @keyframes fadeUp {
663
+ from { opacity: 0; transform: translateY(20px); }
664
+ to { opacity: 1; transform: translateY(0); }
665
+ }
666
+ @keyframes pulseRing {
667
+ 0% { transform: scale(1); box-shadow: 0 4px 14px rgba(123, 97, 255, 0.15); }
668
+ 50% { transform: scale(1.03); box-shadow: 0 6px 20px rgba(123, 97, 255, 0.35); }
669
+ 100% { transform: scale(1); box-shadow: 0 4px 14px rgba(123, 97, 255, 0.15); }
670
+ }
671
+ </style>
672
+ </head>
673
+ <body>
674
+ <div class="card">
675
+ <div class="logo-container">
676
+ <a href="#" class="text-logo">
677
+ <span class="text-logo-brand" style="color: #FFFFFF;">Task<span class="text-logo-accent">Air</span></span>
678
+ <span class="text-logo-spark">\u2726</span>
679
+ </a>
680
+ </div>
681
+ <div>
682
+ <div class="success-badge">
683
+ <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24" style="margin-top: 1px;">
684
+ <polyline points="20 6 9 17 4 12"/>
685
+ </svg>
686
+ <span>CLI Connected</span>
687
+ </div>
688
+ </div>
689
+ <h1>Authenticated Successfully</h1>
690
+ <p>You can close this tab and return to your terminal.</p>
691
+ </div>
692
+ </body>
693
+ </html>
694
+ `);
695
+ if (isActive) {
696
+ setAuthDetails({
697
+ accessToken,
698
+ refreshToken,
699
+ expiresAt: parseInt(expiresAt, 10),
700
+ email,
701
+ password: password || void 0
702
+ });
703
+ setStage("registering");
704
+ }
705
+ } else {
706
+ res.writeHead(404);
707
+ res.end("Not Found");
708
+ }
709
+ });
710
+ server = srv;
711
+ srv.listen(0, "127.0.0.1", () => {
712
+ const addr = srv.address();
713
+ const p = typeof addr === "string" ? 0 : addr?.port || 0;
714
+ setPort(p);
715
+ const authUrl = `https://api-ziphro.vercel.app/cli-auth?callback=http://localhost:${p}/callback`;
716
+ openBrowser(authUrl);
717
+ });
718
+ } catch (err) {
719
+ if (isActive) {
720
+ setErrorMsg(err.message || "Failed to start local server");
721
+ setStage("error");
722
+ setTimeout(() => exit(err), 1500);
723
+ }
724
+ }
725
+ }
726
+ runServer();
727
+ return () => {
728
+ isActive = false;
729
+ if (server) {
730
+ server.close();
731
+ }
732
+ };
733
+ }, [stage, apiUrl]);
734
+ useEffect3(() => {
735
+ if (stage !== "registering" || !authDetails) return;
736
+ async function registerAndSave() {
737
+ try {
738
+ const deviceName = `${hostname2() || "CLI"} - ${process.platform || "Client"}`;
739
+ const devRes = await apiRegisterDevice(apiUrl, authDetails.accessToken, deviceName, "cli");
740
+ if (devRes.success && devRes.data) {
741
+ const existing = readCredentials();
742
+ writeCredentials({
743
+ ...existing,
744
+ api_url: apiUrl,
745
+ email: authDetails.email,
746
+ access_token: authDetails.accessToken,
747
+ refresh_token: authDetails.refreshToken,
748
+ expires_at: authDetails.expiresAt,
749
+ device_id: devRes.data.id,
750
+ password: authDetails.password
751
+ });
752
+ setStage("success");
753
+ setTimeout(() => exit(), 1200);
754
+ } else {
755
+ setErrorMsg(devRes.error?.message ?? "Device registration failed");
756
+ setStage("error");
757
+ setTimeout(() => exit(new Error(devRes.error?.message)), 1500);
758
+ }
759
+ } catch (err) {
760
+ setErrorMsg(err.message || "Error completing configuration");
761
+ setStage("error");
762
+ setTimeout(() => exit(err), 1500);
763
+ }
764
+ }
765
+ registerAndSave();
766
+ }, [stage, authDetails, apiUrl]);
767
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", padding: 1, children: [
768
+ /* @__PURE__ */ jsx4(AsciiHeader, {}),
769
+ /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "\u2726 Configure Ziphro" }) }),
770
+ stage === "waiting_for_browser" && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 0.5, children: [
771
+ /* @__PURE__ */ jsxs4(Text4, { color: BRAND.success, children: [
772
+ "\u2713 API URL set to: ",
773
+ apiUrl
774
+ ] }),
775
+ port > 0 ? /* @__PURE__ */ jsxs4(Fragment, { children: [
776
+ /* @__PURE__ */ jsxs4(Text4, { color: BRAND.warning, children: [
777
+ "\u2726 Started local server on port ",
778
+ port
779
+ ] }),
780
+ /* @__PURE__ */ jsx4(Spinner, { label: "Opening browser to authenticate\u2026", type: "orbit", color: BRAND.orange }),
781
+ /* @__PURE__ */ jsx4(Text4, { color: BRAND.muted, children: "If the page does not load, open this URL manually:" }),
782
+ /* @__PURE__ */ jsx4(Text4, { color: BRAND.orange, underline: true, children: `https://api-ziphro.vercel.app/cli-auth?callback=http://localhost:${port}/callback` })
783
+ ] }) : /* @__PURE__ */ jsx4(Spinner, { label: "Starting local server\u2026", type: "orbit", color: BRAND.orange })
784
+ ] }),
785
+ stage === "registering" && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
786
+ /* @__PURE__ */ jsx4(Text4, { color: BRAND.success, children: "\u2713 Received credentials from browser" }),
787
+ /* @__PURE__ */ jsx4(Spinner, { label: "Registering device with backend\u2026", type: "orbit", color: BRAND.orange })
788
+ ] }),
789
+ stage === "success" && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(StatusBadge, { type: "success", message: "Configuration saved successfully! E2E Encryption Active." }) }),
790
+ stage === "error" && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(StatusBadge, { type: "error", message: errorMsg }) }),
791
+ stage === "waiting_for_browser" && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "Press Esc to cancel" }) })
792
+ ] });
793
+ }
794
+ function registerConfig(program2) {
795
+ program2.command("config").alias("configure").description("Authenticate and configure CLI via browser").action(async () => {
796
+ const { render: render5 } = await import("ink");
797
+ render5(
798
+ React3.createElement(ConfigUI)
799
+ );
800
+ });
801
+ }
802
+
803
+ // src/commands/login.tsx
804
+ import React4, { useState as useState4, useEffect as useEffect4 } from "react";
805
+ import { Box as Box5, Text as Text5, useInput as useInput2, useApp as useApp2 } from "ink";
806
+ import { hostname as hostname3 } from "os";
807
+
808
+ // ../packages/crypto/dist/web.js
809
+ var KEY_LENGTH = 32;
810
+ var PBKDF2_ITERATIONS_V2 = 6e5;
811
+ var PBKDF2_ITERATIONS_V1 = 1e5;
812
+ function uint8ArrayToBase64(arr) {
813
+ const binString = Array.from(arr, (byte) => String.fromCharCode(byte)).join("");
814
+ return btoa(binString);
815
+ }
816
+ function base64ToUint8Array(str) {
817
+ const binString = atob(str);
818
+ return Uint8Array.from(binString, (char) => char.charCodeAt(0));
819
+ }
820
+ function getSubtleCrypto() {
821
+ const c = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
822
+ if (!c || !c.subtle) {
823
+ throw new Error("Web Crypto Subtle is not available in this environment.");
824
+ }
825
+ return c.subtle;
826
+ }
827
+ function getRandomValues(arr) {
828
+ const c = typeof window !== "undefined" ? window.crypto : globalThis.crypto;
829
+ return c.getRandomValues(arr);
830
+ }
831
+ function toBufferSource(bytes) {
832
+ if (bytes.buffer instanceof ArrayBuffer) {
833
+ return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
834
+ }
835
+ return new Uint8Array(bytes);
836
+ }
837
+ async function deriveAccountKeys(passphrase, emailSalt) {
838
+ const subtle = getSubtleCrypto();
839
+ const encoder = new TextEncoder();
840
+ const normalizedSalt = encoder.encode(emailSalt.trim().toLowerCase());
841
+ const passphraseBytes = encoder.encode(passphrase);
842
+ const pbkdf2Key = await subtle.importKey("raw", passphraseBytes, { name: "PBKDF2" }, false, ["deriveBits"]);
843
+ const masterBits = await subtle.deriveBits({
844
+ name: "PBKDF2",
845
+ salt: normalizedSalt,
846
+ iterations: PBKDF2_ITERATIONS_V2,
847
+ hash: "SHA-256"
848
+ }, pbkdf2Key, KEY_LENGTH * 8);
849
+ const hkdfKey = await subtle.importKey("raw", masterBits, "HKDF", false, ["deriveBits"]);
850
+ const authBits = await subtle.deriveBits({
851
+ name: "HKDF",
852
+ hash: "SHA-256",
853
+ salt: new Uint8Array(0),
854
+ info: encoder.encode("ziphro-auth-v2")
855
+ }, hkdfKey, KEY_LENGTH * 8);
856
+ const encBits = await subtle.deriveBits({
857
+ name: "HKDF",
858
+ hash: "SHA-256",
859
+ salt: new Uint8Array(0),
860
+ info: encoder.encode("ziphro-encryption-v2")
861
+ }, hkdfKey, KEY_LENGTH * 8);
862
+ return {
863
+ authSecret: uint8ArrayToBase64(new Uint8Array(authBits)),
864
+ encKey: new Uint8Array(encBits)
865
+ };
866
+ }
867
+ function buildAADBytes(meta) {
868
+ return new TextEncoder().encode(JSON.stringify({
869
+ u: meta.userId,
870
+ r: meta.revision,
871
+ v: meta.version ?? 2
872
+ }));
873
+ }
874
+ async function encryptWithAAD(plaintext, encKey, meta) {
875
+ const subtle = getSubtleCrypto();
876
+ const iv = getRandomValues(new Uint8Array(12));
877
+ const salt = getRandomValues(new Uint8Array(16));
878
+ const aad = buildAADBytes(meta);
879
+ const cryptoKey = ArrayBuffer.isView(encKey) ? await subtle.importKey("raw", toBufferSource(encKey), { name: "AES-GCM" }, false, ["encrypt"]) : encKey;
880
+ const plaintextBytes = new TextEncoder().encode(plaintext);
881
+ const encryptedBuffer = await subtle.encrypt({
882
+ name: "AES-GCM",
883
+ iv: toBufferSource(iv),
884
+ additionalData: toBufferSource(aad),
885
+ tagLength: 128
886
+ }, cryptoKey, plaintextBytes);
887
+ const encryptedBytes = new Uint8Array(encryptedBuffer);
888
+ const ciphertextBytes = encryptedBytes.slice(0, -16);
889
+ const authTagBytes = encryptedBytes.slice(-16);
890
+ return {
891
+ version: "2",
892
+ iterations: PBKDF2_ITERATIONS_V2,
893
+ revision: meta.revision,
894
+ salt: uint8ArrayToBase64(salt),
895
+ iv: uint8ArrayToBase64(iv),
896
+ ciphertext: uint8ArrayToBase64(ciphertextBytes),
897
+ authTag: uint8ArrayToBase64(authTagBytes)
898
+ };
899
+ }
900
+ async function decryptWithAAD(blob, encKey, meta) {
901
+ const subtle = getSubtleCrypto();
902
+ const iv = base64ToUint8Array(blob.iv);
903
+ const ciphertextBytes = base64ToUint8Array(blob.ciphertext);
904
+ const authTagBytes = base64ToUint8Array(blob.authTag);
905
+ const combined = new Uint8Array(ciphertextBytes.length + authTagBytes.length);
906
+ combined.set(ciphertextBytes, 0);
907
+ combined.set(authTagBytes, ciphertextBytes.length);
908
+ const cryptoKey = ArrayBuffer.isView(encKey) ? await subtle.importKey("raw", toBufferSource(encKey), { name: "AES-GCM" }, false, ["decrypt"]) : encKey;
909
+ const gcmParams = {
910
+ name: "AES-GCM",
911
+ iv: toBufferSource(iv),
912
+ tagLength: 128
913
+ };
914
+ if (blob.version === "2" && meta) {
915
+ gcmParams.additionalData = toBufferSource(buildAADBytes({
916
+ userId: meta.userId,
917
+ revision: meta.revision,
918
+ version: meta.version ?? 2
919
+ }));
920
+ }
921
+ const decryptedBuffer = await subtle.decrypt(gcmParams, cryptoKey, combined);
922
+ return new TextDecoder().decode(decryptedBuffer);
923
+ }
924
+ async function legacyDecrypt(blob, password, meta) {
925
+ const subtle = getSubtleCrypto();
926
+ const encoder = new TextEncoder();
927
+ const salt = base64ToUint8Array(blob.salt);
928
+ const iv = base64ToUint8Array(blob.iv);
929
+ const ciphertextBytes = base64ToUint8Array(blob.ciphertext);
930
+ const authTagBytes = base64ToUint8Array(blob.authTag);
931
+ const combined = new Uint8Array(ciphertextBytes.length + authTagBytes.length);
932
+ combined.set(ciphertextBytes, 0);
933
+ combined.set(authTagBytes, ciphertextBytes.length);
934
+ const isV2 = blob.version === "2";
935
+ const iterations = isV2 ? blob.iterations || PBKDF2_ITERATIONS_V2 : PBKDF2_ITERATIONS_V1;
936
+ const passwordBytes = encoder.encode(password);
937
+ const pbkdf2Key = await subtle.importKey("raw", passwordBytes, { name: "PBKDF2" }, false, ["deriveBits"]);
938
+ const masterBits = await subtle.deriveBits({
939
+ name: "PBKDF2",
940
+ salt: toBufferSource(salt),
941
+ iterations,
942
+ hash: "SHA-256"
943
+ }, pbkdf2Key, KEY_LENGTH * 8);
944
+ let cryptoKey;
945
+ if (isV2) {
946
+ const hkdfKey = await subtle.importKey("raw", masterBits, "HKDF", false, ["deriveKey"]);
947
+ cryptoKey = await subtle.deriveKey({
948
+ name: "HKDF",
949
+ hash: "SHA-256",
950
+ salt: new Uint8Array(0),
951
+ info: encoder.encode("ziphro-encryption")
952
+ }, hkdfKey, { name: "AES-GCM", length: 256 }, false, ["decrypt"]);
953
+ } else {
954
+ const labelBytes = encoder.encode("ziphro-encryption");
955
+ const combinedKey = new Uint8Array(masterBits.byteLength + labelBytes.length);
956
+ combinedKey.set(new Uint8Array(masterBits), 0);
957
+ combinedKey.set(labelBytes, masterBits.byteLength);
958
+ const hashBuffer = await subtle.digest("SHA-256", combinedKey);
959
+ cryptoKey = await subtle.importKey("raw", hashBuffer, { name: "AES-GCM" }, false, ["decrypt"]);
960
+ }
961
+ const gcmParams = {
962
+ name: "AES-GCM",
963
+ iv: toBufferSource(iv),
964
+ tagLength: 128
965
+ };
966
+ if (isV2 && meta) {
967
+ try {
968
+ const decryptedWithAAD = await subtle.decrypt({
969
+ ...gcmParams,
970
+ additionalData: toBufferSource(buildAADBytes(meta))
971
+ }, cryptoKey, combined);
972
+ return new TextDecoder().decode(decryptedWithAAD);
973
+ } catch {
974
+ }
975
+ }
976
+ const decryptedBuffer = await subtle.decrypt(gcmParams, cryptoKey, combined);
977
+ return new TextDecoder().decode(decryptedBuffer);
978
+ }
979
+ async function checksum(plaintext) {
980
+ const subtle = getSubtleCrypto();
981
+ const data = new TextEncoder().encode(plaintext);
982
+ const hashBuffer = await subtle.digest("SHA-256", data);
983
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
984
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
985
+ }
986
+
987
+ // src/lib/crypto.ts
988
+ async function encrypt(plaintext, passphraseOrKey, meta) {
989
+ let encKey;
990
+ if (typeof passphraseOrKey === "string") {
991
+ const salt = meta?.userId || "ziphro-cli-user";
992
+ const keys = await deriveAccountKeys(passphraseOrKey, salt);
993
+ encKey = keys.encKey;
994
+ } else {
995
+ encKey = passphraseOrKey;
996
+ }
997
+ const envelope = meta || {
998
+ userId: "cli",
999
+ revision: 1,
1000
+ version: 2
1001
+ };
1002
+ return encryptWithAAD(plaintext, encKey, envelope);
1003
+ }
1004
+ async function decrypt(blob, passphraseOrKey, meta) {
1005
+ if (typeof passphraseOrKey !== "string") {
1006
+ return decryptWithAAD(blob, passphraseOrKey, meta);
1007
+ }
1008
+ try {
1009
+ const salt = meta?.userId || "ziphro-cli-user";
1010
+ const keys = await deriveAccountKeys(passphraseOrKey, salt);
1011
+ return await decryptWithAAD(blob, keys.encKey, meta);
1012
+ } catch {
1013
+ return legacyDecrypt(blob, passphraseOrKey, meta);
1014
+ }
1015
+ }
1016
+
1017
+ // src/commands/login.tsx
1018
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1019
+ function LoginUI() {
1020
+ const { exit } = useApp2();
1021
+ const creds = readCredentials();
1022
+ const [email, setEmail] = useState4(creds?.email ?? "");
1023
+ const [password, setPassword] = useState4("");
1024
+ const [stage, setStage] = useState4("input_email");
1025
+ const [currentInput, setCurrentInput] = useState4(creds?.email ?? "");
1026
+ const [errorMsg, setErrorMsg] = useState4("");
1027
+ useEffect4(() => {
1028
+ if (stage === "loading") {
1029
+ const apiUrl = creds?.api_url ?? "https://api-ziphro.vercel.app";
1030
+ (async () => {
1031
+ try {
1032
+ const { authSecret, encKey } = await deriveAccountKeys(password, email);
1033
+ let res = await apiLogin(apiUrl, email, authSecret);
1034
+ if (!res.success) {
1035
+ const legacyRes = await apiLogin(apiUrl, email, password);
1036
+ if (legacyRes.success) {
1037
+ res = legacyRes;
1038
+ }
1039
+ }
1040
+ if (res.success && res.data) {
1041
+ const accessToken = res.data.access_token;
1042
+ const deviceName = `${hostname3() || "CLI"} - ${process.platform || "Client"}`;
1043
+ const devRes = await apiRegisterDevice(apiUrl, accessToken, deviceName, "cli");
1044
+ if (devRes.success && devRes.data) {
1045
+ const existing = readCredentials();
1046
+ writeCredentials({
1047
+ ...existing,
1048
+ api_url: apiUrl,
1049
+ email,
1050
+ access_token: accessToken,
1051
+ refresh_token: res.data.refresh_token,
1052
+ expires_at: res.data.expires_at,
1053
+ device_id: devRes.data.id,
1054
+ vault_key: Buffer.from(encKey).toString("base64")
1055
+ });
1056
+ setStage("success");
1057
+ setTimeout(() => exit(), 1200);
1058
+ } else {
1059
+ setErrorMsg(devRes.error?.message ?? "Device registration failed");
1060
+ setStage("error");
1061
+ setTimeout(() => exit(new Error(devRes.error?.message)), 1500);
1062
+ }
1063
+ } else {
1064
+ setErrorMsg(res.error?.message ?? "Login failed");
1065
+ setStage("error");
1066
+ setTimeout(() => exit(new Error(res.error?.message)), 1500);
1067
+ }
1068
+ } catch (err) {
1069
+ setErrorMsg(err.message || "Authentication error");
1070
+ setStage("error");
1071
+ setTimeout(() => exit(err), 1500);
1072
+ }
1073
+ })();
1074
+ }
1075
+ }, [stage]);
1076
+ useInput2((input, key) => {
1077
+ if (stage === "loading" || stage === "success") return;
1078
+ if (key.return) {
1079
+ if (stage === "input_email") {
1080
+ if (!currentInput.trim()) return;
1081
+ setEmail(currentInput.trim());
1082
+ setCurrentInput("");
1083
+ setStage("input_password");
1084
+ } else if (stage === "input_password") {
1085
+ if (!currentInput.trim()) return;
1086
+ setPassword(currentInput.trim());
1087
+ setCurrentInput("");
1088
+ setStage("loading");
1089
+ }
1090
+ return;
1091
+ }
1092
+ if (key.backspace || key.delete) {
1093
+ setCurrentInput((v) => v.slice(0, -1));
1094
+ return;
1095
+ }
1096
+ if (key.escape) {
1097
+ exit();
1098
+ return;
1099
+ }
1100
+ setCurrentInput((v) => v + input);
1101
+ });
1102
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
1103
+ /* @__PURE__ */ jsx5(MiniHeader, {}),
1104
+ /* @__PURE__ */ jsxs5(
1105
+ Box5,
1106
+ {
1107
+ flexDirection: "column",
1108
+ borderStyle: "round",
1109
+ borderColor: BRAND.border,
1110
+ paddingX: 2,
1111
+ paddingY: 1,
1112
+ marginTop: 1,
1113
+ children: [
1114
+ /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, justifyContent: "space-between", children: [
1115
+ /* @__PURE__ */ jsx5(Text5, { color: BRAND.orange, bold: true, children: "\u2726 Ziphro Vault Authentication" }),
1116
+ /* @__PURE__ */ jsx5(Text5, { color: BRAND.muted, children: "E2EE AES-256-GCM" })
1117
+ ] }),
1118
+ /* @__PURE__ */ jsxs5(Box5, { marginBottom: 0, children: [
1119
+ /* @__PURE__ */ jsx5(Text5, { color: stage === "input_email" ? BRAND.orange : BRAND.success, bold: true, children: stage !== "input_email" ? "\u2713 " : "\u2192 " }),
1120
+ /* @__PURE__ */ jsx5(Text5, { color: BRAND.muted, children: "Email: " }),
1121
+ stage === "input_email" ? /* @__PURE__ */ jsxs5(Text5, { color: BRAND.white, bold: true, children: [
1122
+ currentInput,
1123
+ /* @__PURE__ */ jsx5(Text5, { color: BRAND.orange, children: "\u2588" })
1124
+ ] }) : /* @__PURE__ */ jsx5(Text5, { color: BRAND.white, children: email })
1125
+ ] }),
1126
+ (stage === "input_password" || stage === "loading" || stage === "success" || stage === "error") && /* @__PURE__ */ jsxs5(Box5, { marginBottom: 0, children: [
1127
+ /* @__PURE__ */ jsx5(
1128
+ Text5,
1129
+ {
1130
+ color: stage === "input_password" ? BRAND.orange : stage === "success" ? BRAND.success : BRAND.muted,
1131
+ bold: true,
1132
+ children: stage !== "input_password" ? "\u2713 " : "\u2192 "
1133
+ }
1134
+ ),
1135
+ /* @__PURE__ */ jsx5(Text5, { color: BRAND.muted, children: "Password: " }),
1136
+ stage === "input_password" ? /* @__PURE__ */ jsxs5(Text5, { color: BRAND.orange, children: [
1137
+ "\u2022".repeat(currentInput.length),
1138
+ /* @__PURE__ */ jsx5(Text5, { color: BRAND.orange, children: "\u2588" })
1139
+ ] }) : /* @__PURE__ */ jsx5(Text5, { color: BRAND.muted, children: "\u2022".repeat(8) })
1140
+ ] }),
1141
+ /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
1142
+ stage === "loading" && /* @__PURE__ */ jsx5(Spinner, { label: "Deriving zero-knowledge keys & authenticating\u2026", type: "orbit", color: BRAND.orange }),
1143
+ stage === "success" && /* @__PURE__ */ jsx5(StatusBadge, { type: "success", message: `Authenticated successfully as ${email}` }),
1144
+ stage === "error" && /* @__PURE__ */ jsx5(StatusBadge, { type: "error", message: errorMsg })
1145
+ ] }),
1146
+ (stage === "input_email" || stage === "input_password") && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: BRAND.muted, children: [
1147
+ "Press ",
1148
+ /* @__PURE__ */ jsx5(Text5, { color: BRAND.orange, bold: true, children: "[Enter]" }),
1149
+ " to confirm \xB7 ",
1150
+ /* @__PURE__ */ jsx5(Text5, { color: BRAND.orange, bold: true, children: "[Esc]" }),
1151
+ " to cancel"
1152
+ ] }) })
1153
+ ]
1154
+ }
1155
+ )
1156
+ ] });
1157
+ }
1158
+ function registerLogin(program2) {
1159
+ program2.command("login").description("Authenticate with Ziphro API (Zero-Knowledge)").action(async () => {
1160
+ const { render: render5 } = await import("ink");
1161
+ render5(React4.createElement(LoginUI));
1162
+ });
1163
+ }
1164
+
1165
+ // src/commands/logout.tsx
1166
+ import React5, { useEffect as useEffect5, useState as useState5 } from "react";
1167
+ import { Box as Box6, useApp as useApp3 } from "ink";
1168
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1169
+ function LogoutUI() {
1170
+ const { exit } = useApp3();
1171
+ const [status, setStatus] = useState5("loading");
1172
+ const [message, setMessage] = useState5("");
1173
+ useEffect5(() => {
1174
+ const creds = readCredentials();
1175
+ if (!creds?.access_token) {
1176
+ setMessage("Not logged in.");
1177
+ setStatus("error");
1178
+ setTimeout(() => exit(), 1e3);
1179
+ return;
1180
+ }
1181
+ apiLogout(creds.api_url, creds.access_token).finally(() => {
1182
+ clearTokens();
1183
+ setMessage(`Logged out. Session cleared for ${creds.email}`);
1184
+ setStatus("success");
1185
+ setTimeout(() => exit(), 1200);
1186
+ });
1187
+ }, []);
1188
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", padding: 1, children: [
1189
+ /* @__PURE__ */ jsx6(MiniHeader, {}),
1190
+ /* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
1191
+ status === "loading" && /* @__PURE__ */ jsx6(Spinner, { label: "Logging out\u2026", type: "orbit" }),
1192
+ status === "success" && /* @__PURE__ */ jsx6(StatusBadge, { type: "success", message }),
1193
+ status === "error" && /* @__PURE__ */ jsx6(StatusBadge, { type: "error", message })
1194
+ ] })
1195
+ ] });
1196
+ }
1197
+ function registerLogout(program2) {
1198
+ program2.command("logout").description("Clear local credentials and revoke session").action(async () => {
1199
+ const { render: render5 } = await import("ink");
1200
+ render5(React5.createElement(LogoutUI));
1201
+ });
1202
+ }
1203
+
1204
+ // src/commands/whoami.tsx
1205
+ import React6 from "react";
1206
+ import { Box as Box7, Text as Text7 } from "ink";
1207
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1208
+ function WhoamiUI() {
1209
+ let auth = null;
1210
+ let error = "";
1211
+ try {
1212
+ auth = requireAuth();
1213
+ } catch (e) {
1214
+ error = e.message;
1215
+ }
1216
+ const creds = readCredentials();
1217
+ if (error || !auth) {
1218
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
1219
+ /* @__PURE__ */ jsx7(MiniHeader, {}),
1220
+ /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(StatusBadge, { type: "error", message: error || "Not authenticated" }) }),
1221
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
1222
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.muted, children: "Run " }),
1223
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.orange, bold: true, children: "ziphro login" }),
1224
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.muted, children: " to authenticate into your cryptographic vault." })
1225
+ ] })
1226
+ ] });
1227
+ }
1228
+ const expiresAt = new Date(auth.expiresAt * 1e3);
1229
+ const now = /* @__PURE__ */ new Date();
1230
+ const msLeft = expiresAt.getTime() - now.getTime();
1231
+ const hoursLeft = Math.floor(msLeft / 1e3 / 60 / 60);
1232
+ const minutesLeft = Math.floor(msLeft / 1e3 / 60 % 60);
1233
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
1234
+ /* @__PURE__ */ jsx7(AsciiHeader, {}),
1235
+ /* @__PURE__ */ jsxs7(
1236
+ Box7,
1237
+ {
1238
+ flexDirection: "column",
1239
+ borderStyle: "round",
1240
+ borderColor: BRAND.border,
1241
+ paddingX: 2,
1242
+ paddingY: 1,
1243
+ children: [
1244
+ /* @__PURE__ */ jsxs7(Box7, { marginBottom: 1, justifyContent: "space-between", children: [
1245
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.orange, bold: true, children: "\u2726 Authenticated Vault Session" }),
1246
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.muted, children: "ACTIVE" })
1247
+ ] }),
1248
+ /* @__PURE__ */ jsxs7(Box7, { marginBottom: 0, children: [
1249
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.muted, children: " Email: " }),
1250
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.white, bold: true, children: auth.email })
1251
+ ] }),
1252
+ /* @__PURE__ */ jsxs7(Box7, { marginBottom: 0, children: [
1253
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.muted, children: " Device ID: " }),
1254
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.orange, children: auth.deviceId })
1255
+ ] }),
1256
+ /* @__PURE__ */ jsxs7(Box7, { marginBottom: 0, children: [
1257
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.muted, children: " API URL: " }),
1258
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.subtle, children: creds?.api_url ?? "\u2014" })
1259
+ ] }),
1260
+ /* @__PURE__ */ jsxs7(Box7, { marginBottom: 0, children: [
1261
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.muted, children: " Expires: " }),
1262
+ /* @__PURE__ */ jsx7(Text7, { color: hoursLeft < 1 ? BRAND.error : BRAND.success, bold: true, children: hoursLeft > 0 ? `${hoursLeft}h ${minutesLeft}m remaining` : `${minutesLeft}m remaining` })
1263
+ ] }),
1264
+ /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
1265
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.success, bold: true, children: " \u2713 " }),
1266
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.muted, children: "Zero-knowledge encryption active \xB7 " }),
1267
+ /* @__PURE__ */ jsx7(Text7, { color: BRAND.orange, bold: true, children: "AES-256-GCM" })
1268
+ ] })
1269
+ ]
1270
+ }
1271
+ )
1272
+ ] });
1273
+ }
1274
+ function registerWhoami(program2) {
1275
+ program2.command("whoami").description("Display the currently authenticated user").action(async () => {
1276
+ const { render: render5 } = await import("ink");
1277
+ render5(React6.createElement(WhoamiUI));
1278
+ });
1279
+ }
1280
+
1281
+ // src/commands/add.tsx
1282
+ import React7, { useEffect as useEffect6, useState as useState6 } from "react";
1283
+ import { Box as Box8, Text as Text8, useApp as useApp4 } from "ink";
1284
+ import { v4 as uuidv42 } from "uuid";
1285
+
1286
+ // src/lib/storage.ts
1287
+ import { join as join2 } from "path";
1288
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, renameSync } from "fs";
1289
+ import { v4 as uuidv4 } from "uuid";
1290
+ var STORE_FILE = join2(ZIPHRO_DIR, "tasks.json");
1291
+ function defaultStore() {
1292
+ return { tasks: [], sync_queue: [] };
1293
+ }
1294
+ function ensureDir() {
1295
+ if (!existsSync2(ZIPHRO_DIR)) {
1296
+ mkdirSync2(ZIPHRO_DIR, { recursive: true, mode: 448 });
1297
+ }
1298
+ }
1299
+ function writeStoreSecurely(storeDir, storePath, data) {
1300
+ ensureDir();
1301
+ const tmpPath = join2(storeDir, `.store.${process.pid}.${Date.now()}.tmp`);
1302
+ writeFileSync2(tmpPath, JSON.stringify(data, null, 2), {
1303
+ encoding: "utf8",
1304
+ mode: 384
1305
+ });
1306
+ renameSync(tmpPath, storePath);
1307
+ }
1308
+ function readStore() {
1309
+ ensureDir();
1310
+ if (!existsSync2(STORE_FILE)) return defaultStore();
1311
+ try {
1312
+ return JSON.parse(readFileSync2(STORE_FILE, "utf8"));
1313
+ } catch {
1314
+ return defaultStore();
1315
+ }
1316
+ }
1317
+ function writeStore(store) {
1318
+ writeStoreSecurely(ZIPHRO_DIR, STORE_FILE, store);
1319
+ }
1320
+ function getAllTasks() {
1321
+ return readStore().tasks;
1322
+ }
1323
+ function replaceTasks(tasks) {
1324
+ const store = readStore();
1325
+ store.tasks = tasks;
1326
+ writeStore(store);
1327
+ }
1328
+ function getTask(id) {
1329
+ const matches = readStore().tasks.filter((t) => t.id === id || t.id.startsWith(id));
1330
+ if (matches.length > 1) {
1331
+ const list = matches.map((t) => `${t.id.substring(0, 8)} ("${t.description}")`).join(", ");
1332
+ throw new Error(`Ambiguous ID "${id}". Matches multiple tasks: ${list}`);
1333
+ }
1334
+ return matches[0];
1335
+ }
1336
+ function insertTask(task) {
1337
+ const store = readStore();
1338
+ store.tasks.push(task);
1339
+ queueOperation(store, "add", task.id);
1340
+ writeStore(store);
1341
+ }
1342
+ function updateTask(id, updates) {
1343
+ const store = readStore();
1344
+ const matches = store.tasks.filter((t) => t.id === id || t.id.startsWith(id));
1345
+ if (matches.length === 0) return null;
1346
+ if (matches.length > 1) {
1347
+ const list = matches.map((t) => `${t.id.substring(0, 8)} ("${t.description}")`).join(", ");
1348
+ throw new Error(`Ambiguous ID "${id}". Matches multiple tasks: ${list}`);
1349
+ }
1350
+ const targetId = matches[0].id;
1351
+ const idx = store.tasks.findIndex((t) => t.id === targetId);
1352
+ store.tasks[idx] = {
1353
+ ...store.tasks[idx],
1354
+ ...updates,
1355
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1356
+ };
1357
+ queueOperation(store, "update", store.tasks[idx].id);
1358
+ writeStore(store);
1359
+ return store.tasks[idx];
1360
+ }
1361
+ function deleteTask(id) {
1362
+ const store = readStore();
1363
+ const matches = store.tasks.filter((t) => t.id === id || t.id.startsWith(id));
1364
+ if (matches.length === 0) return false;
1365
+ if (matches.length > 1) {
1366
+ const list = matches.map((t) => `${t.id.substring(0, 8)} ("${t.description}")`).join(", ");
1367
+ throw new Error(`Ambiguous ID "${id}". Matches multiple tasks: ${list}`);
1368
+ }
1369
+ const targetId = matches[0].id;
1370
+ const idx = store.tasks.findIndex((t) => t.id === targetId);
1371
+ const taskId = store.tasks[idx].id;
1372
+ store.tasks.splice(idx, 1);
1373
+ queueOperation(store, "delete", taskId);
1374
+ writeStore(store);
1375
+ return true;
1376
+ }
1377
+ function queueOperation(store, operation, taskId) {
1378
+ store.sync_queue.push({
1379
+ id: uuidv4(),
1380
+ operation,
1381
+ task_id: taskId,
1382
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1383
+ });
1384
+ }
1385
+ function clearSyncQueue() {
1386
+ const store = readStore();
1387
+ store.sync_queue = [];
1388
+ store.last_synced_at = (/* @__PURE__ */ new Date()).toISOString();
1389
+ writeStore(store);
1390
+ }
1391
+ function getLastSyncedAt() {
1392
+ return readStore().last_synced_at;
1393
+ }
1394
+ function filterTasks(filter) {
1395
+ let tasks = getAllTasks();
1396
+ if (filter.status && filter.status !== "all") {
1397
+ tasks = tasks.filter((t) => t.status === filter.status);
1398
+ }
1399
+ if (filter.priority && filter.priority !== "all") {
1400
+ tasks = tasks.filter((t) => t.priority === filter.priority);
1401
+ }
1402
+ if (filter.tags && filter.tags.length > 0) {
1403
+ tasks = tasks.filter(
1404
+ (t) => filter.tags.some((tag) => t.tags.includes(tag))
1405
+ );
1406
+ }
1407
+ return tasks;
1408
+ }
1409
+ function computeStats() {
1410
+ const tasks = getAllTasks();
1411
+ const now = /* @__PURE__ */ new Date();
1412
+ const pending = tasks.filter((t) => t.status === "pending").length;
1413
+ const in_progress = tasks.filter((t) => t.status === "in_progress").length;
1414
+ const completed = tasks.filter((t) => t.status === "completed").length;
1415
+ const overdue = tasks.filter(
1416
+ (t) => t.due_date && t.status !== "completed" && new Date(t.due_date) < now
1417
+ ).length;
1418
+ return {
1419
+ total: tasks.length,
1420
+ pending,
1421
+ in_progress,
1422
+ completed,
1423
+ high_priority: tasks.filter((t) => t.priority === "high").length,
1424
+ medium_priority: tasks.filter((t) => t.priority === "medium").length,
1425
+ low_priority: tasks.filter((t) => t.priority === "low").length,
1426
+ overdue,
1427
+ completion_rate: tasks.length > 0 ? Math.round(completed / tasks.length * 100) : 0
1428
+ };
1429
+ }
1430
+
1431
+ // src/commands/add.tsx
1432
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1433
+ function AddUI({
1434
+ description,
1435
+ priority,
1436
+ due,
1437
+ tags
1438
+ }) {
1439
+ const { exit } = useApp4();
1440
+ const [status, setStatus] = useState6("working");
1441
+ const [syncStatus, setSyncStatus] = useState6("idle");
1442
+ const [syncMessage, setSyncMessage] = useState6("");
1443
+ const [taskId, setTaskId] = useState6("");
1444
+ const [errorMsg, setErrorMsg] = useState6("");
1445
+ useEffect6(() => {
1446
+ async function createTaskAndSync() {
1447
+ try {
1448
+ const auth = requireAuth();
1449
+ const task = {
1450
+ id: uuidv42(),
1451
+ description,
1452
+ priority,
1453
+ status: "pending",
1454
+ tags,
1455
+ due_date: due,
1456
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1457
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
1458
+ device_id: auth.deviceId,
1459
+ vector_clock: { [auth.deviceId]: 1 }
1460
+ };
1461
+ insertTask(task);
1462
+ setTaskId(task.id.slice(0, 8));
1463
+ setStatus("success");
1464
+ setSyncStatus("syncing");
1465
+ const tasks = getAllTasks();
1466
+ const bundle = {
1467
+ tasks,
1468
+ schema_version: "1",
1469
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
1470
+ };
1471
+ const plaintext = JSON.stringify(bundle);
1472
+ const blobChecksum = await checksum(plaintext);
1473
+ const syncPassword = auth.password;
1474
+ if (!syncPassword) {
1475
+ setSyncStatus("failed");
1476
+ setSyncMessage('Encryption password required. Run "ziphro sync --password <pwd>".');
1477
+ setTimeout(() => exit(), 3e3);
1478
+ return;
1479
+ }
1480
+ const blob = encrypt(plaintext, syncPassword);
1481
+ const res = await apiUploadSync(
1482
+ auth.apiUrl,
1483
+ auth.accessToken,
1484
+ blob,
1485
+ blobChecksum,
1486
+ auth.deviceId
1487
+ );
1488
+ if (res.success) {
1489
+ clearSyncQueue();
1490
+ setSyncStatus("synced");
1491
+ setTimeout(() => exit(), 2e3);
1492
+ } else if (res.error?.code === "NETWORK_ERROR") {
1493
+ setSyncStatus("offline");
1494
+ setTimeout(() => exit(), 2500);
1495
+ } else {
1496
+ setSyncStatus("failed");
1497
+ setSyncMessage(res.error?.message ?? "Sync failed");
1498
+ setTimeout(() => exit(), 3500);
1499
+ }
1500
+ } catch (e) {
1501
+ setErrorMsg(e.message);
1502
+ setStatus("error");
1503
+ setTimeout(() => exit(e), 2e3);
1504
+ }
1505
+ }
1506
+ createTaskAndSync();
1507
+ }, []);
1508
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", padding: 1, children: [
1509
+ /* @__PURE__ */ jsx8(MiniHeader, {}),
1510
+ status === "working" && /* @__PURE__ */ jsx8(Spinner, { label: "Creating task\u2026", type: "star", color: BRAND.orange }),
1511
+ status === "success" && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1512
+ /* @__PURE__ */ jsx8(StarBurst, { label: `Task created! ID: ${taskId}`, color: BRAND.orange }),
1513
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
1514
+ /* @__PURE__ */ jsxs8(Box8, { children: [
1515
+ /* @__PURE__ */ jsx8(Text8, { color: BRAND.muted, children: " Description: " }),
1516
+ /* @__PURE__ */ jsx8(Text8, { color: BRAND.white, bold: true, children: description })
1517
+ ] }),
1518
+ /* @__PURE__ */ jsxs8(Box8, { children: [
1519
+ /* @__PURE__ */ jsx8(Text8, { color: BRAND.muted, children: " Priority: " }),
1520
+ /* @__PURE__ */ jsx8(
1521
+ Text8,
1522
+ {
1523
+ color: priority === "high" ? BRAND.error : priority === "medium" ? BRAND.warning : BRAND.success,
1524
+ bold: true,
1525
+ children: priority.toUpperCase()
1526
+ }
1527
+ )
1528
+ ] }),
1529
+ due && /* @__PURE__ */ jsxs8(Box8, { children: [
1530
+ /* @__PURE__ */ jsx8(Text8, { color: BRAND.muted, children: " Due date: " }),
1531
+ /* @__PURE__ */ jsx8(Text8, { color: BRAND.orange, children: due })
1532
+ ] }),
1533
+ tags.length > 0 && /* @__PURE__ */ jsxs8(Box8, { children: [
1534
+ /* @__PURE__ */ jsx8(Text8, { color: BRAND.muted, children: " Tags: " }),
1535
+ /* @__PURE__ */ jsx8(Text8, { color: BRAND.tag, children: tags.map((t) => `#${t}`).join(" ") })
1536
+ ] }),
1537
+ /* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
1538
+ /* @__PURE__ */ jsx8(Text8, { color: BRAND.muted, children: " Sync: " }),
1539
+ syncStatus === "syncing" && /* @__PURE__ */ jsx8(Text8, { color: BRAND.orange, children: "Syncing to cloud (E2E encrypted)..." }),
1540
+ syncStatus === "synced" && /* @__PURE__ */ jsx8(Text8, { color: BRAND.success, children: "\u2713 Synced to cloud" }),
1541
+ syncStatus === "offline" && /* @__PURE__ */ jsx8(Text8, { color: BRAND.warning, children: "\u2601 Offline (queued locally)" }),
1542
+ syncStatus === "failed" && /* @__PURE__ */ jsxs8(Text8, { color: BRAND.error, children: [
1543
+ "\u2717 ",
1544
+ syncMessage
1545
+ ] })
1546
+ ] })
1547
+ ] })
1548
+ ] }),
1549
+ status === "error" && /* @__PURE__ */ jsx8(StatusBadge, { type: "error", message: errorMsg })
1550
+ ] });
1551
+ }
1552
+ function registerAdd(program2) {
1553
+ program2.command("add [description]").alias("-a").description("Create a new task").option("-p, --priority <level>", "Priority: high | medium | low", "medium").option("-d, --due <datetime>", "Due date (ISO 8601 or YYYY-MM-DD)").option("-t, --tags <tags>", "Comma-separated tags").action(async (description, options) => {
1554
+ if (!description) {
1555
+ console.error('\u2717 Description is required. Usage: ziphro add "description"');
1556
+ process.exit(1);
1557
+ }
1558
+ const priority = ["high", "medium", "low"].includes(options.priority) ? options.priority : "medium";
1559
+ const tags = options.tags ? options.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
1560
+ const { render: render5 } = await import("ink");
1561
+ render5(
1562
+ React7.createElement(AddUI, {
1563
+ description,
1564
+ priority,
1565
+ due: options.due,
1566
+ tags
1567
+ })
1568
+ );
1569
+ });
1570
+ }
1571
+
1572
+ // src/commands/list.tsx
1573
+ import React9, { useEffect as useEffect8, useState as useState8 } from "react";
1574
+ import { Box as Box11, Text as Text11, useApp as useApp6, render } from "ink";
1575
+
1576
+ // src/components/TaskTable.tsx
1577
+ import { Box as Box9, Text as Text9 } from "ink";
1578
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1579
+ function priorityBadge(p) {
1580
+ switch (p) {
1581
+ case "high":
1582
+ return { icon: "\u25CF", label: "HIGH", color: BRAND.error };
1583
+ case "medium":
1584
+ return { icon: "\u25B2", label: "MED ", color: BRAND.warning };
1585
+ case "low":
1586
+ return { icon: "\u25C6", label: "LOW ", color: BRAND.success };
1587
+ default:
1588
+ return { icon: "\u25CB", label: "NONE", color: BRAND.muted };
1589
+ }
1590
+ }
1591
+ function statusBadge(s) {
1592
+ switch (s) {
1593
+ case "completed":
1594
+ return { icon: "\u2713", label: "DONE", color: BRAND.success };
1595
+ case "in_progress":
1596
+ return { icon: "\u27F3", label: "IN PROG", color: BRAND.info };
1597
+ case "pending":
1598
+ default:
1599
+ return { icon: "\u25CB", label: "PENDING", color: BRAND.subtle };
1600
+ }
1601
+ }
1602
+ function truncate(str, max) {
1603
+ if (!str) return "";
1604
+ if (str.length <= max) return str;
1605
+ return str.slice(0, Math.max(1, max - 1)) + "\u2026";
1606
+ }
1607
+ function formatDate(iso) {
1608
+ if (!iso) return "\u2014";
1609
+ try {
1610
+ const d = new Date(iso);
1611
+ if (isNaN(d.getTime())) return "\u2014";
1612
+ const m = String(d.getMonth() + 1).padStart(2, "0");
1613
+ const day = String(d.getDate()).padStart(2, "0");
1614
+ return `${m}-${day}`;
1615
+ } catch {
1616
+ return "\u2014";
1617
+ }
1618
+ }
1619
+ function isOverdue(task) {
1620
+ if (!task.due_date || task.status === "completed") return false;
1621
+ try {
1622
+ const due = new Date(task.due_date).getTime();
1623
+ return !isNaN(due) && due < Date.now();
1624
+ } catch {
1625
+ return false;
1626
+ }
1627
+ }
1628
+ function getResponsiveColumns(totalCols) {
1629
+ const terminalWidth = totalCols || process.stdout?.columns || 80;
1630
+ const effectiveWidth = Math.max(54, terminalWidth - 4);
1631
+ const colId = { id: "id", header: "ID", width: 11 };
1632
+ const colPriority = { id: "priority", header: "PRIORITY", width: 8 };
1633
+ const colStatus = { id: "status", header: "STATUS", width: 11 };
1634
+ const colDue = { id: "due", header: "DUE", width: 7 };
1635
+ const colTags = { id: "tags", header: "TAGS", width: 12 };
1636
+ if (effectiveWidth >= 92) {
1637
+ const fixedTotal2 = colId.width + colPriority.width + colStatus.width + colDue.width + colTags.width;
1638
+ const borders2 = 6 * 3 + 1;
1639
+ const descWidth2 = Math.max(20, effectiveWidth - (fixedTotal2 + borders2));
1640
+ return [
1641
+ colId,
1642
+ { id: "desc", header: "TASK DESCRIPTION", width: descWidth2 },
1643
+ colPriority,
1644
+ colStatus,
1645
+ colDue,
1646
+ colTags
1647
+ ];
1648
+ }
1649
+ if (effectiveWidth >= 72) {
1650
+ const fixedTotal2 = colId.width + colPriority.width + colStatus.width + colDue.width;
1651
+ const borders2 = 5 * 3 + 1;
1652
+ const descWidth2 = Math.max(18, effectiveWidth - (fixedTotal2 + borders2));
1653
+ return [
1654
+ colId,
1655
+ { id: "desc", header: "TASK DESCRIPTION", width: descWidth2 },
1656
+ colPriority,
1657
+ colStatus,
1658
+ colDue
1659
+ ];
1660
+ }
1661
+ const fixedTotal = colId.width + colPriority.width + colStatus.width;
1662
+ const borders = 4 * 3 + 1;
1663
+ const descWidth = Math.max(14, effectiveWidth - (fixedTotal + borders));
1664
+ return [
1665
+ colId,
1666
+ { id: "desc", header: "TASK DESCRIPTION", width: descWidth },
1667
+ colPriority,
1668
+ colStatus
1669
+ ];
1670
+ }
1671
+ function TaskTable({
1672
+ tasks,
1673
+ selectedIndex,
1674
+ maxRows = 12,
1675
+ terminalWidth
1676
+ }) {
1677
+ const cols = getResponsiveColumns(terminalWidth);
1678
+ const borderColor = BRAND.border;
1679
+ const headerColor = BRAND.white;
1680
+ if (tasks.length === 0) {
1681
+ const width = Math.max(50, cols.reduce((sum, c) => sum + c.width + 3, 1));
1682
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", marginY: 1, children: [
1683
+ /* @__PURE__ */ jsxs9(Text9, { color: borderColor, children: [
1684
+ "\u250C",
1685
+ "\u2500".repeat(width - 2),
1686
+ "\u2510"
1687
+ ] }),
1688
+ /* @__PURE__ */ jsxs9(Box9, { children: [
1689
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: "\u2502 " }),
1690
+ /* @__PURE__ */ jsx9(Text9, { color: BRAND.muted, children: "\u25CB No tasks found matching the current filter." }),
1691
+ /* @__PURE__ */ jsxs9(Text9, { color: borderColor, children: [
1692
+ " ".repeat(Math.max(0, width - 47)),
1693
+ "\u2502"
1694
+ ] })
1695
+ ] }),
1696
+ /* @__PURE__ */ jsxs9(Box9, { children: [
1697
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: "\u2502 " }),
1698
+ /* @__PURE__ */ jsx9(Text9, { color: BRAND.orange, children: " Press [a] to create a new task or [f] to switch filters." }),
1699
+ /* @__PURE__ */ jsxs9(Text9, { color: borderColor, children: [
1700
+ " ".repeat(Math.max(0, width - 60)),
1701
+ "\u2502"
1702
+ ] })
1703
+ ] }),
1704
+ /* @__PURE__ */ jsxs9(Text9, { color: borderColor, children: [
1705
+ "\u2514",
1706
+ "\u2500".repeat(width - 2),
1707
+ "\u2518"
1708
+ ] })
1709
+ ] });
1710
+ }
1711
+ let displayTasks = tasks;
1712
+ let startIndex = 0;
1713
+ if (tasks.length > maxRows) {
1714
+ const curr = selectedIndex ?? 0;
1715
+ const half = Math.floor(maxRows / 2);
1716
+ startIndex = Math.max(0, Math.min(curr - half, tasks.length - maxRows));
1717
+ displayTasks = tasks.slice(startIndex, startIndex + maxRows);
1718
+ }
1719
+ const topBorder = "\u250C" + cols.map((c) => "\u2500".repeat(c.width + 2)).join("\u252C") + "\u2510";
1720
+ const headerSeparator = "\u251C" + cols.map((c) => "\u2500".repeat(c.width + 2)).join("\u253C") + "\u2524";
1721
+ const bottomBorder = "\u2514" + cols.map((c) => "\u2500".repeat(c.width + 2)).join("\u2534") + "\u2518";
1722
+ const colMap = new Map(cols.map((c) => [c.id, c.width]));
1723
+ const hasDue = colMap.has("due");
1724
+ const hasTags = colMap.has("tags");
1725
+ const wId = colMap.get("id") ?? 11;
1726
+ const wDesc = colMap.get("desc") ?? 24;
1727
+ const wPriority = colMap.get("priority") ?? 8;
1728
+ const wStatus = colMap.get("status") ?? 11;
1729
+ const wDue = colMap.get("due") ?? 7;
1730
+ const wTags = colMap.get("tags") ?? 12;
1731
+ return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
1732
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: topBorder }),
1733
+ /* @__PURE__ */ jsxs9(Box9, { children: [
1734
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: "\u2502 " }),
1735
+ /* @__PURE__ */ jsx9(Text9, { color: headerColor, bold: true, children: "ID".padEnd(wId) }),
1736
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502 " }),
1737
+ /* @__PURE__ */ jsx9(Text9, { color: headerColor, bold: true, children: "TASK DESCRIPTION".padEnd(wDesc) }),
1738
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502 " }),
1739
+ /* @__PURE__ */ jsx9(Text9, { color: headerColor, bold: true, children: "PRIORITY".padEnd(wPriority) }),
1740
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502 " }),
1741
+ /* @__PURE__ */ jsx9(Text9, { color: headerColor, bold: true, children: "STATUS".padEnd(wStatus) }),
1742
+ hasDue && /* @__PURE__ */ jsxs9(Fragment2, { children: [
1743
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502 " }),
1744
+ /* @__PURE__ */ jsx9(Text9, { color: headerColor, bold: true, children: "DUE".padEnd(wDue) })
1745
+ ] }),
1746
+ hasTags && /* @__PURE__ */ jsxs9(Fragment2, { children: [
1747
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502 " }),
1748
+ /* @__PURE__ */ jsx9(Text9, { color: headerColor, bold: true, children: "TAGS".padEnd(wTags) })
1749
+ ] }),
1750
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502" })
1751
+ ] }),
1752
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: headerSeparator }),
1753
+ displayTasks.map((task, localIdx) => {
1754
+ const actualIdx = startIndex + localIdx;
1755
+ const isSelected = selectedIndex !== void 0 && actualIdx === selectedIndex;
1756
+ const overdue = isOverdue(task);
1757
+ const pBadge = priorityBadge(task.priority);
1758
+ const sBadge = statusBadge(task.status);
1759
+ const cursor = isSelected ? "\u25B8 " : " ";
1760
+ const idText = task.id.slice(0, 8);
1761
+ const idFormatted = idText.padEnd(wId - 2);
1762
+ const descText = truncate(task.description, wDesc).padEnd(wDesc);
1763
+ const pText = `${pBadge.icon} ${pBadge.label}`.padEnd(wPriority);
1764
+ const sText = `${sBadge.icon} ${sBadge.label}`.padEnd(wStatus);
1765
+ const dueDate = formatDate(task.due_date);
1766
+ const dueText = (overdue ? `! ${dueDate}` : dueDate).padEnd(wDue);
1767
+ const tagContent = task.tags.length > 0 ? truncate(task.tags.map((t) => `#${t}`).join(" "), wTags) : "\u2014";
1768
+ const tagText = tagContent.padEnd(wTags);
1769
+ return /* @__PURE__ */ jsxs9(Box9, { children: [
1770
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: "\u2502 " }),
1771
+ /* @__PURE__ */ jsx9(Text9, { color: isSelected ? BRAND.orange : BRAND.muted, bold: isSelected, children: cursor }),
1772
+ /* @__PURE__ */ jsx9(Text9, { color: isSelected ? BRAND.white : BRAND.muted, bold: isSelected, children: idFormatted }),
1773
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502 " }),
1774
+ /* @__PURE__ */ jsx9(
1775
+ Text9,
1776
+ {
1777
+ color: isSelected ? BRAND.white : task.status === "completed" ? BRAND.muted : overdue ? BRAND.error : "#e5e5ea",
1778
+ bold: isSelected,
1779
+ children: descText
1780
+ }
1781
+ ),
1782
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502 " }),
1783
+ /* @__PURE__ */ jsx9(Text9, { color: pBadge.color, bold: task.priority === "high" || isSelected, children: pText }),
1784
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502 " }),
1785
+ /* @__PURE__ */ jsx9(Text9, { color: sBadge.color, bold: isSelected, children: sText }),
1786
+ hasDue && /* @__PURE__ */ jsxs9(Fragment2, { children: [
1787
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502 " }),
1788
+ /* @__PURE__ */ jsx9(Text9, { color: overdue ? BRAND.error : BRAND.subtle, bold: overdue, children: dueText })
1789
+ ] }),
1790
+ hasTags && /* @__PURE__ */ jsxs9(Fragment2, { children: [
1791
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502 " }),
1792
+ /* @__PURE__ */ jsx9(Text9, { color: BRAND.tag, children: tagText })
1793
+ ] }),
1794
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: " \u2502" })
1795
+ ] }, task.id);
1796
+ }),
1797
+ /* @__PURE__ */ jsx9(Text9, { color: borderColor, children: bottomBorder }),
1798
+ /* @__PURE__ */ jsxs9(Box9, { marginTop: 0, paddingX: 1, justifyContent: "space-between", children: [
1799
+ /* @__PURE__ */ jsxs9(Box9, { gap: 1, children: [
1800
+ /* @__PURE__ */ jsx9(Text9, { color: BRAND.muted, children: "Total:" }),
1801
+ /* @__PURE__ */ jsx9(Text9, { color: BRAND.white, bold: true, children: tasks.length }),
1802
+ /* @__PURE__ */ jsx9(Text9, { color: BRAND.muted, children: "\xB7 Completed:" }),
1803
+ /* @__PURE__ */ jsx9(Text9, { color: BRAND.success, bold: true, children: tasks.filter((t) => t.status === "completed").length }),
1804
+ /* @__PURE__ */ jsx9(Text9, { color: BRAND.muted, children: "\xB7 Overdue:" }),
1805
+ /* @__PURE__ */ jsx9(Text9, { color: tasks.some(isOverdue) ? BRAND.error : BRAND.muted, bold: true, children: tasks.filter(isOverdue).length }),
1806
+ tasks.length > maxRows && /* @__PURE__ */ jsxs9(Text9, { color: BRAND.warning, children: [
1807
+ " ",
1808
+ "(Showing ",
1809
+ startIndex + 1,
1810
+ "\u2013",
1811
+ Math.min(startIndex + maxRows, tasks.length),
1812
+ ")"
1813
+ ] })
1814
+ ] }),
1815
+ selectedIndex !== void 0 && tasks.length > 0 && /* @__PURE__ */ jsxs9(Text9, { color: BRAND.muted, children: [
1816
+ "Item ",
1817
+ selectedIndex + 1,
1818
+ " of ",
1819
+ tasks.length
1820
+ ] })
1821
+ ] })
1822
+ ] });
1823
+ }
1824
+ function TaskCompact({ tasks, selectedIndex }) {
1825
+ if (tasks.length === 0) {
1826
+ return /* @__PURE__ */ jsx9(Text9, { color: BRAND.muted, children: "No tasks found." });
1827
+ }
1828
+ return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", borderStyle: "round", borderColor: BRAND.border, paddingX: 1, paddingY: 0, children: tasks.map((task, i) => {
1829
+ const overdue = isOverdue(task);
1830
+ const pBadge = priorityBadge(task.priority);
1831
+ const sBadge = statusBadge(task.status);
1832
+ const isSelected = selectedIndex !== void 0 && selectedIndex === i;
1833
+ return /* @__PURE__ */ jsxs9(Box9, { marginBottom: 0, children: [
1834
+ /* @__PURE__ */ jsx9(Text9, { color: isSelected ? BRAND.orange : BRAND.muted, children: isSelected ? "\u25B8 " : " " }),
1835
+ /* @__PURE__ */ jsxs9(Text9, { color: BRAND.muted, children: [
1836
+ task.id.slice(0, 8),
1837
+ " "
1838
+ ] }),
1839
+ /* @__PURE__ */ jsxs9(Text9, { color: pBadge.color, bold: true, children: [
1840
+ "[",
1841
+ task.priority.slice(0, 1).toUpperCase(),
1842
+ "]",
1843
+ " "
1844
+ ] }),
1845
+ /* @__PURE__ */ jsxs9(
1846
+ Text9,
1847
+ {
1848
+ color: task.status === "completed" ? BRAND.muted : isSelected ? BRAND.white : overdue ? BRAND.error : "#e5e5ea",
1849
+ bold: isSelected,
1850
+ children: [
1851
+ truncate(task.description, 45),
1852
+ " "
1853
+ ]
1854
+ }
1855
+ ),
1856
+ /* @__PURE__ */ jsx9(Text9, { color: sBadge.color, children: sBadge.icon })
1857
+ ] }, task.id);
1858
+ }) });
1859
+ }
1860
+
1861
+ // src/components/InteractiveDashboard.tsx
1862
+ import { useState as useState7, useEffect as useEffect7, useMemo, useCallback } from "react";
1863
+ import { Box as Box10, Text as Text10, useApp as useApp5, useInput as useInput3 } from "ink";
1864
+ import { v4 as uuidv43 } from "uuid";
1865
+
1866
+ // ../packages/crdt/dist/index.js
1867
+ function compareClocks(a = {}, b = {}) {
1868
+ const devices = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
1869
+ let hasGreater = false;
1870
+ let hasLess = false;
1871
+ for (const device of devices) {
1872
+ const valA = a[device] ?? 0;
1873
+ const valB = b[device] ?? 0;
1874
+ if (valA > valB)
1875
+ hasGreater = true;
1876
+ if (valA < valB)
1877
+ hasLess = true;
1878
+ }
1879
+ if (hasGreater && hasLess)
1880
+ return "concurrent";
1881
+ if (hasGreater)
1882
+ return "greater";
1883
+ if (hasLess)
1884
+ return "less";
1885
+ return "equal";
1886
+ }
1887
+ function mergeClocks(a = {}, b = {}) {
1888
+ const result = { ...a };
1889
+ for (const [device, val] of Object.entries(b)) {
1890
+ result[device] = Math.max(result[device] ?? 0, val);
1891
+ }
1892
+ return result;
1893
+ }
1894
+ function mergeSingleTask(local, remote) {
1895
+ const comp = compareClocks(local.vector_clock, remote.vector_clock);
1896
+ const mergedClock = mergeClocks(local.vector_clock, remote.vector_clock);
1897
+ let winner;
1898
+ if (comp === "greater") {
1899
+ winner = local;
1900
+ } else if (comp === "less") {
1901
+ winner = remote;
1902
+ } else {
1903
+ const localTime = new Date(local.updated_at).getTime() || 0;
1904
+ const remoteTime = new Date(remote.updated_at).getTime() || 0;
1905
+ if (localTime > remoteTime) {
1906
+ winner = local;
1907
+ } else if (remoteTime > localTime) {
1908
+ winner = remote;
1909
+ } else {
1910
+ winner = (local.device_id || "") >= (remote.device_id || "") ? local : remote;
1911
+ }
1912
+ }
1913
+ return {
1914
+ ...winner,
1915
+ vector_clock: mergedClock
1916
+ };
1917
+ }
1918
+ function mergeTasks(localTasks, remoteTasks, _currentDeviceId) {
1919
+ const map = /* @__PURE__ */ new Map();
1920
+ for (const task of localTasks) {
1921
+ if (task && task.id) {
1922
+ map.set(task.id, task);
1923
+ }
1924
+ }
1925
+ for (const remote of remoteTasks) {
1926
+ if (!remote || !remote.id)
1927
+ continue;
1928
+ const existing = map.get(remote.id);
1929
+ if (!existing) {
1930
+ map.set(remote.id, remote);
1931
+ } else {
1932
+ map.set(remote.id, mergeSingleTask(existing, remote));
1933
+ }
1934
+ }
1935
+ const merged = [];
1936
+ for (const task of map.values()) {
1937
+ if (!task.deleted) {
1938
+ merged.push(task);
1939
+ }
1940
+ }
1941
+ return merged.sort((a, b) => {
1942
+ const timeA = new Date(a.updated_at).getTime() || 0;
1943
+ const timeB = new Date(b.updated_at).getTime() || 0;
1944
+ if (timeB !== timeA)
1945
+ return timeB - timeA;
1946
+ return a.id.localeCompare(b.id);
1947
+ });
1948
+ }
1949
+
1950
+ // src/components/InteractiveDashboard.tsx
1951
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1952
+ function InteractiveDashboard() {
1953
+ const { exit } = useApp5();
1954
+ const [tasks, setTasks] = useState7([]);
1955
+ const [selectedIndex, setSelectedIndex] = useState7(0);
1956
+ const [filterStatus, setFilterStatus] = useState7("all");
1957
+ const [searchQuery, setSearchQuery] = useState7("");
1958
+ const [mode, setMode] = useState7("view");
1959
+ const [addDescription, setAddDescription] = useState7("");
1960
+ const [addPriority, setAddPriority] = useState7("medium");
1961
+ const [deleteTarget, setDeleteTarget] = useState7(null);
1962
+ const [notice, setNotice] = useState7(null);
1963
+ const [syncStatus, setSyncStatus] = useState7("idle");
1964
+ const [syncDetail, setSyncDetail] = useState7("");
1965
+ const [termDimensions, setTermDimensions] = useState7(() => ({
1966
+ columns: process.stdout?.columns || 80,
1967
+ rows: process.stdout?.rows || 24
1968
+ }));
1969
+ useEffect7(() => {
1970
+ const onResize = () => {
1971
+ setTermDimensions({
1972
+ columns: process.stdout?.columns || 80,
1973
+ rows: process.stdout?.rows || 24
1974
+ });
1975
+ };
1976
+ process.stdout?.on("resize", onResize);
1977
+ return () => {
1978
+ process.stdout?.off("resize", onResize);
1979
+ };
1980
+ }, []);
1981
+ const reloadTasks = useCallback(() => {
1982
+ try {
1983
+ const stored = getAllTasks();
1984
+ setTasks(stored);
1985
+ } catch {
1986
+ setTasks([]);
1987
+ }
1988
+ }, []);
1989
+ useEffect7(() => {
1990
+ reloadTasks();
1991
+ }, [reloadTasks]);
1992
+ const flashNotice = (message, color = "cyan", duration = 2500) => {
1993
+ setNotice({ message, color });
1994
+ const timer = setTimeout(() => {
1995
+ setNotice(null);
1996
+ }, duration);
1997
+ return () => clearTimeout(timer);
1998
+ };
1999
+ const filteredTasks = useMemo(() => {
2000
+ let result = [...tasks];
2001
+ if (filterStatus !== "all") {
2002
+ result = result.filter((t) => t.status === filterStatus);
2003
+ }
2004
+ if (searchQuery.trim()) {
2005
+ const q = searchQuery.toLowerCase();
2006
+ result = result.filter(
2007
+ (t) => t.description.toLowerCase().includes(q) || t.tags.some((tag) => tag.toLowerCase().includes(q)) || t.id.toLowerCase().startsWith(q)
2008
+ );
2009
+ }
2010
+ result.sort((a, b) => {
2011
+ const pOrder = { high: 0, medium: 1, low: 2 };
2012
+ const diff = pOrder[a.priority] - pOrder[b.priority];
2013
+ if (diff !== 0) return diff;
2014
+ return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
2015
+ });
2016
+ return result;
2017
+ }, [tasks, filterStatus, searchQuery]);
2018
+ useEffect7(() => {
2019
+ if (filteredTasks.length === 0) {
2020
+ setSelectedIndex(0);
2021
+ } else if (selectedIndex >= filteredTasks.length) {
2022
+ setSelectedIndex(Math.max(0, filteredTasks.length - 1));
2023
+ }
2024
+ }, [filteredTasks.length, selectedIndex]);
2025
+ const triggerSync = async () => {
2026
+ const auth = readAuth();
2027
+ if (!auth || !auth.apiUrl || !auth.accessToken) {
2028
+ flashNotice('Offline mode (no active cloud session). Run "ziphro login"', "yellow");
2029
+ return;
2030
+ }
2031
+ const syncPassword = auth.password;
2032
+ if (!syncPassword) {
2033
+ flashNotice('Sync password missing. Run "ziphro sync --password <pwd>"', "yellow");
2034
+ return;
2035
+ }
2036
+ setSyncStatus("syncing");
2037
+ setSyncDetail("Contacting sync server\u2026");
2038
+ try {
2039
+ let currentRevision = 1;
2040
+ let currentLocalTasks = getAllTasks();
2041
+ const downloadRes = await apiDownloadSync(auth.apiUrl, auth.accessToken);
2042
+ if (downloadRes.success && downloadRes.data?.encrypted_blob) {
2043
+ try {
2044
+ currentRevision = downloadRes.data.revision || 1;
2045
+ const decrypted = await decrypt(downloadRes.data.encrypted_blob, syncPassword, {
2046
+ userId: auth.email,
2047
+ revision: currentRevision,
2048
+ version: 2
2049
+ });
2050
+ const remoteBundle = JSON.parse(decrypted);
2051
+ if (remoteBundle.tasks && Array.isArray(remoteBundle.tasks)) {
2052
+ currentLocalTasks = mergeTasks(currentLocalTasks, remoteBundle.tasks, auth.deviceId);
2053
+ replaceTasks(currentLocalTasks);
2054
+ reloadTasks();
2055
+ }
2056
+ } catch {
2057
+ }
2058
+ }
2059
+ const nextRevision = currentRevision + 1;
2060
+ const bundle = {
2061
+ tasks: currentLocalTasks,
2062
+ schema_version: "2",
2063
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
2064
+ };
2065
+ const plaintext = JSON.stringify(bundle);
2066
+ const blobChecksum = await checksum(plaintext);
2067
+ const blob = await encrypt(plaintext, syncPassword, {
2068
+ userId: auth.email,
2069
+ revision: nextRevision,
2070
+ version: 2
2071
+ });
2072
+ const uploadRes = await apiUploadSync(
2073
+ auth.apiUrl,
2074
+ auth.accessToken,
2075
+ blob,
2076
+ blobChecksum,
2077
+ auth.deviceId,
2078
+ currentRevision
2079
+ );
2080
+ if (uploadRes.success) {
2081
+ setSyncStatus("synced");
2082
+ setSyncDetail(`Rev ${nextRevision}`);
2083
+ flashNotice(`\u2726 Cloud synced \xB7 Revision ${nextRevision}`, "green");
2084
+ } else {
2085
+ setSyncStatus("error");
2086
+ setSyncDetail(uploadRes.error?.message ?? "Sync failed");
2087
+ flashNotice(`Sync warning: ${uploadRes.error?.message ?? "Upload failed"}`, "yellow");
2088
+ }
2089
+ } catch (err) {
2090
+ setSyncStatus("error");
2091
+ flashNotice(`Sync error: ${err.message}`, "red");
2092
+ }
2093
+ };
2094
+ useInput3((input, key) => {
2095
+ if (mode === "view") {
2096
+ if (key.ctrl && input === "c") {
2097
+ exit();
2098
+ return;
2099
+ }
2100
+ if (input === "q" || key.escape) {
2101
+ exit();
2102
+ return;
2103
+ }
2104
+ if (key.upArrow || input === "k") {
2105
+ if (filteredTasks.length > 0) {
2106
+ setSelectedIndex((prev) => prev > 0 ? prev - 1 : filteredTasks.length - 1);
2107
+ }
2108
+ return;
2109
+ }
2110
+ if (key.downArrow || input === "j") {
2111
+ if (filteredTasks.length > 0) {
2112
+ setSelectedIndex((prev) => prev < filteredTasks.length - 1 ? prev + 1 : 0);
2113
+ }
2114
+ return;
2115
+ }
2116
+ if (input === " " || key.return) {
2117
+ const selected = filteredTasks[selectedIndex];
2118
+ if (!selected) return;
2119
+ const cycle = {
2120
+ pending: "in_progress",
2121
+ in_progress: "completed",
2122
+ completed: "pending"
2123
+ };
2124
+ const nextStatus = cycle[selected.status] || "pending";
2125
+ const updates = {
2126
+ status: nextStatus,
2127
+ completed_at: nextStatus === "completed" ? (/* @__PURE__ */ new Date()).toISOString() : void 0
2128
+ };
2129
+ updateTask(selected.id, updates);
2130
+ reloadTasks();
2131
+ flashNotice(`\u2713 Status updated to ${nextStatus.toUpperCase()}`, "green");
2132
+ return;
2133
+ }
2134
+ if (input === "a") {
2135
+ setAddDescription("");
2136
+ setAddPriority("medium");
2137
+ setMode("add");
2138
+ return;
2139
+ }
2140
+ if (input === "d" || input === "x") {
2141
+ const selected = filteredTasks[selectedIndex];
2142
+ if (selected) {
2143
+ setDeleteTarget(selected);
2144
+ setMode("confirm_delete");
2145
+ }
2146
+ return;
2147
+ }
2148
+ if (input === "p") {
2149
+ const selected = filteredTasks[selectedIndex];
2150
+ if (!selected) return;
2151
+ const pCycle = {
2152
+ low: "medium",
2153
+ medium: "high",
2154
+ high: "low"
2155
+ };
2156
+ const nextP = pCycle[selected.priority] || "medium";
2157
+ updateTask(selected.id, { priority: nextP });
2158
+ reloadTasks();
2159
+ flashNotice(`Priority set to ${nextP.toUpperCase()}`, "yellow");
2160
+ return;
2161
+ }
2162
+ if (input === "f") {
2163
+ const fCycle = {
2164
+ all: "pending",
2165
+ pending: "in_progress",
2166
+ in_progress: "completed",
2167
+ completed: "all"
2168
+ };
2169
+ const nextFilter = fCycle[filterStatus] || "all";
2170
+ setFilterStatus(nextFilter);
2171
+ setSelectedIndex(0);
2172
+ flashNotice(`Filter: ${String(nextFilter).toUpperCase()}`, "cyan");
2173
+ return;
2174
+ }
2175
+ if (input === "/") {
2176
+ setMode("search");
2177
+ return;
2178
+ }
2179
+ if (input === "s") {
2180
+ triggerSync();
2181
+ return;
2182
+ }
2183
+ if (input === "c" && searchQuery) {
2184
+ setSearchQuery("");
2185
+ flashNotice("Search query cleared", "cyan");
2186
+ return;
2187
+ }
2188
+ }
2189
+ if (mode === "add") {
2190
+ if (key.escape) {
2191
+ setMode("view");
2192
+ flashNotice("Add task cancelled", "gray");
2193
+ return;
2194
+ }
2195
+ if (key.tab) {
2196
+ const pCycle = {
2197
+ low: "medium",
2198
+ medium: "high",
2199
+ high: "low"
2200
+ };
2201
+ setAddPriority((prev) => pCycle[prev] || "medium");
2202
+ return;
2203
+ }
2204
+ if (key.return) {
2205
+ if (!addDescription.trim()) {
2206
+ setMode("view");
2207
+ return;
2208
+ }
2209
+ const auth = readAuth();
2210
+ const deviceId = auth?.deviceId || "cli-local";
2211
+ const newTask = {
2212
+ id: uuidv43(),
2213
+ description: addDescription.trim(),
2214
+ priority: addPriority,
2215
+ status: "pending",
2216
+ tags: [],
2217
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
2218
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
2219
+ device_id: deviceId,
2220
+ vector_clock: { [deviceId]: 1 }
2221
+ };
2222
+ insertTask(newTask);
2223
+ reloadTasks();
2224
+ setMode("view");
2225
+ flashNotice(`\u2726 Task created: "${newTask.description.slice(0, 24)}"`, "green");
2226
+ return;
2227
+ }
2228
+ if (key.backspace || key.delete) {
2229
+ setAddDescription((prev) => prev.slice(0, -1));
2230
+ return;
2231
+ }
2232
+ if (input && !key.ctrl && !key.meta) {
2233
+ setAddDescription((prev) => prev + input);
2234
+ }
2235
+ }
2236
+ if (mode === "confirm_delete") {
2237
+ if (input === "y" || input === "Y") {
2238
+ if (deleteTarget) {
2239
+ deleteTask(deleteTarget.id);
2240
+ reloadTasks();
2241
+ flashNotice(`\u2713 Task "${deleteTarget.description.slice(0, 20)}" deleted`, "green");
2242
+ }
2243
+ setDeleteTarget(null);
2244
+ setMode("view");
2245
+ return;
2246
+ }
2247
+ if (input === "n" || input === "N" || key.escape) {
2248
+ setDeleteTarget(null);
2249
+ setMode("view");
2250
+ flashNotice("Deletion cancelled", "gray");
2251
+ return;
2252
+ }
2253
+ }
2254
+ if (mode === "search") {
2255
+ if (key.escape || key.return) {
2256
+ setMode("view");
2257
+ return;
2258
+ }
2259
+ if (key.backspace || key.delete) {
2260
+ setSearchQuery((prev) => prev.slice(0, -1));
2261
+ return;
2262
+ }
2263
+ if (input && !key.ctrl && !key.meta) {
2264
+ setSearchQuery((prev) => prev + input);
2265
+ }
2266
+ }
2267
+ }, { isActive: Boolean(process.stdin?.isTTY && typeof process.stdin?.setRawMode === "function") });
2268
+ const totalCount = tasks.length;
2269
+ const pendingCount = tasks.filter((t) => t.status === "pending").length;
2270
+ const inProgressCount = tasks.filter((t) => t.status === "in_progress").length;
2271
+ const completedCount = tasks.filter((t) => t.status === "completed").length;
2272
+ const overdueCount = tasks.filter(isOverdue).length;
2273
+ const completionRate = totalCount > 0 ? Math.round(completedCount / totalCount * 100) : 0;
2274
+ const authInfo = readAuth();
2275
+ const userAccount = authInfo?.email ? authInfo.email.split("@")[0] : "local";
2276
+ return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
2277
+ /* @__PURE__ */ jsxs10(Box10, { justifyContent: "space-between", alignItems: "center", marginBottom: 0, children: [
2278
+ /* @__PURE__ */ jsx10(MiniHeader, {}),
2279
+ /* @__PURE__ */ jsxs10(Box10, { gap: 1, children: [
2280
+ /* @__PURE__ */ jsxs10(Box10, { borderStyle: "round", borderColor: "#3a3a3c", paddingX: 1, children: [
2281
+ /* @__PURE__ */ jsx10(Text10, { color: "#f54e00", bold: true, children: "E2E " }),
2282
+ /* @__PURE__ */ jsx10(Text10, { color: "#f5f4ef", children: "AES-256-GCM" })
2283
+ ] }),
2284
+ /* @__PURE__ */ jsxs10(
2285
+ Box10,
2286
+ {
2287
+ borderStyle: "round",
2288
+ borderColor: syncStatus === "synced" ? "#22c55e" : syncStatus === "error" ? "#ef4444" : "#3a3a3c",
2289
+ paddingX: 1,
2290
+ children: [
2291
+ /* @__PURE__ */ jsx10(
2292
+ Text10,
2293
+ {
2294
+ color: syncStatus === "synced" ? "#22c55e" : syncStatus === "syncing" ? "#06b6d4" : syncStatus === "error" ? "#ef4444" : "#73726c",
2295
+ bold: true,
2296
+ children: syncStatus === "syncing" ? "\u27F3 Syncing" : syncStatus === "synced" ? "\u2713 Synced" : syncStatus === "error" ? "\u26A0 Error" : "\u25CF Ready"
2297
+ }
2298
+ ),
2299
+ syncDetail && /* @__PURE__ */ jsxs10(Text10, { color: "#73726c", children: [
2300
+ " (",
2301
+ syncDetail,
2302
+ ")"
2303
+ ] })
2304
+ ]
2305
+ }
2306
+ )
2307
+ ] })
2308
+ ] }),
2309
+ /* @__PURE__ */ jsxs10(
2310
+ Box10,
2311
+ {
2312
+ borderStyle: "round",
2313
+ borderColor: "#3a3a3c",
2314
+ paddingX: 1,
2315
+ paddingY: 0,
2316
+ marginBottom: 1,
2317
+ justifyContent: "space-around",
2318
+ children: [
2319
+ /* @__PURE__ */ jsxs10(Box10, { children: [
2320
+ /* @__PURE__ */ jsx10(Text10, { color: "#73726c", children: "USER: " }),
2321
+ /* @__PURE__ */ jsx10(Text10, { color: "#f5f4ef", bold: true, children: userAccount })
2322
+ ] }),
2323
+ /* @__PURE__ */ jsx10(Text10, { color: "#3a3a3c", children: "\u2502" }),
2324
+ /* @__PURE__ */ jsxs10(Box10, { children: [
2325
+ /* @__PURE__ */ jsx10(Text10, { color: "#73726c", children: "TOTAL: " }),
2326
+ /* @__PURE__ */ jsx10(Text10, { color: "#f5f4ef", bold: true, children: totalCount })
2327
+ ] }),
2328
+ /* @__PURE__ */ jsx10(Text10, { color: "#3a3a3c", children: "\u2502" }),
2329
+ /* @__PURE__ */ jsxs10(Box10, { children: [
2330
+ /* @__PURE__ */ jsx10(Text10, { color: "#73726c", children: "PENDING: " }),
2331
+ /* @__PURE__ */ jsx10(Text10, { color: "#94a3b8", bold: true, children: pendingCount })
2332
+ ] }),
2333
+ /* @__PURE__ */ jsx10(Text10, { color: "#3a3a3c", children: "\u2502" }),
2334
+ /* @__PURE__ */ jsxs10(Box10, { children: [
2335
+ /* @__PURE__ */ jsx10(Text10, { color: "#73726c", children: "ACTIVE: " }),
2336
+ /* @__PURE__ */ jsx10(Text10, { color: "#06b6d4", bold: true, children: inProgressCount })
2337
+ ] }),
2338
+ /* @__PURE__ */ jsx10(Text10, { color: "#3a3a3c", children: "\u2502" }),
2339
+ /* @__PURE__ */ jsxs10(Box10, { children: [
2340
+ /* @__PURE__ */ jsx10(Text10, { color: "#73726c", children: "DONE: " }),
2341
+ /* @__PURE__ */ jsxs10(Text10, { color: "#22c55e", bold: true, children: [
2342
+ completedCount,
2343
+ " (",
2344
+ completionRate,
2345
+ "%)"
2346
+ ] })
2347
+ ] }),
2348
+ /* @__PURE__ */ jsx10(Text10, { color: "#3a3a3c", children: "\u2502" }),
2349
+ /* @__PURE__ */ jsxs10(Box10, { children: [
2350
+ /* @__PURE__ */ jsx10(Text10, { color: "#73726c", children: "OVERDUE: " }),
2351
+ /* @__PURE__ */ jsx10(Text10, { color: overdueCount > 0 ? "#ef4444" : "#73726c", bold: true, children: overdueCount })
2352
+ ] })
2353
+ ]
2354
+ }
2355
+ ),
2356
+ /* @__PURE__ */ jsxs10(Box10, { justifyContent: "space-between", marginBottom: 0, paddingX: 1, children: [
2357
+ /* @__PURE__ */ jsxs10(Box10, { gap: 1, children: [
2358
+ /* @__PURE__ */ jsx10(Text10, { color: "#73726c", children: "Filter: " }),
2359
+ /* @__PURE__ */ jsxs10(Text10, { color: filterStatus === "all" ? "#f54e00" : "#06b6d4", bold: true, children: [
2360
+ "[",
2361
+ String(filterStatus).toUpperCase(),
2362
+ "]"
2363
+ ] }),
2364
+ searchQuery && /* @__PURE__ */ jsxs10(Fragment3, { children: [
2365
+ /* @__PURE__ */ jsx10(Text10, { color: "#73726c", children: " | Search: " }),
2366
+ /* @__PURE__ */ jsxs10(Text10, { color: "#eab308", children: [
2367
+ '"',
2368
+ searchQuery,
2369
+ '"'
2370
+ ] }),
2371
+ /* @__PURE__ */ jsx10(Text10, { color: "#73726c", children: " (press 'c' to clear)" })
2372
+ ] })
2373
+ ] }),
2374
+ notice && /* @__PURE__ */ jsx10(Box10, { children: /* @__PURE__ */ jsx10(Text10, { color: notice.color, bold: true, children: notice.message }) })
2375
+ ] }),
2376
+ mode === "add" && /* @__PURE__ */ jsxs10(
2377
+ Box10,
2378
+ {
2379
+ flexDirection: "column",
2380
+ borderStyle: "round",
2381
+ borderColor: BRAND.orange,
2382
+ paddingX: 2,
2383
+ paddingY: 1,
2384
+ marginBottom: 1,
2385
+ children: [
2386
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.orange, bold: true, children: "\u2726 New Task Entry" }),
2387
+ /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, children: [
2388
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.muted, children: "Description: " }),
2389
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.white, bold: true, children: addDescription }),
2390
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.orange, children: "\u2588" })
2391
+ ] }),
2392
+ /* @__PURE__ */ jsxs10(Box10, { marginTop: 0, children: [
2393
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.muted, children: "Priority: " }),
2394
+ /* @__PURE__ */ jsxs10(
2395
+ Text10,
2396
+ {
2397
+ color: addPriority === "high" ? BRAND.error : addPriority === "medium" ? BRAND.warning : BRAND.success,
2398
+ bold: true,
2399
+ children: [
2400
+ "[",
2401
+ addPriority.toUpperCase(),
2402
+ "]"
2403
+ ]
2404
+ }
2405
+ ),
2406
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.muted, children: " (Press [Tab] to cycle priority)" })
2407
+ ] }),
2408
+ /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, children: [
2409
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.muted, children: "Press " }),
2410
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.success, bold: true, children: "[Enter]" }),
2411
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.muted, children: " to save \xB7 " }),
2412
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.error, bold: true, children: "[Esc]" }),
2413
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.muted, children: " to cancel" })
2414
+ ] })
2415
+ ]
2416
+ }
2417
+ ),
2418
+ mode === "confirm_delete" && deleteTarget && /* @__PURE__ */ jsxs10(
2419
+ Box10,
2420
+ {
2421
+ flexDirection: "column",
2422
+ borderStyle: "round",
2423
+ borderColor: BRAND.error,
2424
+ paddingX: 2,
2425
+ paddingY: 1,
2426
+ marginBottom: 1,
2427
+ children: [
2428
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.error, bold: true, children: "\u26A0 Confirm Delete" }),
2429
+ /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, children: [
2430
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.muted, children: "Are you sure you want to delete task: " }),
2431
+ /* @__PURE__ */ jsxs10(Text10, { color: BRAND.white, bold: true, children: [
2432
+ '"',
2433
+ deleteTarget.description,
2434
+ '"'
2435
+ ] }),
2436
+ /* @__PURE__ */ jsxs10(Text10, { color: BRAND.muted, children: [
2437
+ " (",
2438
+ deleteTarget.id.slice(0, 8),
2439
+ ")?"
2440
+ ] })
2441
+ ] }),
2442
+ /* @__PURE__ */ jsxs10(Box10, { marginTop: 1, children: [
2443
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.success, bold: true, children: "[y]" }),
2444
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.white, children: " Yes, delete \xB7 " }),
2445
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.error, bold: true, children: "[n / Esc]" }),
2446
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.white, children: " Cancel" })
2447
+ ] })
2448
+ ]
2449
+ }
2450
+ ),
2451
+ mode === "search" && /* @__PURE__ */ jsxs10(
2452
+ Box10,
2453
+ {
2454
+ flexDirection: "column",
2455
+ borderStyle: "round",
2456
+ borderColor: BRAND.warning,
2457
+ paddingX: 2,
2458
+ paddingY: 0,
2459
+ marginBottom: 1,
2460
+ children: [
2461
+ /* @__PURE__ */ jsxs10(Box10, { children: [
2462
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.warning, bold: true, children: "Search Tasks: " }),
2463
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.white, children: searchQuery }),
2464
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.warning, children: "\u2588" })
2465
+ ] }),
2466
+ /* @__PURE__ */ jsx10(Text10, { color: BRAND.muted, children: "Press [Enter] or [Esc] to commit filter" })
2467
+ ]
2468
+ }
2469
+ ),
2470
+ /* @__PURE__ */ jsx10(
2471
+ TaskTable,
2472
+ {
2473
+ tasks: filteredTasks,
2474
+ selectedIndex: mode === "view" ? selectedIndex : void 0,
2475
+ terminalWidth: termDimensions.columns,
2476
+ maxRows: Math.max(5, termDimensions.rows - 18)
2477
+ }
2478
+ ),
2479
+ /* @__PURE__ */ jsxs10(
2480
+ Box10,
2481
+ {
2482
+ marginTop: 1,
2483
+ borderStyle: "round",
2484
+ borderColor: "#3a3a3c",
2485
+ paddingX: 1,
2486
+ justifyContent: "space-between",
2487
+ children: [
2488
+ /* @__PURE__ */ jsxs10(Text10, { color: "#73726c", children: [
2489
+ /* @__PURE__ */ jsx10(Text10, { color: "#f54e00", children: "[\u2191\u2193/jk]" }),
2490
+ " Move \xB7",
2491
+ " ",
2492
+ /* @__PURE__ */ jsx10(Text10, { color: "#f54e00", children: "[Space]" }),
2493
+ " Toggle \xB7",
2494
+ " ",
2495
+ /* @__PURE__ */ jsx10(Text10, { color: "#f54e00", children: "[a]" }),
2496
+ " Add \xB7",
2497
+ " ",
2498
+ /* @__PURE__ */ jsx10(Text10, { color: "#f54e00", children: "[d]" }),
2499
+ " Del \xB7",
2500
+ " ",
2501
+ /* @__PURE__ */ jsx10(Text10, { color: "#f54e00", children: "[p]" }),
2502
+ " Priority \xB7",
2503
+ " ",
2504
+ /* @__PURE__ */ jsx10(Text10, { color: "#f54e00", children: "[f]" }),
2505
+ " Filter \xB7",
2506
+ " ",
2507
+ /* @__PURE__ */ jsx10(Text10, { color: "#f54e00", children: "[/]" }),
2508
+ " Search \xB7",
2509
+ " ",
2510
+ /* @__PURE__ */ jsx10(Text10, { color: "#f54e00", children: "[s]" }),
2511
+ " Sync \xB7",
2512
+ " ",
2513
+ /* @__PURE__ */ jsx10(Text10, { color: "#f54e00", children: "[q]" }),
2514
+ " Quit"
2515
+ ] }),
2516
+ /* @__PURE__ */ jsxs10(Text10, { color: "#73726c", children: [
2517
+ "Ziphro v",
2518
+ CLI_VERSION
2519
+ ] })
2520
+ ]
2521
+ }
2522
+ )
2523
+ ] });
2524
+ }
2525
+
2526
+ // src/commands/list.tsx
2527
+ import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
2528
+ function ListUI({ filter, format }) {
2529
+ const { exit } = useApp6();
2530
+ const [tasks] = useState8(() => {
2531
+ const results = filterTasks(filter);
2532
+ results.sort((a, b) => {
2533
+ const pOrder = { high: 0, medium: 1, low: 2 };
2534
+ const pDiff = pOrder[a.priority] - pOrder[b.priority];
2535
+ if (pDiff !== 0) return pDiff;
2536
+ return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
2537
+ });
2538
+ return results;
2539
+ });
2540
+ useEffect8(() => {
2541
+ if (format === "json") {
2542
+ console.log(JSON.stringify(tasks, null, 2));
2543
+ exit();
2544
+ } else {
2545
+ const t = setTimeout(() => exit(), 50);
2546
+ return () => clearTimeout(t);
2547
+ }
2548
+ }, [exit, format, tasks]);
2549
+ if (format === "json") {
2550
+ return /* @__PURE__ */ jsx11(Box11, {});
2551
+ }
2552
+ return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
2553
+ /* @__PURE__ */ jsx11(MiniHeader, {}),
2554
+ (filter.status || filter.priority || filter.tags?.length) && /* @__PURE__ */ jsxs11(Box11, { marginBottom: 1, gap: 1, children: [
2555
+ /* @__PURE__ */ jsx11(Text11, { color: BRAND.muted, children: "Active Filters:" }),
2556
+ filter.status && /* @__PURE__ */ jsxs11(Text11, { color: BRAND.orange, bold: true, children: [
2557
+ "status=",
2558
+ filter.status
2559
+ ] }),
2560
+ filter.priority && /* @__PURE__ */ jsxs11(
2561
+ Text11,
2562
+ {
2563
+ color: filter.priority === "high" ? BRAND.error : filter.priority === "medium" ? BRAND.warning : BRAND.success,
2564
+ bold: true,
2565
+ children: [
2566
+ "priority=",
2567
+ filter.priority
2568
+ ]
2569
+ }
2570
+ ),
2571
+ filter.tags?.map((t) => /* @__PURE__ */ jsxs11(Text11, { color: BRAND.tag, children: [
2572
+ "#",
2573
+ t
2574
+ ] }, t))
2575
+ ] }),
2576
+ format === "table" && /* @__PURE__ */ jsx11(TaskTable, { tasks, maxRows: 100 }),
2577
+ format === "compact" && /* @__PURE__ */ jsx11(TaskCompact, { tasks })
2578
+ ] });
2579
+ }
2580
+ function registerList(program2) {
2581
+ program2.command("list").alias("-l").description("List tasks with optional filters or interactive mode").option("-s, --status <status>", "Filter by status: pending | in_progress | completed | all", "all").option("-p, --priority <priority>", "Filter by priority: high | medium | low | all", "all").option("-t, --tags <tags>", "Filter by comma-separated tags").option("-f, --format <format>", "Output format: table | compact | json", "table").option("-i, --interactive", "Open in interactive TUI mode with keyboard navigation", false).action(async (options) => {
2582
+ if (options.interactive) {
2583
+ render(React9.createElement(InteractiveDashboard));
2584
+ return;
2585
+ }
2586
+ const filter = {};
2587
+ if (options.status && options.status !== "all") {
2588
+ filter.status = options.status;
2589
+ }
2590
+ if (options.priority && options.priority !== "all") {
2591
+ filter.priority = options.priority;
2592
+ }
2593
+ if (options.tags) {
2594
+ filter.tags = options.tags.split(",").map((t) => t.trim());
2595
+ }
2596
+ const format = ["table", "compact", "json"].includes(options.format || "table") ? options.format : "table";
2597
+ render(React9.createElement(ListUI, { filter, format }));
2598
+ });
2599
+ }
2600
+
2601
+ // src/commands/done.tsx
2602
+ import React10, { useEffect as useEffect9, useState as useState9 } from "react";
2603
+ import { Box as Box12, Text as Text12, useApp as useApp7 } from "ink";
2604
+ import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
2605
+ function DoneUI({ id, note }) {
2606
+ const { exit } = useApp7();
2607
+ const [status, setStatus] = useState9("working");
2608
+ const [syncStatus, setSyncStatus] = useState9("idle");
2609
+ const [syncMessage, setSyncMessage] = useState9("");
2610
+ const [message, setMessage] = useState9("");
2611
+ const [description, setDescription] = useState9("");
2612
+ useEffect9(() => {
2613
+ async function completeTaskAndSync() {
2614
+ try {
2615
+ const auth = requireAuth();
2616
+ const task = getTask(id);
2617
+ if (!task) {
2618
+ setMessage(`No task found with ID starting with "${id}"`);
2619
+ setStatus("error");
2620
+ setTimeout(() => exit(new Error("Task not found")), 1200);
2621
+ return;
2622
+ }
2623
+ if (task.status === "completed") {
2624
+ setMessage(`Task is already marked as completed.`);
2625
+ setStatus("error");
2626
+ setTimeout(() => exit(), 1200);
2627
+ return;
2628
+ }
2629
+ const updated = updateTask(task.id, {
2630
+ status: "completed",
2631
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
2632
+ completion_note: note
2633
+ });
2634
+ if (updated) {
2635
+ setDescription(task.description);
2636
+ setMessage(`Task marked complete!`);
2637
+ setStatus("success");
2638
+ setSyncStatus("syncing");
2639
+ const tasks = getAllTasks();
2640
+ const bundle = {
2641
+ tasks,
2642
+ schema_version: "1",
2643
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
2644
+ };
2645
+ const plaintext = JSON.stringify(bundle);
2646
+ const blobChecksum = await checksum(plaintext);
2647
+ const syncPassword = auth.password;
2648
+ if (!syncPassword) {
2649
+ setSyncStatus("failed");
2650
+ setSyncMessage('Encryption password required. Run "ziphro sync --password <pwd>".');
2651
+ setTimeout(() => exit(), 3e3);
2652
+ return;
2653
+ }
2654
+ const blob = encrypt(plaintext, syncPassword);
2655
+ const res = await apiUploadSync(
2656
+ auth.apiUrl,
2657
+ auth.accessToken,
2658
+ blob,
2659
+ blobChecksum,
2660
+ auth.deviceId
2661
+ );
2662
+ if (res.success) {
2663
+ clearSyncQueue();
2664
+ setSyncStatus("synced");
2665
+ setTimeout(() => exit(), 2e3);
2666
+ } else if (res.error?.code === "NETWORK_ERROR") {
2667
+ setSyncStatus("offline");
2668
+ setTimeout(() => exit(), 2500);
2669
+ } else {
2670
+ setSyncStatus("failed");
2671
+ setSyncMessage(res.error?.message ?? "Sync failed");
2672
+ setTimeout(() => exit(), 3500);
2673
+ }
2674
+ }
2675
+ } catch (e) {
2676
+ setMessage(e.message);
2677
+ setStatus("error");
2678
+ setTimeout(() => exit(e), 2e3);
2679
+ }
2680
+ }
2681
+ completeTaskAndSync();
2682
+ }, []);
2683
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
2684
+ /* @__PURE__ */ jsx12(MiniHeader, {}),
2685
+ status === "working" && /* @__PURE__ */ jsx12(Spinner, { label: "Updating task\u2026", type: "star", color: BRAND.orange }),
2686
+ status === "success" && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginTop: 1, children: [
2687
+ /* @__PURE__ */ jsx12(StarBurst, { label: message, color: BRAND.success }),
2688
+ /* @__PURE__ */ jsxs12(Box12, { marginTop: 1, flexDirection: "column", children: [
2689
+ description && /* @__PURE__ */ jsxs12(Box12, { children: [
2690
+ /* @__PURE__ */ jsx12(Text12, { color: BRAND.muted, children: " Task: " }),
2691
+ /* @__PURE__ */ jsx12(Text12, { color: BRAND.white, bold: true, children: description })
2692
+ ] }),
2693
+ note && /* @__PURE__ */ jsxs12(Box12, { children: [
2694
+ /* @__PURE__ */ jsx12(Text12, { color: BRAND.muted, children: " Note: " }),
2695
+ /* @__PURE__ */ jsx12(Text12, { color: BRAND.orange, children: note })
2696
+ ] }),
2697
+ /* @__PURE__ */ jsxs12(Box12, { marginTop: 1, children: [
2698
+ /* @__PURE__ */ jsx12(Text12, { color: BRAND.muted, children: " Sync: " }),
2699
+ syncStatus === "syncing" && /* @__PURE__ */ jsx12(Text12, { color: BRAND.orange, children: "Syncing to cloud (E2E encrypted)..." }),
2700
+ syncStatus === "synced" && /* @__PURE__ */ jsx12(Text12, { color: BRAND.success, children: "\u2713 Synced to cloud" }),
2701
+ syncStatus === "offline" && /* @__PURE__ */ jsx12(Text12, { color: BRAND.warning, children: "\u2601 Offline (queued locally)" }),
2702
+ syncStatus === "failed" && /* @__PURE__ */ jsxs12(Text12, { color: BRAND.error, children: [
2703
+ "\u2717 ",
2704
+ syncMessage
2705
+ ] })
2706
+ ] })
2707
+ ] })
2708
+ ] }),
2709
+ status === "error" && /* @__PURE__ */ jsx12(Box12, { marginTop: 1, children: /* @__PURE__ */ jsx12(StatusBadge, { type: "error", message }) })
2710
+ ] });
2711
+ }
2712
+ function registerDone(program2) {
2713
+ program2.command("done <id>").alias("-d").description("Mark a task as complete").option("-n, --note <note>", "Completion note").action(async (id, options) => {
2714
+ const { render: render5 } = await import("ink");
2715
+ render5(React10.createElement(DoneUI, { id, note: options.note }));
2716
+ });
2717
+ }
2718
+
2719
+ // src/commands/remove.tsx
2720
+ import React11, { useEffect as useEffect10, useState as useState10 } from "react";
2721
+ import { Box as Box13, Text as Text13, useApp as useApp8, useInput as useInput4 } from "ink";
2722
+ import { jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
2723
+ function RemoveUI({ id, force }) {
2724
+ const { exit } = useApp8();
2725
+ const [stage, setStage] = useState10("confirm");
2726
+ const [syncStatus, setSyncStatus] = useState10("idle");
2727
+ const [syncMessage, setSyncMessage] = useState10("");
2728
+ const [message, setMessage] = useState10("");
2729
+ const task = getTask(id);
2730
+ const performDeleteAndSync = async () => {
2731
+ try {
2732
+ const auth = requireAuth();
2733
+ setStage("deleting");
2734
+ const deleted = deleteTask(task.id);
2735
+ if (deleted) {
2736
+ setMessage(`Task removed: ${task.description}`);
2737
+ setSyncStatus("syncing");
2738
+ const tasks = getAllTasks();
2739
+ const bundle = {
2740
+ tasks,
2741
+ schema_version: "1",
2742
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
2743
+ };
2744
+ const plaintext = JSON.stringify(bundle);
2745
+ const blobChecksum = await checksum(plaintext);
2746
+ const syncPassword = auth.password;
2747
+ if (!syncPassword) {
2748
+ setSyncStatus("failed");
2749
+ setSyncMessage('Encryption password required. Run "ziphro sync --password <pwd>".');
2750
+ setStage("success");
2751
+ setTimeout(() => exit(), 3e3);
2752
+ return;
2753
+ }
2754
+ const blob = encrypt(plaintext, syncPassword);
2755
+ const res = await apiUploadSync(
2756
+ auth.apiUrl,
2757
+ auth.accessToken,
2758
+ blob,
2759
+ blobChecksum,
2760
+ auth.deviceId
2761
+ );
2762
+ setStage("success");
2763
+ if (res.success) {
2764
+ clearSyncQueue();
2765
+ setSyncStatus("synced");
2766
+ setTimeout(() => exit(), 2e3);
2767
+ } else if (res.error?.code === "NETWORK_ERROR") {
2768
+ setSyncStatus("offline");
2769
+ setTimeout(() => exit(), 2500);
2770
+ } else {
2771
+ setSyncStatus("failed");
2772
+ setSyncMessage(res.error?.message ?? "Sync failed");
2773
+ setTimeout(() => exit(), 3500);
2774
+ }
2775
+ } else {
2776
+ setMessage("Failed to delete task.");
2777
+ setStage("error");
2778
+ setTimeout(() => exit(new Error("Delete failed")), 1200);
2779
+ }
2780
+ } catch (e) {
2781
+ setMessage(e.message);
2782
+ setStage("error");
2783
+ setTimeout(() => exit(e), 2e3);
2784
+ }
2785
+ };
2786
+ useEffect10(() => {
2787
+ try {
2788
+ requireAuth();
2789
+ } catch (e) {
2790
+ setMessage(e.message);
2791
+ setStage("error");
2792
+ setTimeout(() => exit(e), 2e3);
2793
+ return;
2794
+ }
2795
+ if (!task) {
2796
+ setMessage(`No task found with ID starting with "${id}"`);
2797
+ setStage("error");
2798
+ setTimeout(() => exit(new Error("Task not found")), 1200);
2799
+ return;
2800
+ }
2801
+ if (force) {
2802
+ performDeleteAndSync();
2803
+ }
2804
+ }, []);
2805
+ useInput4((input) => {
2806
+ if (stage !== "confirm" || !task) return;
2807
+ if (input.toLowerCase() === "y") {
2808
+ performDeleteAndSync();
2809
+ } else if (input.toLowerCase() === "n" || input === "") {
2810
+ setStage("cancelled");
2811
+ setTimeout(() => exit(), 800);
2812
+ }
2813
+ });
2814
+ return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
2815
+ /* @__PURE__ */ jsx13(MiniHeader, {}),
2816
+ !task && stage !== "error" && /* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx13(StatusBadge, { type: "error", message: `No task found with ID "${id}"` }) }),
2817
+ task && stage === "confirm" && /* @__PURE__ */ jsxs13(
2818
+ Box13,
2819
+ {
2820
+ flexDirection: "column",
2821
+ borderStyle: "round",
2822
+ borderColor: BRAND.border,
2823
+ paddingX: 2,
2824
+ paddingY: 1,
2825
+ marginTop: 1,
2826
+ children: [
2827
+ /* @__PURE__ */ jsxs13(Box13, { marginBottom: 1, children: [
2828
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.warning, bold: true, children: "\u26A0 " }),
2829
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.white, bold: true, children: "Delete this task?" })
2830
+ ] }),
2831
+ /* @__PURE__ */ jsxs13(Box13, { marginBottom: 1, paddingX: 1, flexDirection: "column", children: [
2832
+ /* @__PURE__ */ jsxs13(Box13, { children: [
2833
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.muted, children: "ID: " }),
2834
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.orange, bold: true, children: task.id.slice(0, 8) })
2835
+ ] }),
2836
+ /* @__PURE__ */ jsxs13(Box13, { children: [
2837
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.muted, children: "Description: " }),
2838
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.white, children: task.description })
2839
+ ] }),
2840
+ /* @__PURE__ */ jsxs13(Box13, { children: [
2841
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.muted, children: "Status: " }),
2842
+ /* @__PURE__ */ jsx13(Text13, { color: task.status === "completed" ? BRAND.success : BRAND.warning, children: task.status })
2843
+ ] })
2844
+ ] }),
2845
+ /* @__PURE__ */ jsxs13(Box13, { children: [
2846
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.muted, children: "Type " }),
2847
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.success, bold: true, children: "[y]" }),
2848
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.muted, children: " to confirm or " }),
2849
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.error, bold: true, children: "[n / Esc]" }),
2850
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.muted, children: " to cancel" })
2851
+ ] })
2852
+ ]
2853
+ }
2854
+ ),
2855
+ stage === "deleting" && /* @__PURE__ */ jsx13(Box13, { marginTop: 1, children: /* @__PURE__ */ jsx13(Spinner, { label: "Removing task\u2026", type: "star", color: BRAND.orange }) }),
2856
+ stage === "success" && /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", marginTop: 1, children: [
2857
+ /* @__PURE__ */ jsx13(StarBurst, { label: message, color: BRAND.orange }),
2858
+ /* @__PURE__ */ jsx13(Box13, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsxs13(Box13, { children: [
2859
+ /* @__PURE__ */ jsx13(Text13, { color: BRAND.muted, children: " Sync: " }),
2860
+ syncStatus === "syncing" && /* @__PURE__ */ jsx13(Text13, { color: BRAND.orange, children: "Syncing to cloud (E2E encrypted)..." }),
2861
+ syncStatus === "synced" && /* @__PURE__ */ jsx13(Text13, { color: BRAND.success, children: "\u2713 Synced to cloud" }),
2862
+ syncStatus === "offline" && /* @__PURE__ */ jsx13(Text13, { color: BRAND.warning, children: "\u2601 Offline (queued locally)" }),
2863
+ syncStatus === "failed" && /* @__PURE__ */ jsxs13(Text13, { color: BRAND.error, children: [
2864
+ "\u2717 ",
2865
+ syncMessage
2866
+ ] })
2867
+ ] }) })
2868
+ ] }),
2869
+ stage === "error" && /* @__PURE__ */ jsx13(StatusBadge, { type: "error", message }),
2870
+ stage === "cancelled" && /* @__PURE__ */ jsx13(StatusBadge, { type: "info", message: "Deletion cancelled." })
2871
+ ] });
2872
+ }
2873
+ function registerRemove(program2) {
2874
+ program2.command("remove <id>").alias("-r").description("Delete a task").option("--force", "Skip confirmation prompt", false).action(async (id, options) => {
2875
+ const { render: render5 } = await import("ink");
2876
+ render5(React11.createElement(RemoveUI, { id, force: options.force }));
2877
+ });
2878
+ }
2879
+
2880
+ // src/commands/edit.tsx
2881
+ import React12, { useState as useState11, useEffect as useEffect11 } from "react";
2882
+ import { Box as Box14, Text as Text14, useApp as useApp9 } from "ink";
2883
+ import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
2884
+ function EditUI({ id, updates }) {
2885
+ const { exit } = useApp9();
2886
+ const [status, setStatus] = useState11("working");
2887
+ const [syncStatus, setSyncStatus] = useState11("idle");
2888
+ const [syncMessage, setSyncMessage] = useState11("");
2889
+ const [message, setMessage] = useState11("");
2890
+ const [changes, setChanges] = useState11([]);
2891
+ useEffect11(() => {
2892
+ async function editTaskAndSync() {
2893
+ try {
2894
+ const auth = requireAuth();
2895
+ const task = getTask(id);
2896
+ if (!task) {
2897
+ setMessage(`No task found with ID starting with "${id}"`);
2898
+ setStatus("error");
2899
+ setTimeout(() => exit(new Error("Task not found")), 1200);
2900
+ return;
2901
+ }
2902
+ const taskUpdates = {};
2903
+ const changesLog = [];
2904
+ if (updates.description) {
2905
+ taskUpdates.description = updates.description;
2906
+ changesLog.push(`description \u2192 "${updates.description}"`);
2907
+ }
2908
+ if (updates.priority) {
2909
+ taskUpdates.priority = updates.priority;
2910
+ changesLog.push(`priority \u2192 ${updates.priority}`);
2911
+ }
2912
+ if (updates.status) {
2913
+ taskUpdates.status = updates.status;
2914
+ if (updates.status === "completed") {
2915
+ taskUpdates.completed_at = (/* @__PURE__ */ new Date()).toISOString();
2916
+ }
2917
+ changesLog.push(`status \u2192 ${updates.status}`);
2918
+ }
2919
+ if (updates.due) {
2920
+ taskUpdates.due_date = updates.due;
2921
+ changesLog.push(`due date \u2192 ${updates.due}`);
2922
+ }
2923
+ if (updates.tags) {
2924
+ taskUpdates.tags = updates.tags.split(",").map((t) => t.trim());
2925
+ changesLog.push(`tags \u2192 [${taskUpdates.tags.join(", ")}]`);
2926
+ }
2927
+ if (updates.note) {
2928
+ taskUpdates.completion_note = updates.note;
2929
+ changesLog.push(`note \u2192 "${updates.note}"`);
2930
+ }
2931
+ if (changesLog.length === 0) {
2932
+ setMessage("No changes specified. Use options like --description, --priority, etc.");
2933
+ setStatus("error");
2934
+ setTimeout(() => exit(), 1200);
2935
+ return;
2936
+ }
2937
+ const updated = updateTask(task.id, taskUpdates);
2938
+ if (updated) {
2939
+ setChanges(changesLog);
2940
+ setMessage(`Task ${task.id.slice(0, 8)} updated!`);
2941
+ setStatus("success");
2942
+ setSyncStatus("syncing");
2943
+ const tasks = getAllTasks();
2944
+ const bundle = {
2945
+ tasks,
2946
+ schema_version: "1",
2947
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
2948
+ };
2949
+ const plaintext = JSON.stringify(bundle);
2950
+ const blobChecksum = await checksum(plaintext);
2951
+ const syncPassword = auth.password;
2952
+ if (!syncPassword) {
2953
+ setSyncStatus("failed");
2954
+ setSyncMessage('Encryption password required. Run "ziphro sync --password <pwd>".');
2955
+ setTimeout(() => exit(), 3e3);
2956
+ return;
2957
+ }
2958
+ const blob = encrypt(plaintext, syncPassword);
2959
+ const res = await apiUploadSync(
2960
+ auth.apiUrl,
2961
+ auth.accessToken,
2962
+ blob,
2963
+ blobChecksum,
2964
+ auth.deviceId
2965
+ );
2966
+ if (res.success) {
2967
+ clearSyncQueue();
2968
+ setSyncStatus("synced");
2969
+ setTimeout(() => exit(), 2e3);
2970
+ } else if (res.error?.code === "NETWORK_ERROR") {
2971
+ setSyncStatus("offline");
2972
+ setTimeout(() => exit(), 2500);
2973
+ } else {
2974
+ setSyncStatus("failed");
2975
+ setSyncMessage(res.error?.message ?? "Sync failed");
2976
+ setTimeout(() => exit(), 3500);
2977
+ }
2978
+ }
2979
+ } catch (e) {
2980
+ setMessage(e.message);
2981
+ setStatus("error");
2982
+ setTimeout(() => exit(e), 2e3);
2983
+ }
2984
+ }
2985
+ editTaskAndSync();
2986
+ }, []);
2987
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
2988
+ /* @__PURE__ */ jsx14(MiniHeader, {}),
2989
+ status === "working" && /* @__PURE__ */ jsx14(Spinner, { label: "Updating task\u2026", type: "star", color: BRAND.orange }),
2990
+ status === "success" && /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", marginTop: 1, children: [
2991
+ /* @__PURE__ */ jsx14(StarBurst, { label: message, color: BRAND.orange }),
2992
+ /* @__PURE__ */ jsxs14(Box14, { marginTop: 1, flexDirection: "column", children: [
2993
+ changes.map((c) => /* @__PURE__ */ jsxs14(Box14, { marginTop: 0, children: [
2994
+ /* @__PURE__ */ jsx14(Text14, { color: BRAND.muted, children: " \u21B3 " }),
2995
+ /* @__PURE__ */ jsx14(Text14, { color: BRAND.white, children: c })
2996
+ ] }, c)),
2997
+ /* @__PURE__ */ jsxs14(Box14, { marginTop: 1, children: [
2998
+ /* @__PURE__ */ jsx14(Text14, { color: BRAND.muted, children: " Sync: " }),
2999
+ syncStatus === "syncing" && /* @__PURE__ */ jsx14(Text14, { color: BRAND.orange, children: "Syncing to cloud (E2E encrypted)..." }),
3000
+ syncStatus === "synced" && /* @__PURE__ */ jsx14(Text14, { color: BRAND.success, children: "\u2713 Synced to cloud" }),
3001
+ syncStatus === "offline" && /* @__PURE__ */ jsx14(Text14, { color: BRAND.warning, children: "\u2601 Offline (queued locally)" }),
3002
+ syncStatus === "failed" && /* @__PURE__ */ jsxs14(Text14, { color: BRAND.error, children: [
3003
+ "\u2717 ",
3004
+ syncMessage
3005
+ ] })
3006
+ ] })
3007
+ ] })
3008
+ ] }),
3009
+ status === "error" && /* @__PURE__ */ jsx14(Box14, { marginTop: 1, children: /* @__PURE__ */ jsx14(StatusBadge, { type: "error", message }) })
3010
+ ] });
3011
+ }
3012
+ function registerEdit(program2) {
3013
+ program2.command("edit <id>").alias("-e").description("Update one or more fields of a task").option("--description <text>", "New description").option("--priority <level>", "New priority: high | medium | low").option("--status <status>", "New status: pending | in_progress | completed").option("--due <datetime>", "New due date").option("--tags <tags>", "New comma-separated tags").option("--note <note>", "Add a completion note").action(async (id, options) => {
3014
+ const { render: render5 } = await import("ink");
3015
+ render5(React12.createElement(EditUI, { id, updates: options }));
3016
+ });
3017
+ }
3018
+
3019
+ // src/commands/stats.tsx
3020
+ import React13, { useEffect as useEffect12, useState as useState12 } from "react";
3021
+ import { Box as Box15, Text as Text15, useApp as useApp10, useInput as useInput5, render as render2 } from "ink";
3022
+ import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
3023
+ function ProgressBar({
3024
+ value,
3025
+ max,
3026
+ width = 24,
3027
+ color = "cyan",
3028
+ showPercent = false
3029
+ }) {
3030
+ const percent = max > 0 ? Math.round(value / max * 100) : 0;
3031
+ const filled = max > 0 ? Math.min(width, Math.round(value / max * width)) : 0;
3032
+ const empty = Math.max(0, width - filled);
3033
+ return /* @__PURE__ */ jsxs15(Box15, { children: [
3034
+ /* @__PURE__ */ jsx15(Text15, { color: "gray", children: "\u2562" }),
3035
+ /* @__PURE__ */ jsx15(Text15, { color, children: "\u2588".repeat(filled) }),
3036
+ /* @__PURE__ */ jsx15(Text15, { color: "gray", children: "\u2591".repeat(empty) }),
3037
+ /* @__PURE__ */ jsx15(Text15, { color: "gray", children: "\u255F" }),
3038
+ showPercent && /* @__PURE__ */ jsxs15(Text15, { color, bold: true, children: [
3039
+ " ",
3040
+ String(percent).padStart(3),
3041
+ "%"
3042
+ ] })
3043
+ ] });
3044
+ }
3045
+ function StatCard({
3046
+ title,
3047
+ value,
3048
+ subtitle,
3049
+ borderColor = "gray",
3050
+ valueColor = "white"
3051
+ }) {
3052
+ return /* @__PURE__ */ jsxs15(
3053
+ Box15,
3054
+ {
3055
+ flexDirection: "column",
3056
+ borderStyle: "round",
3057
+ borderColor,
3058
+ paddingX: 2,
3059
+ paddingY: 0,
3060
+ flexGrow: 1,
3061
+ minWidth: 16,
3062
+ children: [
3063
+ /* @__PURE__ */ jsx15(Text15, { color: "gray", dimColor: true, wrap: "truncate", children: title.toUpperCase() }),
3064
+ /* @__PURE__ */ jsx15(Text15, { color: valueColor, bold: true, wrap: "truncate", children: value }),
3065
+ subtitle && /* @__PURE__ */ jsx15(Text15, { color: "gray", dimColor: true, wrap: "truncate", children: subtitle })
3066
+ ]
3067
+ }
3068
+ );
3069
+ }
3070
+ function StatsUI({ live = false }) {
3071
+ const { exit } = useApp10();
3072
+ const [stats, setStats] = useState12(computeStats());
3073
+ const [tasks, setTasks] = useState12(getAllTasks());
3074
+ useEffect12(() => {
3075
+ if (!live) {
3076
+ const timer = setTimeout(() => exit(), 100);
3077
+ return () => clearTimeout(timer);
3078
+ }
3079
+ }, [live, exit]);
3080
+ useEffect12(() => {
3081
+ if (!live) return;
3082
+ const interval = setInterval(() => {
3083
+ setStats(computeStats());
3084
+ setTasks(getAllTasks());
3085
+ }, 1e3);
3086
+ return () => clearInterval(interval);
3087
+ }, [live]);
3088
+ useInput5(
3089
+ (input, key) => {
3090
+ if (live && (input === "q" || key.escape || key.ctrl && input === "c")) {
3091
+ exit();
3092
+ }
3093
+ },
3094
+ { isActive: Boolean(live && process.stdin?.isTTY && typeof process.stdin?.setRawMode === "function") }
3095
+ );
3096
+ const overdueTasks = tasks.filter(isOverdue);
3097
+ const completionRate = stats.completion_rate;
3098
+ const statusRows = [
3099
+ { label: "Pending", value: stats.pending, color: BRAND.muted, desc: "Awaiting action" },
3100
+ { label: "In Progress", value: stats.in_progress, color: BRAND.info, desc: "Currently active" },
3101
+ { label: "Completed", value: stats.completed, color: BRAND.success, desc: "Resolved" },
3102
+ { label: "Overdue", value: stats.overdue, color: stats.overdue > 0 ? BRAND.error : BRAND.success, desc: "Past due date" }
3103
+ ];
3104
+ const priorityRows = [
3105
+ { label: "\u25CF High Priority", value: stats.high_priority, color: BRAND.error },
3106
+ { label: "\u25B2 Medium Priority", value: stats.medium_priority, color: BRAND.warning },
3107
+ { label: "\u25C6 Low Priority", value: stats.low_priority, color: BRAND.success }
3108
+ ];
3109
+ return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
3110
+ /* @__PURE__ */ jsx15(MiniHeader, {}),
3111
+ /* @__PURE__ */ jsxs15(Box15, { marginBottom: 1, justifyContent: "space-between", alignItems: "center", children: [
3112
+ /* @__PURE__ */ jsx15(Text15, { color: BRAND.orange, bold: true, children: "\u2726 Ziphro Performance Analytics" }),
3113
+ live && /* @__PURE__ */ jsx15(Box15, { borderStyle: "round", borderColor: BRAND.success, paddingX: 1, children: /* @__PURE__ */ jsx15(Text15, { color: BRAND.success, children: "\u25CF LIVE (Press 'q' to exit)" }) })
3114
+ ] }),
3115
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, marginBottom: 1, children: [
3116
+ /* @__PURE__ */ jsx15(
3117
+ StatCard,
3118
+ {
3119
+ title: "Total Tasks",
3120
+ value: stats.total,
3121
+ subtitle: "All items in local store",
3122
+ borderColor: BRAND.border,
3123
+ valueColor: BRAND.white
3124
+ }
3125
+ ),
3126
+ /* @__PURE__ */ jsx15(
3127
+ StatCard,
3128
+ {
3129
+ title: "Completion Rate",
3130
+ value: `${completionRate}%`,
3131
+ subtitle: `${stats.completed} of ${stats.total} resolved`,
3132
+ borderColor: BRAND.border,
3133
+ valueColor: completionRate > 75 ? BRAND.success : BRAND.orange
3134
+ }
3135
+ ),
3136
+ /* @__PURE__ */ jsx15(
3137
+ StatCard,
3138
+ {
3139
+ title: "Active (In Prog)",
3140
+ value: stats.in_progress,
3141
+ subtitle: "Focused in flight",
3142
+ borderColor: BRAND.border,
3143
+ valueColor: BRAND.info
3144
+ }
3145
+ ),
3146
+ /* @__PURE__ */ jsx15(
3147
+ StatCard,
3148
+ {
3149
+ title: "Overdue Alert",
3150
+ value: stats.overdue,
3151
+ subtitle: stats.overdue > 0 ? "Action required!" : "Zero overdue items",
3152
+ borderColor: stats.overdue > 0 ? BRAND.error : BRAND.border,
3153
+ valueColor: stats.overdue > 0 ? BRAND.error : BRAND.success
3154
+ }
3155
+ )
3156
+ ] }),
3157
+ /* @__PURE__ */ jsxs15(Box15, { gap: 1, marginBottom: 1, children: [
3158
+ /* @__PURE__ */ jsxs15(
3159
+ Box15,
3160
+ {
3161
+ flexDirection: "column",
3162
+ borderStyle: "round",
3163
+ borderColor: BRAND.border,
3164
+ paddingX: 2,
3165
+ paddingY: 1,
3166
+ flexGrow: 1,
3167
+ children: [
3168
+ /* @__PURE__ */ jsx15(Text15, { color: BRAND.white, bold: true, children: "Status Distribution" }),
3169
+ /* @__PURE__ */ jsx15(Box15, { height: 1 }),
3170
+ statusRows.map((row) => /* @__PURE__ */ jsxs15(Box15, { justifyContent: "space-between", alignItems: "center", marginBottom: 0, children: [
3171
+ /* @__PURE__ */ jsx15(Box15, { width: 16, children: /* @__PURE__ */ jsx15(Text15, { color: BRAND.muted, children: row.label }) }),
3172
+ /* @__PURE__ */ jsx15(Box15, { width: 6, children: /* @__PURE__ */ jsx15(Text15, { color: row.color, bold: true, children: String(row.value).padStart(4) }) }),
3173
+ /* @__PURE__ */ jsx15(
3174
+ ProgressBar,
3175
+ {
3176
+ value: row.value,
3177
+ max: stats.total,
3178
+ width: 16,
3179
+ color: row.color,
3180
+ showPercent: true
3181
+ }
3182
+ )
3183
+ ] }, row.label))
3184
+ ]
3185
+ }
3186
+ ),
3187
+ /* @__PURE__ */ jsxs15(
3188
+ Box15,
3189
+ {
3190
+ flexDirection: "column",
3191
+ borderStyle: "round",
3192
+ borderColor: BRAND.border,
3193
+ paddingX: 2,
3194
+ paddingY: 1,
3195
+ flexGrow: 1,
3196
+ children: [
3197
+ /* @__PURE__ */ jsx15(Text15, { color: BRAND.white, bold: true, children: "Priority Allocation" }),
3198
+ /* @__PURE__ */ jsx15(Box15, { height: 1 }),
3199
+ priorityRows.map((row) => /* @__PURE__ */ jsxs15(Box15, { justifyContent: "space-between", alignItems: "center", marginBottom: 0, children: [
3200
+ /* @__PURE__ */ jsx15(Box15, { width: 20, children: /* @__PURE__ */ jsx15(Text15, { color: row.color, children: row.label }) }),
3201
+ /* @__PURE__ */ jsx15(Box15, { width: 6, children: /* @__PURE__ */ jsx15(Text15, { color: row.color, bold: true, children: String(row.value).padStart(4) }) }),
3202
+ /* @__PURE__ */ jsx15(
3203
+ ProgressBar,
3204
+ {
3205
+ value: row.value,
3206
+ max: stats.total,
3207
+ width: 16,
3208
+ color: row.color,
3209
+ showPercent: true
3210
+ }
3211
+ )
3212
+ ] }, row.label))
3213
+ ]
3214
+ }
3215
+ )
3216
+ ] }),
3217
+ /* @__PURE__ */ jsxs15(
3218
+ Box15,
3219
+ {
3220
+ borderStyle: "round",
3221
+ borderColor: "gray",
3222
+ paddingX: 2,
3223
+ paddingY: 0,
3224
+ justifyContent: "space-between",
3225
+ children: [
3226
+ /* @__PURE__ */ jsxs15(Text15, { color: "gray", children: [
3227
+ "Health:",
3228
+ " ",
3229
+ /* @__PURE__ */ jsx15(Text15, { color: stats.overdue === 0 ? "green" : "yellow", bold: true, children: stats.overdue === 0 ? "Optimal \xB7 All tasks on track" : `Attention \xB7 ${stats.overdue} tasks overdue` })
3230
+ ] }),
3231
+ /* @__PURE__ */ jsxs15(Text15, { color: "gray", children: [
3232
+ "Storage: ",
3233
+ /* @__PURE__ */ jsxs15(Text15, { color: "cyan", children: [
3234
+ tasks.length,
3235
+ " local records"
3236
+ ] }),
3237
+ " \xB7 Sync ready"
3238
+ ] })
3239
+ ]
3240
+ }
3241
+ )
3242
+ ] });
3243
+ }
3244
+ function registerStats(program2) {
3245
+ program2.command("stats").description("View task analytics, completion rates, and priority breakdown").option("-l, --live", "Keep dashboard open with real-time updates", false).action((options) => {
3246
+ render2(React13.createElement(StatsUI, { live: options.live }));
3247
+ });
3248
+ }
3249
+
3250
+ // src/commands/sync.tsx
3251
+ import React14, { useState as useState13, useEffect as useEffect13 } from "react";
3252
+ import { Box as Box16, Text as Text16, useApp as useApp11 } from "ink";
3253
+ import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
3254
+ function SyncUI({ dryRun, password }) {
3255
+ const { exit } = useApp11();
3256
+ const [status, setStatus] = useState13("loading");
3257
+ const [message, setMessage] = useState13("");
3258
+ const [details, setDetails] = useState13([]);
3259
+ useEffect13(() => {
3260
+ async function doSync() {
3261
+ try {
3262
+ const auth = requireAuth();
3263
+ let tasks = getAllTasks();
3264
+ const syncPassword = password || auth.password;
3265
+ if (!syncPassword) {
3266
+ setMessage("Encryption password required. Use --password flag or configure it.");
3267
+ setStatus("error");
3268
+ setTimeout(() => exit(new Error("Password required")), 1200);
3269
+ return;
3270
+ }
3271
+ let currentRevision = 1;
3272
+ const downloadRes = await apiDownloadSync(auth.apiUrl, auth.accessToken);
3273
+ if (downloadRes.success && downloadRes.data?.encrypted_blob) {
3274
+ try {
3275
+ currentRevision = downloadRes.data.revision || 1;
3276
+ const decrypted = await decrypt(downloadRes.data.encrypted_blob, syncPassword, {
3277
+ userId: auth.email,
3278
+ revision: currentRevision,
3279
+ version: 2
3280
+ });
3281
+ const remoteBundle = JSON.parse(decrypted);
3282
+ if (remoteBundle.tasks && Array.isArray(remoteBundle.tasks)) {
3283
+ tasks = mergeTasks(tasks, remoteBundle.tasks, auth.deviceId);
3284
+ replaceTasks(tasks);
3285
+ }
3286
+ } catch (decryptErr) {
3287
+ console.warn("[Sync] Could not decrypt existing remote blob (new master key?):", decryptErr);
3288
+ }
3289
+ }
3290
+ const nextRevision = currentRevision + 1;
3291
+ const bundle = {
3292
+ tasks,
3293
+ schema_version: "2",
3294
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
3295
+ };
3296
+ const plaintext = JSON.stringify(bundle);
3297
+ const blobChecksum = await checksum(plaintext);
3298
+ if (dryRun) {
3299
+ const lastSync = getLastSyncedAt();
3300
+ setDetails([
3301
+ `Tasks to sync: ${tasks.length}`,
3302
+ `Checksum: ${blobChecksum.slice(0, 16)}\u2026`,
3303
+ `Revision: ${nextRevision} (current: ${currentRevision})`,
3304
+ `Last synced: ${lastSync ?? "never"}`,
3305
+ `API endpoint: ${auth.apiUrl}/api/v1/tasks`,
3306
+ "(dry-run: no changes made)"
3307
+ ]);
3308
+ setStatus("dry-run");
3309
+ setTimeout(() => exit(), 100);
3310
+ return;
3311
+ }
3312
+ const blob = await encrypt(plaintext, syncPassword, {
3313
+ userId: auth.email,
3314
+ revision: nextRevision,
3315
+ version: 2
3316
+ });
3317
+ const res = await apiUploadSync(
3318
+ auth.apiUrl,
3319
+ auth.accessToken,
3320
+ blob,
3321
+ blobChecksum,
3322
+ auth.deviceId,
3323
+ currentRevision
3324
+ );
3325
+ if (res.success) {
3326
+ clearSyncQueue();
3327
+ setMessage(`Synced ${tasks.length} task${tasks.length !== 1 ? "s" : ""} to cloud`);
3328
+ setDetails([
3329
+ `Encrypted with AES-256-GCM + AAD (Revision ${nextRevision})`,
3330
+ `Checksum: ${blobChecksum.slice(0, 16)}\u2026`,
3331
+ `CRDT Conflict-Free Merge verified`
3332
+ ]);
3333
+ setStatus("success");
3334
+ setTimeout(() => exit(), 1500);
3335
+ } else if (res.error?.code === "NETWORK_ERROR") {
3336
+ setMessage("System is offline. Sync is queued!");
3337
+ setDetails([
3338
+ "Tasks stored locally in cache (tasks.json) at zero cost",
3339
+ `Sync queue contains ${tasks.length} pending task(s)`,
3340
+ "Will upload to cloud & Google Drive once internet is restored."
3341
+ ]);
3342
+ setStatus("offline");
3343
+ setTimeout(() => exit(), 3e3);
3344
+ } else {
3345
+ setMessage(res.error?.message ?? "Sync failed");
3346
+ setStatus("error");
3347
+ setTimeout(() => exit(new Error(res.error?.message)), 1200);
3348
+ }
3349
+ } catch (e) {
3350
+ setMessage(e.message);
3351
+ setStatus("error");
3352
+ setTimeout(() => exit(e), 1200);
3353
+ }
3354
+ }
3355
+ doSync();
3356
+ }, []);
3357
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
3358
+ /* @__PURE__ */ jsx16(MiniHeader, {}),
3359
+ /* @__PURE__ */ jsxs16(Box16, { marginTop: 1, flexDirection: "column", children: [
3360
+ status === "loading" && /* @__PURE__ */ jsx16(Spinner, { label: "Syncing to cloud (E2E encrypted + CRDT)\u2026", type: "orbit", color: BRAND.orange }),
3361
+ status === "success" && /* @__PURE__ */ jsxs16(Fragment4, { children: [
3362
+ /* @__PURE__ */ jsx16(StarBurst, { label: message, color: BRAND.orange }),
3363
+ details.map((d) => /* @__PURE__ */ jsxs16(Box16, { children: [
3364
+ /* @__PURE__ */ jsx16(Text16, { color: BRAND.muted, children: " \xB7 " }),
3365
+ /* @__PURE__ */ jsx16(Text16, { color: BRAND.muted, children: d })
3366
+ ] }, d))
3367
+ ] }),
3368
+ status === "error" && /* @__PURE__ */ jsx16(StatusBadge, { type: "error", message }),
3369
+ status === "offline" && /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
3370
+ /* @__PURE__ */ jsx16(StatusBadge, { type: "warn", message }),
3371
+ details.map((d) => /* @__PURE__ */ jsxs16(Box16, { children: [
3372
+ /* @__PURE__ */ jsx16(Text16, { color: BRAND.muted, children: " \xB7 " }),
3373
+ /* @__PURE__ */ jsx16(Text16, { color: BRAND.warning, children: d })
3374
+ ] }, d))
3375
+ ] }),
3376
+ status === "dry-run" && /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
3377
+ /* @__PURE__ */ jsx16(StatusBadge, { type: "info", message: "Dry run \u2014 sync preview:" }),
3378
+ details.map((d) => /* @__PURE__ */ jsxs16(Box16, { children: [
3379
+ /* @__PURE__ */ jsx16(Text16, { color: BRAND.muted, children: " \xB7 " }),
3380
+ /* @__PURE__ */ jsx16(Text16, { color: BRAND.info, children: d })
3381
+ ] }, d))
3382
+ ] })
3383
+ ] })
3384
+ ] });
3385
+ }
3386
+ function registerSync(program2) {
3387
+ program2.command("sync").description("Sync tasks with cloud (E2E encrypted + CRDT merge)").option("--dry-run", "Preview what would be synced without making changes", false).option("--password <password>", "Master password for encryption").action(async (options) => {
3388
+ const { render: render5 } = await import("ink");
3389
+ render5(React14.createElement(SyncUI, { dryRun: options.dryRun, password: options.password }));
3390
+ });
3391
+ }
3392
+
3393
+ // src/commands/export.ts
3394
+ import { writeFileSync as writeFileSync3 } from "fs";
3395
+ function tasksToCSV(bundle) {
3396
+ const headers = ["id", "description", "priority", "status", "tags", "due_date", "created_at", "updated_at", "completed_at", "completion_note"];
3397
+ const rows = bundle.tasks.map((t) => [
3398
+ t.id,
3399
+ `"${t.description.replace(/"/g, '""')}"`,
3400
+ t.priority,
3401
+ t.status,
3402
+ `"${t.tags.join(";")}"`,
3403
+ t.due_date ?? "",
3404
+ t.created_at,
3405
+ t.updated_at,
3406
+ t.completed_at ?? "",
3407
+ t.completion_note ? `"${t.completion_note.replace(/"/g, '""')}"` : ""
3408
+ ]);
3409
+ return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
3410
+ }
3411
+ function tasksToMarkdown(bundle) {
3412
+ const lines = [
3413
+ "# Ziphro Export",
3414
+ `_Exported at: ${bundle.exported_at}_`,
3415
+ "",
3416
+ "| ID | Description | Priority | Status | Due | Tags |",
3417
+ "|---|---|---|---|---|---|"
3418
+ ];
3419
+ for (const t of bundle.tasks) {
3420
+ lines.push(
3421
+ `| ${t.id.slice(0, 8)} | ${t.description} | ${t.priority} | ${t.status} | ${t.due_date ?? "\u2014"} | ${t.tags.join(", ")} |`
3422
+ );
3423
+ }
3424
+ return lines.join("\n");
3425
+ }
3426
+ function registerExport(program2) {
3427
+ program2.command("export").description("Export tasks to JSON, CSV, or Markdown").option("-f, --format <format>", "Export format: json | csv | markdown", "json").option("-o, --output <file>", "Output file path (default: stdout)").action((options) => {
3428
+ const tasks = getAllTasks();
3429
+ const bundle = {
3430
+ tasks,
3431
+ schema_version: "1",
3432
+ exported_at: (/* @__PURE__ */ new Date()).toISOString()
3433
+ };
3434
+ let content;
3435
+ switch (options.format) {
3436
+ case "csv":
3437
+ content = tasksToCSV(bundle);
3438
+ break;
3439
+ case "markdown":
3440
+ content = tasksToMarkdown(bundle);
3441
+ break;
3442
+ default:
3443
+ content = JSON.stringify(bundle, null, 2);
3444
+ }
3445
+ if (options.output) {
3446
+ writeFileSync3(options.output, content, "utf8");
3447
+ console.log(`\u2713 Exported ${tasks.length} tasks to ${options.output}`);
3448
+ } else {
3449
+ console.log(content);
3450
+ }
3451
+ });
3452
+ }
3453
+
3454
+ // src/commands/upgrade.tsx
3455
+ import React15, { useState as useState14, useEffect as useEffect14 } from "react";
3456
+ import { Box as Box17, Text as Text17, useApp as useApp12 } from "ink";
3457
+ import { exec as exec2 } from "child_process";
3458
+ import { jsx as jsx17, jsxs as jsxs17 } from "react/jsx-runtime";
3459
+ async function fetchTargetVersion(pkgName, currentVersion) {
3460
+ const isPrerelease = currentVersion.includes("-");
3461
+ try {
3462
+ const res = await fetch(`https://registry.npmjs.org/-/package/${pkgName}/dist-tags`, {
3463
+ signal: AbortSignal.timeout(5e3)
3464
+ });
3465
+ if (!res.ok) return { version: "", tag: "latest" };
3466
+ const tags = await res.json();
3467
+ const latestTag = tags.latest || "";
3468
+ const previewTag = tags.preview || "";
3469
+ if (isPrerelease && previewTag) {
3470
+ if (latestTag && isOutdated(previewTag, latestTag)) {
3471
+ return { version: latestTag, tag: "latest" };
3472
+ }
3473
+ return { version: previewTag, tag: "preview" };
3474
+ }
3475
+ return { version: latestTag || previewTag, tag: latestTag ? "latest" : "preview" };
3476
+ } catch {
3477
+ return { version: "", tag: "latest" };
3478
+ }
3479
+ }
3480
+ function installGlobalPackage(spec, setState) {
3481
+ return new Promise((resolve) => {
3482
+ exec2(`npm install -g ${spec}`, (error, _stdout, stderr) => {
3483
+ if (error) {
3484
+ setState((prev) => ({
3485
+ ...prev,
3486
+ status: "error",
3487
+ errorMsg: error.message || stderr || "Failed to install updates"
3488
+ }));
3489
+ } else {
3490
+ setState((prev) => ({ ...prev, status: "success" }));
3491
+ }
3492
+ resolve();
3493
+ });
3494
+ });
3495
+ }
3496
+ function UpgradeUI({ target }) {
3497
+ const { exit } = useApp12();
3498
+ const shouldUpgradeCli = !target || target.toLowerCase() === "cli";
3499
+ const shouldUpgradeMcp = !target || target.toLowerCase() === "mcp";
3500
+ const [cliState, setCliState] = useState14({
3501
+ name: "Ziphro CLI",
3502
+ pkgName: "ziphro-cli",
3503
+ current: CLI_VERSION,
3504
+ latest: "",
3505
+ status: shouldUpgradeCli ? "checking" : "up-to-date",
3506
+ errorMsg: ""
3507
+ });
3508
+ const [mcpState, setMcpState] = useState14({
3509
+ name: "Ziphro MCP Server",
3510
+ pkgName: "ziphro-mcp",
3511
+ current: "",
3512
+ latest: "",
3513
+ status: shouldUpgradeMcp ? "checking" : "up-to-date",
3514
+ errorMsg: ""
3515
+ });
3516
+ useEffect14(() => {
3517
+ async function checkAndUpgrade() {
3518
+ const tasks = [];
3519
+ let mcpCurrent = "";
3520
+ if (shouldUpgradeMcp) {
3521
+ mcpCurrent = await new Promise((resolve) => {
3522
+ exec2("npm list -g ziphro-mcp --json", (err, stdout) => {
3523
+ if (err) {
3524
+ resolve("");
3525
+ return;
3526
+ }
3527
+ try {
3528
+ const data = JSON.parse(stdout);
3529
+ resolve(data.dependencies?.["ziphro-mcp"]?.version || "");
3530
+ } catch {
3531
+ resolve("");
3532
+ }
3533
+ });
3534
+ });
3535
+ }
3536
+ const cliTarget = shouldUpgradeCli ? await fetchTargetVersion("ziphro-cli", CLI_VERSION) : { version: "", tag: "latest" };
3537
+ const mcpTarget = shouldUpgradeMcp ? await fetchTargetVersion("ziphro-mcp", mcpCurrent || CLI_VERSION) : { version: "", tag: "latest" };
3538
+ if (shouldUpgradeCli) {
3539
+ if (!cliTarget.version) {
3540
+ setCliState((prev) => ({
3541
+ ...prev,
3542
+ status: "error",
3543
+ errorMsg: "Could not fetch latest version from registry."
3544
+ }));
3545
+ } else {
3546
+ setCliState((prev) => ({ ...prev, latest: cliTarget.version }));
3547
+ if (isOutdated(CLI_VERSION, cliTarget.version)) {
3548
+ setCliState((prev) => ({ ...prev, status: "upgrading" }));
3549
+ tasks.push(installGlobalPackage(`ziphro-cli@${cliTarget.tag}`, setCliState));
3550
+ } else {
3551
+ setCliState((prev) => ({ ...prev, status: "up-to-date" }));
3552
+ }
3553
+ }
3554
+ }
3555
+ if (shouldUpgradeMcp) {
3556
+ if (!mcpTarget.version) {
3557
+ setMcpState((prev) => ({
3558
+ ...prev,
3559
+ status: "error",
3560
+ errorMsg: "Could not fetch latest version from registry."
3561
+ }));
3562
+ } else {
3563
+ setMcpState((prev) => ({ ...prev, latest: mcpTarget.version, current: mcpCurrent }));
3564
+ const isMcpInstalled = !!mcpCurrent;
3565
+ if (!isMcpInstalled) {
3566
+ if (target === "mcp") {
3567
+ setMcpState((prev) => ({ ...prev, status: "upgrading" }));
3568
+ tasks.push(installGlobalPackage(`ziphro-mcp@${mcpTarget.tag}`, setMcpState));
3569
+ } else {
3570
+ setMcpState((prev) => ({ ...prev, status: "not-installed" }));
3571
+ }
3572
+ } else if (isOutdated(mcpCurrent, mcpTarget.version)) {
3573
+ setMcpState((prev) => ({ ...prev, status: "upgrading" }));
3574
+ tasks.push(installGlobalPackage(`ziphro-mcp@${mcpTarget.tag}`, setMcpState));
3575
+ } else {
3576
+ setMcpState((prev) => ({ ...prev, status: "up-to-date" }));
3577
+ }
3578
+ }
3579
+ }
3580
+ if (tasks.length > 0) {
3581
+ await Promise.all(tasks);
3582
+ }
3583
+ setTimeout(() => exit(), 2500);
3584
+ }
3585
+ checkAndUpgrade();
3586
+ }, []);
3587
+ const manualInstallHint = CLI_VERSION.includes("-") ? "@preview" : "";
3588
+ return /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", paddingX: 1, paddingY: 0, children: [
3589
+ /* @__PURE__ */ jsx17(MiniHeader, {}),
3590
+ /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", borderStyle: "round", borderColor: BRAND.border, paddingX: 2, paddingY: 1, marginTop: 1, children: [
3591
+ /* @__PURE__ */ jsx17(Box17, { marginBottom: 1, children: /* @__PURE__ */ jsx17(Text17, { color: BRAND.orange, bold: true, children: "\u2726 Ziphro Upgrade" }) }),
3592
+ shouldUpgradeCli && /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", marginBottom: 1, children: [
3593
+ /* @__PURE__ */ jsx17(Text17, { bold: true, color: "white", children: "Ziphro CLI:" }),
3594
+ cliState.status === "checking" && /* @__PURE__ */ jsx17(Spinner, { label: `Checking latest version... (Current: v${CLI_VERSION})`, type: "orbit" }),
3595
+ cliState.status === "upgrading" && /* @__PURE__ */ jsx17(Spinner, { label: `Upgrading globally: v${CLI_VERSION} \u2192 v${cliState.latest}...`, type: "star", color: "yellow" }),
3596
+ cliState.status === "success" && /* @__PURE__ */ jsxs17(Text17, { color: "green", children: [
3597
+ "\u2713 Successfully upgraded to v",
3598
+ cliState.latest,
3599
+ "!"
3600
+ ] }),
3601
+ cliState.status === "up-to-date" && /* @__PURE__ */ jsxs17(Text17, { color: "gray", children: [
3602
+ "\u2713 Already up to date (v",
3603
+ CLI_VERSION,
3604
+ ")"
3605
+ ] }),
3606
+ cliState.status === "error" && /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", children: [
3607
+ /* @__PURE__ */ jsxs17(Text17, { color: "red", children: [
3608
+ "\u2717 Upgrade failed: ",
3609
+ cliState.errorMsg
3610
+ ] }),
3611
+ /* @__PURE__ */ jsxs17(Text17, { color: "gray", dimColor: true, children: [
3612
+ "Run manually: npm install -g ziphro-cli",
3613
+ manualInstallHint
3614
+ ] })
3615
+ ] })
3616
+ ] }),
3617
+ shouldUpgradeMcp && /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", children: [
3618
+ /* @__PURE__ */ jsx17(Text17, { bold: true, color: "white", children: "Ziphro MCP Server:" }),
3619
+ mcpState.status === "checking" && /* @__PURE__ */ jsx17(Spinner, { label: `Checking latest version... (Current: ${mcpState.current ? "v" + mcpState.current : "not installed"})`, type: "orbit" }),
3620
+ mcpState.status === "upgrading" && /* @__PURE__ */ jsx17(Spinner, { label: `Upgrading globally: ${mcpState.current ? "v" + mcpState.current : "installing"} \u2192 v${mcpState.latest}...`, type: "star", color: "yellow" }),
3621
+ mcpState.status === "success" && /* @__PURE__ */ jsxs17(Text17, { color: "green", children: [
3622
+ "\u2713 Successfully upgraded to v",
3623
+ mcpState.latest,
3624
+ "!"
3625
+ ] }),
3626
+ mcpState.status === "up-to-date" && /* @__PURE__ */ jsxs17(Text17, { color: "gray", children: [
3627
+ "\u2713 Already up to date (v",
3628
+ mcpState.current,
3629
+ ")"
3630
+ ] }),
3631
+ mcpState.status === "not-installed" && /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", children: [
3632
+ /* @__PURE__ */ jsx17(Text17, { color: "yellow", children: "\u2601 Not installed globally." }),
3633
+ /* @__PURE__ */ jsxs17(Text17, { color: "gray", dimColor: true, children: [
3634
+ "Install manually: npm install -g ziphro-mcp",
3635
+ manualInstallHint
3636
+ ] }),
3637
+ /* @__PURE__ */ jsx17(Text17, { color: "gray", dimColor: true, children: "Or install now with: ziphro upgrade mcp" })
3638
+ ] }),
3639
+ mcpState.status === "error" && /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", children: [
3640
+ /* @__PURE__ */ jsxs17(Text17, { color: "red", children: [
3641
+ "\u2717 Upgrade failed: ",
3642
+ mcpState.errorMsg
3643
+ ] }),
3644
+ /* @__PURE__ */ jsxs17(Text17, { color: "gray", dimColor: true, children: [
3645
+ "Run manually: npm install -g ziphro-mcp",
3646
+ manualInstallHint
3647
+ ] })
3648
+ ] })
3649
+ ] })
3650
+ ] })
3651
+ ] });
3652
+ }
3653
+ function registerUpgrade(program2) {
3654
+ program2.command("upgrade [target]").description('Upgrade Ziphro CLI and MCP Server. Target can be "cli", "mcp", or omitted to upgrade both.').action(async (target) => {
3655
+ const { render: render5 } = await import("ink");
3656
+ render5(React15.createElement(UpgradeUI, { target }));
3657
+ });
3658
+ }
3659
+
3660
+ // src/commands/dashboard.tsx
3661
+ import React16 from "react";
3662
+ import { render as render3 } from "ink";
3663
+ function registerDashboard(program2) {
3664
+ program2.command("dashboard").alias("dash").alias("tui").description("Launch the full interactive TUI Command Deck with live task navigation").action(() => {
3665
+ render3(React16.createElement(InteractiveDashboard));
3666
+ });
3667
+ }
3668
+
3669
+ // src/index.ts
3670
+ program.name("ziphro").description(
3671
+ "\u2726 Privacy-first task management with E2E encryption \xB7 AI-native \xB7 CLI \xB7 MCP \xB7 Web"
3672
+ ).version(CLI_VERSION, "-v, --version", "Output the current version").helpOption("-h, --help", "Display help information");
3673
+ registerDashboard(program);
3674
+ registerConfig(program);
3675
+ registerLogin(program);
3676
+ registerLogout(program);
3677
+ registerWhoami(program);
3678
+ registerUpgrade(program);
3679
+ registerAdd(program);
3680
+ registerList(program);
3681
+ registerDone(program);
3682
+ registerRemove(program);
3683
+ registerEdit(program);
3684
+ registerStats(program);
3685
+ registerSync(program);
3686
+ registerExport(program);
3687
+ program.addHelpText(
3688
+ "beforeAll",
3689
+ `
3690
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557
3691
+ \u255A\u2550\u2550\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557
3692
+ \u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551
3693
+ \u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551
3694
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
3695
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D
3696
+
3697
+ \u2726 CLI \xB7 MCP \xB7 Web \u2726
3698
+ `
3699
+ );
3700
+ var args = [...process.argv];
3701
+ var aliasMap = {
3702
+ "-a": "add",
3703
+ "-l": "list",
3704
+ "-d": "done",
3705
+ "-r": "remove",
3706
+ "-e": "edit"
3707
+ };
3708
+ if (args[2] && aliasMap[args[2]]) {
3709
+ args[2] = aliasMap[args[2]];
3710
+ }
3711
+ if (args.length <= 2 && process.stdout.isTTY) {
3712
+ render4(React17.createElement(InteractiveDashboard));
3713
+ } else {
3714
+ program.parseAsync(args).catch((err) => {
3715
+ console.error(`
3716
+ \u2717 ${err.message}`);
3717
+ process.exit(1);
3718
+ });
3719
+ }