ziphro-cli 0.0.0-stage → 2.0.0-preview.1
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/README.md +175 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3719 -0
- package/package.json +70 -4
- package/scripts/postinstall.js +29 -0
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.1";
|
|
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
|
+
}
|