taskair-cli 1.0.1 → 1.0.4

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.
Files changed (3) hide show
  1. package/README.md +7 -15
  2. package/dist/index.js +434 -251
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -18,30 +18,22 @@ npm install -g taskair-cli
18
18
 
19
19
  ## 🪐 Setup & Configuration
20
20
 
21
- Before managing your tasks, you need to point the CLI to your TaskAir Web server and set up your encryption master password.
21
+ Before managing your tasks, you need to configure the CLI to point to your TaskAir Web server and authenticate.
22
22
 
23
- ### 1. Configure the CLI
23
+ ### 1. Configure & Authenticate
24
24
 
25
- Run the configuration command and follow the prompts:
25
+ Run the configuration command and follow the prompt to open the browser for authentication:
26
26
 
27
27
  ```bash
28
- taskair configure
28
+ taskair config
29
29
  ```
30
30
 
31
31
  You will be prompted to enter:
32
- * **API URL**: The URL of your running TaskAir server (e.g., `http://localhost:4321` or your production domain).
33
- * **Email**: Your TaskAir account email address.
34
- * **Master Password**: Your local master password. This password is used client-side to derive the encryption keys for E2E encryption. *It is never sent to the server.*
32
+ * **API URL**: The URL of your running TaskAir server (e.g., `http://localhost:3001` or your production domain).
35
33
 
36
- ### 2. Log In
34
+ After pressing Enter, a web browser window will open automatically. Complete your sign-in or enter your Master Password on the website to link your CLI terminal.
37
35
 
38
- Log in to establish a session with the remote backend:
39
-
40
- ```bash
41
- taskair login
42
- ```
43
-
44
- ### 3. Verify Session
36
+ ### 2. Verify Session
45
37
 
46
38
  You can check your configuration and login status at any time:
47
39
 
package/dist/index.js CHANGED
@@ -3,26 +3,47 @@
3
3
  // src/index.ts
4
4
  import { program } from "commander";
5
5
 
6
- // src/commands/configure.tsx
7
- import React2, { useState as useState2 } from "react";
8
- import { Box as Box3, Text as Text3, useInput, useApp } from "ink";
6
+ // src/commands/config.tsx
7
+ import React3, { useState as useState3, useEffect as useEffect3 } from "react";
8
+ import { Box as Box4, Text as Text4, useInput, useApp } from "ink";
9
+ import { hostname } from "os";
10
+ import http from "http";
11
+ import { parse as parseUrl } from "url";
12
+ import { exec } from "child_process";
9
13
 
10
14
  // src/lib/auth.ts
11
15
  import { homedir } from "os";
12
16
  import { join } from "path";
13
- import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
17
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, statSync } from "fs";
14
18
  var TASKAIR_DIR = join(homedir(), ".taskair");
15
19
  var CREDENTIALS_FILE = join(TASKAIR_DIR, "credentials");
16
20
  function ensureTaskairDir() {
17
- if (!existsSync(TASKAIR_DIR)) {
21
+ if (existsSync(TASKAIR_DIR)) {
22
+ try {
23
+ const stat = statSync(TASKAIR_DIR);
24
+ if (!stat.isDirectory()) {
25
+ throw new Error(`Path ${TASKAIR_DIR} exists but is not a directory.`);
26
+ }
27
+ } catch (err) {
28
+ if (err.code !== "ENOENT") {
29
+ throw err;
30
+ }
31
+ }
32
+ return;
33
+ }
34
+ try {
18
35
  mkdirSync(TASKAIR_DIR, { recursive: true, mode: 448 });
36
+ } catch (err) {
37
+ if (err.code !== "EEXIST") {
38
+ throw err;
39
+ }
19
40
  }
20
41
  }
21
42
  function parseIni(content) {
22
43
  const result = {};
23
- for (const line of content.split("\n")) {
44
+ for (const line of content.split(/\r?\n/)) {
24
45
  const trimmed = line.trim();
25
- if (trimmed.startsWith("#") || trimmed.startsWith("[") || !trimmed) continue;
46
+ if (trimmed.startsWith("#") || trimmed.startsWith(";") || trimmed.startsWith("[") || !trimmed) continue;
26
47
  const eqIdx = trimmed.indexOf("=");
27
48
  if (eqIdx === -1) continue;
28
49
  const key = trimmed.slice(0, eqIdx).trim();
@@ -46,13 +67,15 @@ function readCredentials() {
46
67
  try {
47
68
  const content = readFileSync(CREDENTIALS_FILE, "utf8");
48
69
  const raw = parseIni(content);
70
+ const rawExpires = raw.expires_at ? parseInt(raw.expires_at, 10) : void 0;
49
71
  return {
50
72
  api_url: raw.api_url || "http://localhost:3001",
51
73
  email: raw.email || "",
52
74
  access_token: raw.access_token || void 0,
53
75
  refresh_token: raw.refresh_token || void 0,
54
- expires_at: raw.expires_at ? parseInt(raw.expires_at) : void 0,
55
- device_id: raw.device_id || void 0
76
+ expires_at: rawExpires && !isNaN(rawExpires) ? rawExpires : void 0,
77
+ device_id: raw.device_id || void 0,
78
+ password: raw.password || void 0
56
79
  };
57
80
  } catch {
58
81
  return null;
@@ -66,7 +89,8 @@ function writeCredentials(creds) {
66
89
  access_token: creds.access_token,
67
90
  refresh_token: creds.refresh_token,
68
91
  expires_at: creds.expires_at,
69
- device_id: creds.device_id
92
+ device_id: creds.device_id,
93
+ password: creds.password
70
94
  };
71
95
  writeFileSync(CREDENTIALS_FILE, toIni(data), { encoding: "utf8" });
72
96
  try {
@@ -98,10 +122,67 @@ function requireAuth() {
98
122
  deviceId: creds.device_id ?? "default",
99
123
  accessToken: creds.access_token,
100
124
  apiUrl: creds.api_url,
101
- expiresAt: creds.expires_at ?? 0
125
+ expiresAt: creds.expires_at ?? 0,
126
+ password: creds.password
102
127
  };
103
128
  }
104
129
 
130
+ // src/lib/api.ts
131
+ async function request(apiUrl, path, options = {}) {
132
+ const url = `${apiUrl.replace(/\/$/, "")}${path}`;
133
+ const headers = {
134
+ "Content-Type": "application/json",
135
+ "User-Agent": "taskair-cli/1.0.0"
136
+ };
137
+ if (options.token) {
138
+ headers["Authorization"] = `Bearer ${options.token}`;
139
+ }
140
+ try {
141
+ const res = await fetch(url, {
142
+ method: options.method ?? "GET",
143
+ headers,
144
+ body: options.body ? JSON.stringify(options.body) : void 0
145
+ });
146
+ const json = await res.json();
147
+ return json;
148
+ } catch (err) {
149
+ const message = err instanceof Error ? err.message : "Network error";
150
+ return {
151
+ success: false,
152
+ error: {
153
+ code: "NETWORK_ERROR",
154
+ message: `Cannot reach API: ${message}`
155
+ }
156
+ };
157
+ }
158
+ }
159
+ async function apiLogin(apiUrl, email, password) {
160
+ return request(apiUrl, "/auth/login", {
161
+ method: "POST",
162
+ body: { email, password }
163
+ });
164
+ }
165
+ async function apiLogout(apiUrl, token) {
166
+ return request(apiUrl, "/auth/logout", {
167
+ method: "POST",
168
+ token
169
+ });
170
+ }
171
+ async function apiRegisterDevice(apiUrl, token, deviceName, platform) {
172
+ return request(apiUrl, "/auth/device", {
173
+ method: "POST",
174
+ token,
175
+ body: { device_name: deviceName, platform }
176
+ });
177
+ }
178
+ async function apiUploadSync(apiUrl, token, encryptedBlob, chk, deviceId) {
179
+ return request(apiUrl, "/sync/upload", {
180
+ method: "POST",
181
+ token,
182
+ body: { encrypted_blob: encryptedBlob, checksum: chk, device_id: deviceId }
183
+ });
184
+ }
185
+
105
186
  // src/components/StarBurst.tsx
106
187
  import { useState, useEffect } from "react";
107
188
  import { Box, Text } from "ink";
@@ -173,235 +254,299 @@ function StatusBadge({
173
254
  ] });
174
255
  }
175
256
 
176
- // src/components/AsciiHeader.tsx
257
+ // src/components/Spinner.tsx
258
+ import { useState as useState2, useEffect as useEffect2 } from "react";
177
259
  import { Box as Box2, Text as Text2 } from "ink";
178
260
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
261
+ var ORBIT_FRAMES = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
262
+ var STAR_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
263
+ function Spinner({
264
+ label = "Loading...",
265
+ type = "orbit",
266
+ color = "cyan"
267
+ }) {
268
+ const frames = type === "orbit" ? ORBIT_FRAMES : STAR_FRAMES;
269
+ const [frame, setFrame] = useState2(0);
270
+ useEffect2(() => {
271
+ const timer = setInterval(() => {
272
+ setFrame((f) => (f + 1) % frames.length);
273
+ }, 80);
274
+ return () => clearInterval(timer);
275
+ }, [frames.length]);
276
+ return /* @__PURE__ */ jsxs2(Box2, { children: [
277
+ /* @__PURE__ */ jsxs2(Text2, { color, bold: true, children: [
278
+ frames[frame],
279
+ " "
280
+ ] }),
281
+ /* @__PURE__ */ jsx2(Text2, { color: "white", children: label })
282
+ ] });
283
+ }
284
+
285
+ // src/components/AsciiHeader.tsx
286
+ import { Box as Box3, Text as Text3 } from "ink";
287
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
179
288
  var LOGO = [
180
- "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
181
- "\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
182
- " \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
183
- " \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
184
- " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
185
- " \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\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\u255D"
289
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
290
+ "\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
291
+ " \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
292
+ " \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
293
+ " \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
294
+ " \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\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\u255D"
186
295
  ];
187
296
  var TAGLINE = " \u2726 Space-themed \xB7 Privacy-first \xB7 AI-native task management \u2726";
188
297
  function AsciiHeader() {
189
- return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
190
- /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: LOGO.map((line, i) => /* @__PURE__ */ jsx2(Text2, { color: "magenta", bold: true, children: line }, i)) }),
191
- /* @__PURE__ */ jsx2(Text2, { color: "cyan", dimColor: true, children: TAGLINE }),
192
- /* @__PURE__ */ jsx2(Box2, { marginTop: 0, children: /* @__PURE__ */ jsx2(Text2, { color: "#7B61FF", children: "\u2500".repeat(60) }) })
298
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
299
+ /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: LOGO.map((line, i) => /* @__PURE__ */ jsx3(Text3, { color: "magenta", bold: true, children: line }, i)) }),
300
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", dimColor: true, children: TAGLINE }),
301
+ /* @__PURE__ */ jsx3(Box3, { marginTop: 0, children: /* @__PURE__ */ jsx3(Text3, { color: "#7B61FF", children: "\u2500".repeat(60) }) })
193
302
  ] });
194
303
  }
195
304
  function MiniHeader() {
196
- return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
197
- /* @__PURE__ */ jsxs2(Text2, { color: "magenta", bold: true, children: [
305
+ return /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
306
+ /* @__PURE__ */ jsxs3(Text3, { color: "magenta", bold: true, children: [
198
307
  "\u2726",
199
308
  " "
200
309
  ] }),
201
- /* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "TaskAir" }),
202
- /* @__PURE__ */ jsx2(Text2, { color: "#7B61FF", children: " \u2014 Space-grade task management" })
310
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "TaskAir" }),
311
+ /* @__PURE__ */ jsx3(Text3, { color: "#7B61FF", children: " \u2014 Space-grade task management" })
203
312
  ] });
204
313
  }
205
314
 
206
- // src/commands/configure.tsx
207
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
208
- function ConfigureUI({ initial }) {
315
+ // src/commands/config.tsx
316
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
317
+ function openBrowser(url) {
318
+ const start = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
319
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : `${start} "${url}"`;
320
+ exec(cmd);
321
+ }
322
+ function ConfigUI({ initialApiUrl }) {
209
323
  const { exit } = useApp();
210
- const fields = ["apiUrl", "email", "password"];
211
- const [fieldIndex, setFieldIndex] = useState2(0);
212
- const [values, setValues] = useState2({
213
- apiUrl: initial.apiUrl,
214
- email: initial.email,
215
- password: ""
216
- });
217
- const [currentInput, setCurrentInput] = useState2(
218
- initial.apiUrl
219
- );
220
- const [done, setDone] = useState2(false);
221
- const [error, setError] = useState2("");
222
- const labels = {
223
- apiUrl: "API URL",
224
- email: "Email address",
225
- password: "Master password (used for E2E encryption)"
226
- };
324
+ const [stage, setStage] = useState3("input_api_url");
325
+ const [apiUrl, setApiUrl] = useState3(initialApiUrl);
326
+ const [currentInput, setCurrentInput] = useState3(initialApiUrl);
327
+ const [port, setPort] = useState3(0);
328
+ const [errorMsg, setErrorMsg] = useState3("");
329
+ const [authDetails, setAuthDetails] = useState3(null);
227
330
  useInput((input, key) => {
228
- if (done) return;
229
- if (key.return) {
230
- const field = fields[fieldIndex];
231
- if (!field) return;
232
- if (!currentInput.trim()) {
233
- setError(`${labels[field]} cannot be empty`);
234
- return;
235
- }
236
- setError("");
237
- const updated = { ...values, [field]: currentInput.trim() };
238
- setValues(updated);
239
- if (fieldIndex < fields.length - 1) {
240
- setFieldIndex(fieldIndex + 1);
241
- const nextField = fields[fieldIndex + 1];
242
- setCurrentInput(updated[nextField ?? "password"] ?? "");
331
+ if (stage === "input_api_url") {
332
+ if (key.return) {
333
+ if (!currentInput.trim()) {
334
+ setErrorMsg("API URL cannot be empty");
335
+ return;
336
+ }
337
+ setErrorMsg("");
338
+ setApiUrl(currentInput.trim());
339
+ setStage("waiting_for_browser");
340
+ } else if (key.backspace || key.delete) {
341
+ setCurrentInput((v) => v.slice(0, -1));
342
+ } else if (key.escape) {
343
+ exit();
243
344
  } else {
244
- writeCredentials({
245
- api_url: updated.apiUrl,
246
- email: updated.email
247
- });
248
- setDone(true);
249
- setTimeout(() => exit(), 500);
345
+ setCurrentInput((v) => v + input);
346
+ }
347
+ } else {
348
+ if (key.escape) {
349
+ exit();
250
350
  }
251
- return;
252
351
  }
253
- if (key.backspace || key.delete) {
254
- setCurrentInput((v) => v.slice(0, -1));
255
- return;
352
+ });
353
+ useEffect3(() => {
354
+ if (stage !== "waiting_for_browser") return;
355
+ let server = null;
356
+ let isActive = true;
357
+ async function runServer() {
358
+ try {
359
+ const srv = http.createServer((req, res) => {
360
+ const parsed = parseUrl(req.url || "", true);
361
+ if (parsed.pathname === "/callback") {
362
+ const q = parsed.query;
363
+ const accessToken = q.access_token;
364
+ const refreshToken = q.refresh_token;
365
+ const expiresAt = q.expires_at;
366
+ const email = q.email;
367
+ const password = q.password;
368
+ if (!accessToken || !email) {
369
+ res.writeHead(400, { "Content-Type": "text/html" });
370
+ res.end("<h1>Authentication Failed</h1><p>Missing credentials.</p>");
371
+ if (isActive) {
372
+ setErrorMsg("Authentication failed: Missing credentials from redirect.");
373
+ setStage("error");
374
+ setTimeout(() => exit(new Error("Missing credentials")), 1500);
375
+ }
376
+ return;
377
+ }
378
+ res.writeHead(200, { "Content-Type": "text/html" });
379
+ res.end(`
380
+ <!DOCTYPE html>
381
+ <html>
382
+ <head>
383
+ <title>TaskAir CLI Authenticated</title>
384
+ <style>
385
+ body {
386
+ background: #0D0D11;
387
+ color: #FFFFFF;
388
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: center;
392
+ min-height: 100vh;
393
+ margin: 0;
394
+ }
395
+ .card {
396
+ background: #13131A;
397
+ border: 1px solid #23232F;
398
+ border-radius: 12px;
399
+ padding: 40px;
400
+ text-align: center;
401
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.5);
402
+ max-width: 400px;
403
+ width: 100%;
404
+ }
405
+ h1 {
406
+ color: #7B61FF;
407
+ margin-top: 0;
408
+ font-size: 24px;
409
+ }
410
+ p {
411
+ color: #A1A1B5;
412
+ font-size: 14px;
413
+ }
414
+ </style>
415
+ </head>
416
+ <body>
417
+ <div class="card">
418
+ <h1>Authenticated Successfully</h1>
419
+ <p>You can close this tab and return to your terminal.</p>
420
+ </div>
421
+ </body>
422
+ </html>
423
+ `);
424
+ if (isActive) {
425
+ setAuthDetails({
426
+ accessToken,
427
+ refreshToken,
428
+ expiresAt: parseInt(expiresAt, 10),
429
+ email,
430
+ password: password || void 0
431
+ });
432
+ setStage("registering");
433
+ }
434
+ } else {
435
+ res.writeHead(404);
436
+ res.end("Not Found");
437
+ }
438
+ });
439
+ server = srv;
440
+ srv.listen(0, "127.0.0.1", () => {
441
+ const addr = srv.address();
442
+ const p = typeof addr === "string" ? 0 : addr?.port || 0;
443
+ setPort(p);
444
+ const authUrl = `${apiUrl.replace(/\/$/, "")}/cli-auth?callback=http://localhost:${p}/callback`;
445
+ openBrowser(authUrl);
446
+ });
447
+ } catch (err) {
448
+ if (isActive) {
449
+ setErrorMsg(err.message || "Failed to start local server");
450
+ setStage("error");
451
+ setTimeout(() => exit(err), 1500);
452
+ }
453
+ }
256
454
  }
257
- if (key.escape) {
258
- exit();
259
- return;
455
+ runServer();
456
+ return () => {
457
+ isActive = false;
458
+ if (server) {
459
+ server.close();
460
+ }
461
+ };
462
+ }, [stage, apiUrl]);
463
+ useEffect3(() => {
464
+ if (stage !== "registering" || !authDetails) return;
465
+ async function registerAndSave() {
466
+ try {
467
+ const deviceName = `${hostname() || "CLI"} - ${process.platform || "Client"}`;
468
+ const devRes = await apiRegisterDevice(apiUrl, authDetails.accessToken, deviceName, "cli");
469
+ if (devRes.success && devRes.data) {
470
+ const existing = readCredentials();
471
+ writeCredentials({
472
+ ...existing,
473
+ api_url: apiUrl,
474
+ email: authDetails.email,
475
+ access_token: authDetails.accessToken,
476
+ refresh_token: authDetails.refreshToken,
477
+ expires_at: authDetails.expiresAt,
478
+ device_id: devRes.data.id,
479
+ password: authDetails.password
480
+ });
481
+ setStage("success");
482
+ setTimeout(() => exit(), 1200);
483
+ } else {
484
+ setErrorMsg(devRes.error?.message ?? "Device registration failed");
485
+ setStage("error");
486
+ setTimeout(() => exit(new Error(devRes.error?.message)), 1500);
487
+ }
488
+ } catch (err) {
489
+ setErrorMsg(err.message || "Error completing configuration");
490
+ setStage("error");
491
+ setTimeout(() => exit(err), 1500);
492
+ }
260
493
  }
261
- setCurrentInput((v) => v + input);
262
- });
263
- const currentField = fields[fieldIndex];
264
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", padding: 1, children: [
265
- /* @__PURE__ */ jsx3(AsciiHeader, {}),
266
- /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
267
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "\u2726 Configure TaskAir" }),
268
- /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "Set up your credentials. Press Enter to confirm each field." })
494
+ registerAndSave();
495
+ }, [stage, authDetails, apiUrl]);
496
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", padding: 1, children: [
497
+ /* @__PURE__ */ jsx4(AsciiHeader, {}),
498
+ /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
499
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "\u2726 Configure TaskAir" }),
500
+ stage === "input_api_url" && /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "Enter the API URL of your TaskAir instance. Press Enter to confirm." })
269
501
  ] }),
270
- fields.map((field, i) => {
271
- const isActive = i === fieldIndex && !done;
272
- const isDone = i < fieldIndex || done;
273
- return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginBottom: 0, children: /* @__PURE__ */ jsxs3(Box3, { children: [
274
- /* @__PURE__ */ jsx3(Text3, { color: isActive ? "cyan" : isDone ? "green" : "gray", bold: true, children: isDone ? "\u2713 " : isActive ? "\u2192 " : " " }),
275
- /* @__PURE__ */ jsxs3(Text3, { color: isActive ? "white" : isDone ? "green" : "gray", children: [
276
- labels[field],
277
- ":",
278
- " "
279
- ] }),
280
- isDone && field !== "password" && /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: values[field] }),
281
- isDone && field === "password" && /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "\u2022".repeat(8) }),
282
- isActive && /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
283
- field === "password" ? "\u2022".repeat(currentInput.length) : currentInput,
284
- /* @__PURE__ */ jsx3(Text3, { color: "white", children: "\u258C" })
285
- ] })
286
- ] }) }, field);
287
- }),
288
- error && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(StatusBadge, { type: "error", message: error }) }),
289
- done && /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
290
- /* @__PURE__ */ jsx3(
291
- StatusBadge,
292
- {
293
- type: "success",
294
- message: "Configuration saved to ~/.taskair/credentials"
295
- }
296
- ),
297
- /* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
298
- "Run",
299
- " ",
300
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "taskair login" }),
301
- " to authenticate."
502
+ stage === "input_api_url" && /* @__PURE__ */ jsxs4(Box4, { children: [
503
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "\u2192 " }),
504
+ /* @__PURE__ */ jsx4(Text4, { color: "white", children: "API URL: " }),
505
+ /* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
506
+ currentInput,
507
+ /* @__PURE__ */ jsx4(Text4, { color: "white", children: "\u258C" })
302
508
  ] })
303
509
  ] }),
304
- /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Press Esc to cancel" }) })
510
+ stage === "waiting_for_browser" && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 0.5, children: [
511
+ /* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
512
+ "\u2713 API URL set to: ",
513
+ apiUrl
514
+ ] }),
515
+ port > 0 ? /* @__PURE__ */ jsxs4(Fragment, { children: [
516
+ /* @__PURE__ */ jsxs4(Text4, { color: "yellow", children: [
517
+ "\u2726 Started local server on port ",
518
+ port
519
+ ] }),
520
+ /* @__PURE__ */ jsx4(Spinner, { label: "Opening browser to authenticate\u2026", type: "orbit", color: "magenta" }),
521
+ /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "If the page does not load, open this URL manually:" }),
522
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", underline: true, children: `${apiUrl.replace(/\/$/, "")}/cli-auth?callback=http://localhost:${port}/callback` })
523
+ ] }) : /* @__PURE__ */ jsx4(Spinner, { label: "Starting local server\u2026", type: "orbit", color: "magenta" })
524
+ ] }),
525
+ stage === "registering" && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
526
+ /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u2713 Received credentials from browser" }),
527
+ /* @__PURE__ */ jsx4(Spinner, { label: "Registering device with backend\u2026", type: "orbit", color: "magenta" })
528
+ ] }),
529
+ stage === "success" && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(StatusBadge, { type: "success", message: "Configuration saved successfully! E2E Encryption Active." }) }),
530
+ stage === "error" && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(StatusBadge, { type: "error", message: errorMsg }) }),
531
+ (stage === "input_api_url" || stage === "waiting_for_browser") && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "Press Esc to cancel" }) })
305
532
  ] });
306
533
  }
307
- function registerConfigure(program2) {
308
- program2.command("configure").description("Set up API URL, email, and master password").action(async () => {
534
+ function registerConfig(program2) {
535
+ program2.command("config").alias("configure").description("Authenticate and configure CLI via browser").action(async () => {
309
536
  const existing = readCredentials();
310
537
  const { render } = await import("ink");
311
538
  render(
312
- React2.createElement(ConfigureUI, {
313
- initial: {
314
- apiUrl: existing?.api_url ?? "http://localhost:3001",
315
- email: existing?.email ?? ""
316
- }
539
+ React3.createElement(ConfigUI, {
540
+ initialApiUrl: existing?.api_url ?? "http://localhost:3001"
317
541
  })
318
542
  );
319
543
  });
320
544
  }
321
545
 
322
546
  // src/commands/login.tsx
323
- import React4, { useState as useState4, useEffect as useEffect3 } from "react";
547
+ import React4, { useState as useState4, useEffect as useEffect4 } from "react";
324
548
  import { Box as Box5, Text as Text5, useInput as useInput2, useApp as useApp2 } from "ink";
325
- import { v4 as uuidv4 } from "uuid";
326
-
327
- // src/lib/api.ts
328
- async function request(apiUrl, path, options = {}) {
329
- const url = `${apiUrl.replace(/\/$/, "")}${path}`;
330
- const headers = {
331
- "Content-Type": "application/json",
332
- "User-Agent": "taskair-cli/1.0.0"
333
- };
334
- if (options.token) {
335
- headers["Authorization"] = `Bearer ${options.token}`;
336
- }
337
- try {
338
- const res = await fetch(url, {
339
- method: options.method ?? "GET",
340
- headers,
341
- body: options.body ? JSON.stringify(options.body) : void 0
342
- });
343
- const json = await res.json();
344
- return json;
345
- } catch (err) {
346
- const message = err instanceof Error ? err.message : "Network error";
347
- return {
348
- success: false,
349
- error: {
350
- code: "NETWORK_ERROR",
351
- message: `Cannot reach API: ${message}`
352
- }
353
- };
354
- }
355
- }
356
- async function apiLogin(apiUrl, email, password) {
357
- return request(apiUrl, "/auth/login", {
358
- method: "POST",
359
- body: { email, password }
360
- });
361
- }
362
- async function apiLogout(apiUrl, token) {
363
- return request(apiUrl, "/auth/logout", {
364
- method: "POST",
365
- token
366
- });
367
- }
368
- async function apiUploadSync(apiUrl, token, encryptedBlob, chk, deviceId) {
369
- return request(apiUrl, "/sync/upload", {
370
- method: "POST",
371
- token,
372
- body: { encrypted_blob: encryptedBlob, checksum: chk, device_id: deviceId }
373
- });
374
- }
375
-
376
- // src/components/Spinner.tsx
377
- import { useState as useState3, useEffect as useEffect2 } from "react";
378
- import { Box as Box4, Text as Text4 } from "ink";
379
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
380
- var ORBIT_FRAMES = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
381
- var STAR_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
382
- function Spinner({
383
- label = "Loading...",
384
- type = "orbit",
385
- color = "cyan"
386
- }) {
387
- const frames = type === "orbit" ? ORBIT_FRAMES : STAR_FRAMES;
388
- const [frame, setFrame] = useState3(0);
389
- useEffect2(() => {
390
- const timer = setInterval(() => {
391
- setFrame((f) => (f + 1) % frames.length);
392
- }, 80);
393
- return () => clearInterval(timer);
394
- }, [frames.length]);
395
- return /* @__PURE__ */ jsxs4(Box4, { children: [
396
- /* @__PURE__ */ jsxs4(Text4, { color, bold: true, children: [
397
- frames[frame],
398
- " "
399
- ] }),
400
- /* @__PURE__ */ jsx4(Text4, { color: "white", children: label })
401
- ] });
402
- }
403
-
404
- // src/commands/login.tsx
549
+ import { hostname as hostname2 } from "os";
405
550
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
406
551
  function LoginUI() {
407
552
  const { exit } = useApp2();
@@ -411,22 +556,32 @@ function LoginUI() {
411
556
  const [stage, setStage] = useState4("input_email");
412
557
  const [currentInput, setCurrentInput] = useState4(creds?.email ?? "");
413
558
  const [errorMsg, setErrorMsg] = useState4("");
414
- useEffect3(() => {
559
+ useEffect4(() => {
415
560
  if (stage === "loading") {
416
561
  const apiUrl = creds?.api_url ?? "http://localhost:3001";
417
- apiLogin(apiUrl, email, password).then((res) => {
562
+ apiLogin(apiUrl, email, password).then(async (res) => {
418
563
  if (res.success && res.data) {
419
- const deviceId = creds?.device_id ?? uuidv4();
420
- writeCredentials({
421
- api_url: apiUrl,
422
- email,
423
- access_token: res.data.access_token,
424
- refresh_token: res.data.refresh_token,
425
- expires_at: res.data.expires_at,
426
- device_id: deviceId
427
- });
428
- setStage("success");
429
- setTimeout(() => exit(), 1200);
564
+ const accessToken = res.data.access_token;
565
+ const deviceName = `${hostname2() || "CLI"} - ${process.platform || "Client"}`;
566
+ const devRes = await apiRegisterDevice(apiUrl, accessToken, deviceName, "cli");
567
+ if (devRes.success && devRes.data) {
568
+ const existing = readCredentials();
569
+ writeCredentials({
570
+ ...existing,
571
+ api_url: apiUrl,
572
+ email,
573
+ access_token: accessToken,
574
+ refresh_token: res.data.refresh_token,
575
+ expires_at: res.data.expires_at,
576
+ device_id: devRes.data.id
577
+ });
578
+ setStage("success");
579
+ setTimeout(() => exit(), 1200);
580
+ } else {
581
+ setErrorMsg(devRes.error?.message ?? "Device registration failed");
582
+ setStage("error");
583
+ setTimeout(() => exit(new Error(devRes.error?.message)), 1500);
584
+ }
430
585
  } else {
431
586
  setErrorMsg(res.error?.message ?? "Login failed");
432
587
  setStage("error");
@@ -503,14 +658,14 @@ function registerLogin(program2) {
503
658
  }
504
659
 
505
660
  // src/commands/logout.tsx
506
- import React5, { useEffect as useEffect4, useState as useState5 } from "react";
661
+ import React5, { useEffect as useEffect5, useState as useState5 } from "react";
507
662
  import { Box as Box6, useApp as useApp3 } from "ink";
508
663
  import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
509
664
  function LogoutUI() {
510
665
  const { exit } = useApp3();
511
666
  const [status, setStatus] = useState5("loading");
512
667
  const [message, setMessage] = useState5("");
513
- useEffect4(() => {
668
+ useEffect5(() => {
514
669
  const creds = readCredentials();
515
670
  if (!creds?.access_token) {
516
671
  setMessage("Not logged in.");
@@ -607,14 +762,14 @@ function registerWhoami(program2) {
607
762
  }
608
763
 
609
764
  // src/commands/add.tsx
610
- import React7, { useEffect as useEffect5, useState as useState6 } from "react";
765
+ import React7, { useEffect as useEffect6, useState as useState6 } from "react";
611
766
  import { Box as Box8, Text as Text8, useApp as useApp4 } from "ink";
612
- import { v4 as uuidv43 } from "uuid";
767
+ import { v4 as uuidv42 } from "uuid";
613
768
 
614
769
  // src/lib/storage.ts
615
770
  import { join as join2 } from "path";
616
771
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
617
- import { v4 as uuidv42 } from "uuid";
772
+ import { v4 as uuidv4 } from "uuid";
618
773
  var STORE_FILE = join2(TASKAIR_DIR, "tasks.json");
619
774
  function defaultStore() {
620
775
  return { tasks: [], sync_queue: [] };
@@ -677,7 +832,7 @@ function deleteTask(id) {
677
832
  }
678
833
  function queueOperation(store, operation, taskId) {
679
834
  store.sync_queue.push({
680
- id: uuidv42(),
835
+ id: uuidv4(),
681
836
  operation,
682
837
  task_id: taskId,
683
838
  created_at: (/* @__PURE__ */ new Date()).toISOString()
@@ -741,11 +896,11 @@ function AddUI({
741
896
  const [status, setStatus] = useState6("working");
742
897
  const [taskId, setTaskId] = useState6("");
743
898
  const [errorMsg, setErrorMsg] = useState6("");
744
- useEffect5(() => {
899
+ useEffect6(() => {
745
900
  try {
746
901
  const auth = requireAuth();
747
902
  const task = {
748
- id: uuidv43(),
903
+ id: uuidv42(),
749
904
  description,
750
905
  priority,
751
906
  status: "pending",
@@ -832,7 +987,7 @@ function registerAdd(program2) {
832
987
  }
833
988
 
834
989
  // src/commands/list.tsx
835
- import React9, { useEffect as useEffect6, useState as useState7 } from "react";
990
+ import React9, { useEffect as useEffect7, useState as useState7 } from "react";
836
991
  import { Box as Box10, Text as Text10, useApp as useApp5 } from "ink";
837
992
 
838
993
  // src/components/TaskTable.tsx
@@ -996,7 +1151,7 @@ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
996
1151
  function ListUI({ filter, format }) {
997
1152
  const { exit } = useApp5();
998
1153
  const [tasks, setTasks] = useState7([]);
999
- useEffect6(() => {
1154
+ useEffect7(() => {
1000
1155
  const results = filterTasks(filter);
1001
1156
  results.sort((a, b) => {
1002
1157
  const pOrder = { high: 0, medium: 1, low: 2 };
@@ -1064,15 +1219,15 @@ function registerList(program2) {
1064
1219
  }
1065
1220
 
1066
1221
  // src/commands/done.tsx
1067
- import React10, { useEffect as useEffect7, useState as useState8 } from "react";
1222
+ import React10, { useEffect as useEffect8, useState as useState8 } from "react";
1068
1223
  import { Box as Box11, Text as Text11, useApp as useApp6 } from "ink";
1069
- import { Fragment, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1224
+ import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
1070
1225
  function DoneUI({ id, note }) {
1071
1226
  const { exit } = useApp6();
1072
1227
  const [status, setStatus] = useState8("success");
1073
1228
  const [message, setMessage] = useState8("");
1074
1229
  const [description, setDescription] = useState8("");
1075
- useEffect7(() => {
1230
+ useEffect8(() => {
1076
1231
  const task = getTask(id);
1077
1232
  if (!task) {
1078
1233
  setMessage(`No task found with ID starting with "${id}"`);
@@ -1100,7 +1255,7 @@ function DoneUI({ id, note }) {
1100
1255
  }, []);
1101
1256
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", padding: 1, children: [
1102
1257
  /* @__PURE__ */ jsx11(MiniHeader, {}),
1103
- /* @__PURE__ */ jsx11(Box11, { marginTop: 1, flexDirection: "column", children: status === "success" ? /* @__PURE__ */ jsxs11(Fragment, { children: [
1258
+ /* @__PURE__ */ jsx11(Box11, { marginTop: 1, flexDirection: "column", children: status === "success" ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
1104
1259
  /* @__PURE__ */ jsx11(StarBurst, { label: message, color: "green" }),
1105
1260
  description && /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, children: [
1106
1261
  /* @__PURE__ */ jsx11(Text11, { color: "gray", children: " " }),
@@ -1122,7 +1277,7 @@ function registerDone(program2) {
1122
1277
  }
1123
1278
 
1124
1279
  // src/commands/remove.tsx
1125
- import React11, { useEffect as useEffect8, useState as useState9 } from "react";
1280
+ import React11, { useEffect as useEffect9, useState as useState9 } from "react";
1126
1281
  import { Box as Box12, Text as Text12, useApp as useApp7, useInput as useInput3 } from "ink";
1127
1282
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
1128
1283
  function RemoveUI({ id, force }) {
@@ -1130,7 +1285,7 @@ function RemoveUI({ id, force }) {
1130
1285
  const [stage, setStage] = useState9("confirm");
1131
1286
  const [message, setMessage] = useState9("");
1132
1287
  const task = getTask(id);
1133
- useEffect8(() => {
1288
+ useEffect9(() => {
1134
1289
  if (!task) {
1135
1290
  setMessage(`No task found with ID starting with "${id}"`);
1136
1291
  setStage("error");
@@ -1203,15 +1358,15 @@ function registerRemove(program2) {
1203
1358
  }
1204
1359
 
1205
1360
  // src/commands/edit.tsx
1206
- import React12, { useState as useState10, useEffect as useEffect9 } from "react";
1361
+ import React12, { useState as useState10, useEffect as useEffect10 } from "react";
1207
1362
  import { Box as Box13, Text as Text13, useApp as useApp8 } from "ink";
1208
- import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1363
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
1209
1364
  function EditUI({ id, updates }) {
1210
1365
  const { exit } = useApp8();
1211
1366
  const [status, setStatus] = useState10("success");
1212
1367
  const [message, setMessage] = useState10("");
1213
1368
  const [changes, setChanges] = useState10([]);
1214
- useEffect9(() => {
1369
+ useEffect10(() => {
1215
1370
  const task = getTask(id);
1216
1371
  if (!task) {
1217
1372
  setMessage(`No task found with ID starting with "${id}"`);
@@ -1264,7 +1419,7 @@ function EditUI({ id, updates }) {
1264
1419
  }, []);
1265
1420
  return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", padding: 1, children: [
1266
1421
  /* @__PURE__ */ jsx13(MiniHeader, {}),
1267
- /* @__PURE__ */ jsx13(Box13, { marginTop: 1, flexDirection: "column", children: status === "success" ? /* @__PURE__ */ jsxs13(Fragment2, { children: [
1422
+ /* @__PURE__ */ jsx13(Box13, { marginTop: 1, flexDirection: "column", children: status === "success" ? /* @__PURE__ */ jsxs13(Fragment3, { children: [
1268
1423
  /* @__PURE__ */ jsx13(StarBurst, { label: message, color: "magenta" }),
1269
1424
  changes.map((c) => /* @__PURE__ */ jsxs13(Box13, { marginTop: 0, children: [
1270
1425
  /* @__PURE__ */ jsx13(Text13, { color: "gray", children: " \u21B3 " }),
@@ -1281,9 +1436,9 @@ function registerEdit(program2) {
1281
1436
  }
1282
1437
 
1283
1438
  // src/commands/stats.tsx
1284
- import React13, { useEffect as useEffect10 } from "react";
1439
+ import React13, { useEffect as useEffect11 } from "react";
1285
1440
  import { Box as Box14, Text as Text14, useApp as useApp9 } from "ink";
1286
- import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
1441
+ import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
1287
1442
  function ProgressBar({
1288
1443
  value,
1289
1444
  max,
@@ -1302,7 +1457,7 @@ function ProgressBar({
1302
1457
  function StatsUI() {
1303
1458
  const { exit } = useApp9();
1304
1459
  const stats = computeStats();
1305
- useEffect10(() => {
1460
+ useEffect11(() => {
1306
1461
  setTimeout(() => exit(), 100);
1307
1462
  }, []);
1308
1463
  const rows = [
@@ -1338,7 +1493,7 @@ function StatsUI() {
1338
1493
  row.label.padEnd(16)
1339
1494
  ] }),
1340
1495
  /* @__PURE__ */ jsx14(Text14, { color: row.color, bold: true, children: String(row.value).padStart(4) }),
1341
- stats.total > 0 && /* @__PURE__ */ jsxs14(Fragment3, { children: [
1496
+ stats.total > 0 && /* @__PURE__ */ jsxs14(Fragment4, { children: [
1342
1497
  /* @__PURE__ */ jsx14(Text14, { color: "gray", children: " " }),
1343
1498
  /* @__PURE__ */ jsx14(
1344
1499
  ProgressBar,
@@ -1360,7 +1515,7 @@ function StatsUI() {
1360
1515
  row.label.padEnd(20)
1361
1516
  ] }),
1362
1517
  /* @__PURE__ */ jsx14(Text14, { color: row.color, bold: true, children: String(row.value).padStart(4) }),
1363
- stats.total > 0 && /* @__PURE__ */ jsxs14(Fragment3, { children: [
1518
+ stats.total > 0 && /* @__PURE__ */ jsxs14(Fragment4, { children: [
1364
1519
  /* @__PURE__ */ jsx14(Text14, { color: "gray", children: " " }),
1365
1520
  /* @__PURE__ */ jsx14(
1366
1521
  ProgressBar,
@@ -1398,7 +1553,7 @@ function registerStats(program2) {
1398
1553
  }
1399
1554
 
1400
1555
  // src/commands/sync.tsx
1401
- import React14, { useState as useState11, useEffect as useEffect11 } from "react";
1556
+ import React14, { useState as useState11, useEffect as useEffect12 } from "react";
1402
1557
  import { Box as Box15, Text as Text15, useApp as useApp10 } from "ink";
1403
1558
 
1404
1559
  // src/lib/crypto.ts
@@ -1446,13 +1601,13 @@ function checksum(plaintext) {
1446
1601
  }
1447
1602
 
1448
1603
  // src/commands/sync.tsx
1449
- import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
1604
+ import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
1450
1605
  function SyncUI({ dryRun, password }) {
1451
1606
  const { exit } = useApp10();
1452
1607
  const [status, setStatus] = useState11("loading");
1453
1608
  const [message, setMessage] = useState11("");
1454
1609
  const [details, setDetails] = useState11([]);
1455
- useEffect11(() => {
1610
+ useEffect12(() => {
1456
1611
  async function doSync() {
1457
1612
  try {
1458
1613
  const auth = requireAuth();
@@ -1477,13 +1632,14 @@ function SyncUI({ dryRun, password }) {
1477
1632
  setTimeout(() => exit(), 100);
1478
1633
  return;
1479
1634
  }
1480
- if (!password) {
1481
- setMessage("Encryption password required. Use --password flag.");
1635
+ const syncPassword = password || auth.password;
1636
+ if (!syncPassword) {
1637
+ setMessage("Encryption password required. Use --password flag or configure it.");
1482
1638
  setStatus("error");
1483
1639
  setTimeout(() => exit(new Error("Password required")), 1200);
1484
1640
  return;
1485
1641
  }
1486
- const blob = encrypt(plaintext, password);
1642
+ const blob = encrypt(plaintext, syncPassword);
1487
1643
  const res = await apiUploadSync(
1488
1644
  auth.apiUrl,
1489
1645
  auth.accessToken,
@@ -1500,6 +1656,15 @@ function SyncUI({ dryRun, password }) {
1500
1656
  ]);
1501
1657
  setStatus("success");
1502
1658
  setTimeout(() => exit(), 1500);
1659
+ } else if (res.error?.code === "NETWORK_ERROR") {
1660
+ setMessage("System is offline. Sync is queued!");
1661
+ setDetails([
1662
+ "Tasks stored locally in cache (tasks.json) at zero cost",
1663
+ `Sync queue contains ${tasks.length} pending task(s)`,
1664
+ "Will upload to cloud & Google Drive once internet is restored."
1665
+ ]);
1666
+ setStatus("offline");
1667
+ setTimeout(() => exit(), 3e3);
1503
1668
  } else {
1504
1669
  setMessage(res.error?.message ?? "Sync failed");
1505
1670
  setStatus("error");
@@ -1517,7 +1682,7 @@ function SyncUI({ dryRun, password }) {
1517
1682
  /* @__PURE__ */ jsx15(MiniHeader, {}),
1518
1683
  /* @__PURE__ */ jsxs15(Box15, { marginTop: 1, flexDirection: "column", children: [
1519
1684
  status === "loading" && /* @__PURE__ */ jsx15(Spinner, { label: "Syncing to cloud (E2E encrypted)\u2026", type: "orbit", color: "magenta" }),
1520
- status === "success" && /* @__PURE__ */ jsxs15(Fragment4, { children: [
1685
+ status === "success" && /* @__PURE__ */ jsxs15(Fragment5, { children: [
1521
1686
  /* @__PURE__ */ jsx15(StarBurst, { label: message, color: "cyan" }),
1522
1687
  details.map((d) => /* @__PURE__ */ jsxs15(Box15, { children: [
1523
1688
  /* @__PURE__ */ jsx15(Text15, { color: "gray", children: " \xB7 " }),
@@ -1525,6 +1690,13 @@ function SyncUI({ dryRun, password }) {
1525
1690
  ] }, d))
1526
1691
  ] }),
1527
1692
  status === "error" && /* @__PURE__ */ jsx15(StatusBadge, { type: "error", message }),
1693
+ status === "offline" && /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
1694
+ /* @__PURE__ */ jsx15(StatusBadge, { type: "warn", message }),
1695
+ details.map((d) => /* @__PURE__ */ jsxs15(Box15, { children: [
1696
+ /* @__PURE__ */ jsx15(Text15, { color: "gray", children: " \xB7 " }),
1697
+ /* @__PURE__ */ jsx15(Text15, { color: "yellow", dimColor: true, children: d })
1698
+ ] }, d))
1699
+ ] }),
1528
1700
  status === "dry-run" && /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
1529
1701
  /* @__PURE__ */ jsx15(StatusBadge, { type: "info", message: "Dry run \u2014 sync preview:" }),
1530
1702
  details.map((d) => /* @__PURE__ */ jsxs15(Box15, { children: [
@@ -1606,8 +1778,8 @@ function registerExport(program2) {
1606
1778
  // src/index.ts
1607
1779
  program.name("taskair").description(
1608
1780
  "\u2726 Space-themed task management with E2E encryption \xB7 AI-native \xB7 Privacy-first"
1609
- ).version("1.0.0", "-v, --version", "Output the current version").helpOption("-h, --help", "Display help information");
1610
- registerConfigure(program);
1781
+ ).version("1.0.4", "-v, --version", "Output the current version").helpOption("-h, --help", "Display help information");
1782
+ registerConfig(program);
1611
1783
  registerLogin(program);
1612
1784
  registerLogout(program);
1613
1785
  registerWhoami(program);
@@ -1628,7 +1800,18 @@ program.addHelpText(
1628
1800
  \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
1629
1801
  `
1630
1802
  );
1631
- program.parseAsync(process.argv).catch((err) => {
1803
+ var args = [...process.argv];
1804
+ var aliasMap = {
1805
+ "-a": "add",
1806
+ "-l": "list",
1807
+ "-d": "done",
1808
+ "-r": "remove",
1809
+ "-e": "edit"
1810
+ };
1811
+ if (args[2] && aliasMap[args[2]]) {
1812
+ args[2] = aliasMap[args[2]];
1813
+ }
1814
+ program.parseAsync(args).catch((err) => {
1632
1815
  console.error(`
1633
1816
  \u2717 ${err.message}`);
1634
1817
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskair-cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "Space-themed, privacy-first task management CLI with E2E encryption",
5
5
  "type": "module",
6
6
  "bin": {