taskair-cli 1.0.2 → 1.0.5
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 +7 -15
- package/dist/index.js +618 -250
- 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
|
|
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
|
|
23
|
+
### 1. Configure & Authenticate
|
|
24
24
|
|
|
25
|
-
Run the configuration command and follow the
|
|
25
|
+
Run the configuration command and follow the prompt to open the browser for authentication:
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
taskair
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
7
|
-
import
|
|
8
|
-
import { Box as
|
|
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 (
|
|
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(
|
|
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:
|
|
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,8 +122,65 @@ 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
|
|
127
|
+
};
|
|
128
|
+
}
|
|
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"
|
|
102
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
|
+
});
|
|
103
184
|
}
|
|
104
185
|
|
|
105
186
|
// src/components/StarBurst.tsx
|
|
@@ -173,235 +254,396 @@ function StatusBadge({
|
|
|
173
254
|
] });
|
|
174
255
|
}
|
|
175
256
|
|
|
176
|
-
// src/components/
|
|
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
|
|
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
|
|
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
|
|
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__ */
|
|
190
|
-
/* @__PURE__ */
|
|
191
|
-
/* @__PURE__ */
|
|
192
|
-
/* @__PURE__ */
|
|
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__ */
|
|
197
|
-
/* @__PURE__ */
|
|
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__ */
|
|
202
|
-
/* @__PURE__ */
|
|
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/
|
|
207
|
-
import { jsx as
|
|
208
|
-
function
|
|
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
|
|
211
|
-
const [
|
|
212
|
-
const [
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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 (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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:
|
|
387
|
+
radial-gradient(ellipse 80% 60% at 50% -20%, rgba(123, 97, 255, 0.15) 0%, transparent 70%),
|
|
388
|
+
radial-gradient(ellipse 60% 50% at 80% 20%, rgba(0, 112, 243, 0.12) 0%, transparent 60%),
|
|
389
|
+
radial-gradient(ellipse 50% 40% at 20% 30%, rgba(0, 223, 216, 0.1) 0%, transparent 60%),
|
|
390
|
+
radial-gradient(ellipse 70% 40% at 60% 60%, rgba(235, 54, 127, 0.08) 0%, transparent 70%),
|
|
391
|
+
#0D0D11;
|
|
392
|
+
color: #FFFFFF;
|
|
393
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
394
|
+
display: flex;
|
|
395
|
+
align-items: center;
|
|
396
|
+
justify-content: center;
|
|
397
|
+
min-height: 100vh;
|
|
398
|
+
margin: 0;
|
|
399
|
+
}
|
|
400
|
+
.card {
|
|
401
|
+
background: rgba(19, 19, 26, 0.8);
|
|
402
|
+
backdrop-filter: blur(12px);
|
|
403
|
+
-webkit-backdrop-filter: blur(12px);
|
|
404
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
405
|
+
border-radius: 16px;
|
|
406
|
+
padding: 40px;
|
|
407
|
+
text-align: center;
|
|
408
|
+
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
|
|
409
|
+
max-width: 400px;
|
|
410
|
+
width: 100%;
|
|
411
|
+
animation: fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
412
|
+
}
|
|
413
|
+
.logo-container {
|
|
414
|
+
background: #0D0D11;
|
|
415
|
+
border: 1px solid #1F1F2E;
|
|
416
|
+
border-radius: 8px;
|
|
417
|
+
padding: 8px 16px;
|
|
418
|
+
display: inline-flex;
|
|
419
|
+
align-items: center;
|
|
420
|
+
justify-content: center;
|
|
421
|
+
margin-bottom: 24px;
|
|
422
|
+
animation: pulseRing 2s infinite ease-in-out;
|
|
423
|
+
}
|
|
424
|
+
.text-logo {
|
|
425
|
+
display: inline-flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 6px;
|
|
428
|
+
text-decoration: none !important;
|
|
429
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
430
|
+
font-size: 16px;
|
|
431
|
+
font-weight: 600;
|
|
432
|
+
letter-spacing: -0.5px;
|
|
433
|
+
color: #FFFFFF;
|
|
434
|
+
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
435
|
+
flex-shrink: 0;
|
|
436
|
+
}
|
|
437
|
+
.text-logo-brand {
|
|
438
|
+
transition: letter-spacing 0.3s ease, color 0.3s ease;
|
|
439
|
+
}
|
|
440
|
+
.text-logo-accent {
|
|
441
|
+
background: linear-gradient(135deg, #7B61FF, #0070f3);
|
|
442
|
+
-webkit-background-clip: text;
|
|
443
|
+
-webkit-text-fill-color: transparent;
|
|
444
|
+
}
|
|
445
|
+
.text-logo-spark {
|
|
446
|
+
color: #7B61FF;
|
|
447
|
+
font-size: 11px;
|
|
448
|
+
opacity: 0.7;
|
|
449
|
+
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.3s ease, color 0.3s ease;
|
|
450
|
+
}
|
|
451
|
+
.text-logo:hover {
|
|
452
|
+
opacity: 0.95;
|
|
453
|
+
}
|
|
454
|
+
.text-logo:hover .text-logo-brand {
|
|
455
|
+
letter-spacing: 0.3px;
|
|
456
|
+
}
|
|
457
|
+
.text-logo:hover .text-logo-spark {
|
|
458
|
+
transform: scale(1.3) rotate(180deg);
|
|
459
|
+
opacity: 1;
|
|
460
|
+
color: #00dfd8;
|
|
461
|
+
}
|
|
462
|
+
.success-badge {
|
|
463
|
+
display: inline-flex;
|
|
464
|
+
align-items: center;
|
|
465
|
+
gap: 6px;
|
|
466
|
+
background: rgba(16, 185, 129, 0.1);
|
|
467
|
+
color: #10B981;
|
|
468
|
+
border: 1px solid rgba(16, 185, 129, 0.2);
|
|
469
|
+
padding: 6px 14px;
|
|
470
|
+
border-radius: 99px;
|
|
471
|
+
font-size: 13px;
|
|
472
|
+
font-weight: 500;
|
|
473
|
+
margin-bottom: 20px;
|
|
474
|
+
}
|
|
475
|
+
h1 {
|
|
476
|
+
color: #FFFFFF;
|
|
477
|
+
margin: 0 0 12px 0;
|
|
478
|
+
font-size: 22px;
|
|
479
|
+
font-weight: 600;
|
|
480
|
+
letter-spacing: -0.5px;
|
|
481
|
+
}
|
|
482
|
+
p {
|
|
483
|
+
color: #A1A1B5;
|
|
484
|
+
font-size: 14px;
|
|
485
|
+
line-height: 1.6;
|
|
486
|
+
margin: 0;
|
|
487
|
+
}
|
|
488
|
+
@keyframes fadeUp {
|
|
489
|
+
from { opacity: 0; transform: translateY(20px); }
|
|
490
|
+
to { opacity: 1; transform: translateY(0); }
|
|
491
|
+
}
|
|
492
|
+
@keyframes pulseRing {
|
|
493
|
+
0% { transform: scale(1); box-shadow: 0 4px 14px rgba(123, 97, 255, 0.15); }
|
|
494
|
+
50% { transform: scale(1.03); box-shadow: 0 6px 20px rgba(123, 97, 255, 0.35); }
|
|
495
|
+
100% { transform: scale(1); box-shadow: 0 4px 14px rgba(123, 97, 255, 0.15); }
|
|
496
|
+
}
|
|
497
|
+
</style>
|
|
498
|
+
</head>
|
|
499
|
+
<body>
|
|
500
|
+
<div class="card">
|
|
501
|
+
<div class="logo-container">
|
|
502
|
+
<a href="#" class="text-logo">
|
|
503
|
+
<span class="text-logo-brand" style="color: #FFFFFF;">Task<span class="text-logo-accent">Air</span></span>
|
|
504
|
+
<span class="text-logo-spark">\u2726</span>
|
|
505
|
+
</a>
|
|
506
|
+
</div>
|
|
507
|
+
<div>
|
|
508
|
+
<div class="success-badge">
|
|
509
|
+
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24" style="margin-top: 1px;">
|
|
510
|
+
<polyline points="20 6 9 17 4 12"/>
|
|
511
|
+
</svg>
|
|
512
|
+
<span>CLI Connected</span>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
515
|
+
<h1>Authenticated Successfully</h1>
|
|
516
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
517
|
+
</div>
|
|
518
|
+
</body>
|
|
519
|
+
</html>
|
|
520
|
+
`);
|
|
521
|
+
if (isActive) {
|
|
522
|
+
setAuthDetails({
|
|
523
|
+
accessToken,
|
|
524
|
+
refreshToken,
|
|
525
|
+
expiresAt: parseInt(expiresAt, 10),
|
|
526
|
+
email,
|
|
527
|
+
password: password || void 0
|
|
528
|
+
});
|
|
529
|
+
setStage("registering");
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
res.writeHead(404);
|
|
533
|
+
res.end("Not Found");
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
server = srv;
|
|
537
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
538
|
+
const addr = srv.address();
|
|
539
|
+
const p = typeof addr === "string" ? 0 : addr?.port || 0;
|
|
540
|
+
setPort(p);
|
|
541
|
+
const authUrl = `${apiUrl.replace(/\/$/, "")}/cli-auth?callback=http://localhost:${p}/callback`;
|
|
542
|
+
openBrowser(authUrl);
|
|
543
|
+
});
|
|
544
|
+
} catch (err) {
|
|
545
|
+
if (isActive) {
|
|
546
|
+
setErrorMsg(err.message || "Failed to start local server");
|
|
547
|
+
setStage("error");
|
|
548
|
+
setTimeout(() => exit(err), 1500);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
256
551
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
552
|
+
runServer();
|
|
553
|
+
return () => {
|
|
554
|
+
isActive = false;
|
|
555
|
+
if (server) {
|
|
556
|
+
server.close();
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}, [stage, apiUrl]);
|
|
560
|
+
useEffect3(() => {
|
|
561
|
+
if (stage !== "registering" || !authDetails) return;
|
|
562
|
+
async function registerAndSave() {
|
|
563
|
+
try {
|
|
564
|
+
const deviceName = `${hostname() || "CLI"} - ${process.platform || "Client"}`;
|
|
565
|
+
const devRes = await apiRegisterDevice(apiUrl, authDetails.accessToken, deviceName, "cli");
|
|
566
|
+
if (devRes.success && devRes.data) {
|
|
567
|
+
const existing = readCredentials();
|
|
568
|
+
writeCredentials({
|
|
569
|
+
...existing,
|
|
570
|
+
api_url: apiUrl,
|
|
571
|
+
email: authDetails.email,
|
|
572
|
+
access_token: authDetails.accessToken,
|
|
573
|
+
refresh_token: authDetails.refreshToken,
|
|
574
|
+
expires_at: authDetails.expiresAt,
|
|
575
|
+
device_id: devRes.data.id,
|
|
576
|
+
password: authDetails.password
|
|
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
|
+
}
|
|
585
|
+
} catch (err) {
|
|
586
|
+
setErrorMsg(err.message || "Error completing configuration");
|
|
587
|
+
setStage("error");
|
|
588
|
+
setTimeout(() => exit(err), 1500);
|
|
589
|
+
}
|
|
260
590
|
}
|
|
261
|
-
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
/* @__PURE__ */
|
|
266
|
-
|
|
267
|
-
/* @__PURE__ */
|
|
268
|
-
/* @__PURE__ */ jsx3(Text3, { color: "gray", children: "Set up your credentials. Press Enter to confirm each field." })
|
|
591
|
+
registerAndSave();
|
|
592
|
+
}, [stage, authDetails, apiUrl]);
|
|
593
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", padding: 1, children: [
|
|
594
|
+
/* @__PURE__ */ jsx4(AsciiHeader, {}),
|
|
595
|
+
/* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 1, children: [
|
|
596
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "\u2726 Configure TaskAir" }),
|
|
597
|
+
stage === "input_api_url" && /* @__PURE__ */ jsx4(Text4, { color: "gray", children: "Enter the API URL of your TaskAir instance. Press Enter to confirm." })
|
|
269
598
|
] }),
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
/* @__PURE__ */
|
|
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."
|
|
599
|
+
stage === "input_api_url" && /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
600
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "\u2192 " }),
|
|
601
|
+
/* @__PURE__ */ jsx4(Text4, { color: "white", children: "API URL: " }),
|
|
602
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
|
|
603
|
+
currentInput,
|
|
604
|
+
/* @__PURE__ */ jsx4(Text4, { color: "white", children: "\u258C" })
|
|
302
605
|
] })
|
|
303
606
|
] }),
|
|
304
|
-
|
|
607
|
+
stage === "waiting_for_browser" && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 0.5, children: [
|
|
608
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
|
|
609
|
+
"\u2713 API URL set to: ",
|
|
610
|
+
apiUrl
|
|
611
|
+
] }),
|
|
612
|
+
port > 0 ? /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
613
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "yellow", children: [
|
|
614
|
+
"\u2726 Started local server on port ",
|
|
615
|
+
port
|
|
616
|
+
] }),
|
|
617
|
+
/* @__PURE__ */ jsx4(Spinner, { label: "Opening browser to authenticate\u2026", type: "orbit", color: "magenta" }),
|
|
618
|
+
/* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "If the page does not load, open this URL manually:" }),
|
|
619
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", underline: true, children: `${apiUrl.replace(/\/$/, "")}/cli-auth?callback=http://localhost:${port}/callback` })
|
|
620
|
+
] }) : /* @__PURE__ */ jsx4(Spinner, { label: "Starting local server\u2026", type: "orbit", color: "magenta" })
|
|
621
|
+
] }),
|
|
622
|
+
stage === "registering" && /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
|
|
623
|
+
/* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u2713 Received credentials from browser" }),
|
|
624
|
+
/* @__PURE__ */ jsx4(Spinner, { label: "Registering device with backend\u2026", type: "orbit", color: "magenta" })
|
|
625
|
+
] }),
|
|
626
|
+
stage === "success" && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(StatusBadge, { type: "success", message: "Configuration saved successfully! E2E Encryption Active." }) }),
|
|
627
|
+
stage === "error" && /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx4(StatusBadge, { type: "error", message: errorMsg }) }),
|
|
628
|
+
(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
629
|
] });
|
|
306
630
|
}
|
|
307
|
-
function
|
|
308
|
-
program2.command("configure").description("
|
|
631
|
+
function registerConfig(program2) {
|
|
632
|
+
program2.command("config").alias("configure").description("Authenticate and configure CLI via browser").action(async () => {
|
|
309
633
|
const existing = readCredentials();
|
|
310
634
|
const { render } = await import("ink");
|
|
311
635
|
render(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
apiUrl: existing?.api_url ?? "http://localhost:3001",
|
|
315
|
-
email: existing?.email ?? ""
|
|
316
|
-
}
|
|
636
|
+
React3.createElement(ConfigUI, {
|
|
637
|
+
initialApiUrl: existing?.api_url ?? "http://localhost:3001"
|
|
317
638
|
})
|
|
318
639
|
);
|
|
319
640
|
});
|
|
320
641
|
}
|
|
321
642
|
|
|
322
643
|
// src/commands/login.tsx
|
|
323
|
-
import React4, { useState as useState4, useEffect as
|
|
644
|
+
import React4, { useState as useState4, useEffect as useEffect4 } from "react";
|
|
324
645
|
import { Box as Box5, Text as Text5, useInput as useInput2, useApp as useApp2 } from "ink";
|
|
325
|
-
import {
|
|
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
|
|
646
|
+
import { hostname as hostname2 } from "os";
|
|
405
647
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
406
648
|
function LoginUI() {
|
|
407
649
|
const { exit } = useApp2();
|
|
@@ -411,22 +653,32 @@ function LoginUI() {
|
|
|
411
653
|
const [stage, setStage] = useState4("input_email");
|
|
412
654
|
const [currentInput, setCurrentInput] = useState4(creds?.email ?? "");
|
|
413
655
|
const [errorMsg, setErrorMsg] = useState4("");
|
|
414
|
-
|
|
656
|
+
useEffect4(() => {
|
|
415
657
|
if (stage === "loading") {
|
|
416
658
|
const apiUrl = creds?.api_url ?? "http://localhost:3001";
|
|
417
|
-
apiLogin(apiUrl, email, password).then((res) => {
|
|
659
|
+
apiLogin(apiUrl, email, password).then(async (res) => {
|
|
418
660
|
if (res.success && res.data) {
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
661
|
+
const accessToken = res.data.access_token;
|
|
662
|
+
const deviceName = `${hostname2() || "CLI"} - ${process.platform || "Client"}`;
|
|
663
|
+
const devRes = await apiRegisterDevice(apiUrl, accessToken, deviceName, "cli");
|
|
664
|
+
if (devRes.success && devRes.data) {
|
|
665
|
+
const existing = readCredentials();
|
|
666
|
+
writeCredentials({
|
|
667
|
+
...existing,
|
|
668
|
+
api_url: apiUrl,
|
|
669
|
+
email,
|
|
670
|
+
access_token: accessToken,
|
|
671
|
+
refresh_token: res.data.refresh_token,
|
|
672
|
+
expires_at: res.data.expires_at,
|
|
673
|
+
device_id: devRes.data.id
|
|
674
|
+
});
|
|
675
|
+
setStage("success");
|
|
676
|
+
setTimeout(() => exit(), 1200);
|
|
677
|
+
} else {
|
|
678
|
+
setErrorMsg(devRes.error?.message ?? "Device registration failed");
|
|
679
|
+
setStage("error");
|
|
680
|
+
setTimeout(() => exit(new Error(devRes.error?.message)), 1500);
|
|
681
|
+
}
|
|
430
682
|
} else {
|
|
431
683
|
setErrorMsg(res.error?.message ?? "Login failed");
|
|
432
684
|
setStage("error");
|
|
@@ -503,14 +755,14 @@ function registerLogin(program2) {
|
|
|
503
755
|
}
|
|
504
756
|
|
|
505
757
|
// src/commands/logout.tsx
|
|
506
|
-
import React5, { useEffect as
|
|
758
|
+
import React5, { useEffect as useEffect5, useState as useState5 } from "react";
|
|
507
759
|
import { Box as Box6, useApp as useApp3 } from "ink";
|
|
508
760
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
509
761
|
function LogoutUI() {
|
|
510
762
|
const { exit } = useApp3();
|
|
511
763
|
const [status, setStatus] = useState5("loading");
|
|
512
764
|
const [message, setMessage] = useState5("");
|
|
513
|
-
|
|
765
|
+
useEffect5(() => {
|
|
514
766
|
const creds = readCredentials();
|
|
515
767
|
if (!creds?.access_token) {
|
|
516
768
|
setMessage("Not logged in.");
|
|
@@ -607,14 +859,14 @@ function registerWhoami(program2) {
|
|
|
607
859
|
}
|
|
608
860
|
|
|
609
861
|
// src/commands/add.tsx
|
|
610
|
-
import React7, { useEffect as
|
|
862
|
+
import React7, { useEffect as useEffect6, useState as useState6 } from "react";
|
|
611
863
|
import { Box as Box8, Text as Text8, useApp as useApp4 } from "ink";
|
|
612
|
-
import { v4 as
|
|
864
|
+
import { v4 as uuidv42 } from "uuid";
|
|
613
865
|
|
|
614
866
|
// src/lib/storage.ts
|
|
615
867
|
import { join as join2 } from "path";
|
|
616
868
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
617
|
-
import { v4 as
|
|
869
|
+
import { v4 as uuidv4 } from "uuid";
|
|
618
870
|
var STORE_FILE = join2(TASKAIR_DIR, "tasks.json");
|
|
619
871
|
function defaultStore() {
|
|
620
872
|
return { tasks: [], sync_queue: [] };
|
|
@@ -677,7 +929,7 @@ function deleteTask(id) {
|
|
|
677
929
|
}
|
|
678
930
|
function queueOperation(store, operation, taskId) {
|
|
679
931
|
store.sync_queue.push({
|
|
680
|
-
id:
|
|
932
|
+
id: uuidv4(),
|
|
681
933
|
operation,
|
|
682
934
|
task_id: taskId,
|
|
683
935
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -741,11 +993,11 @@ function AddUI({
|
|
|
741
993
|
const [status, setStatus] = useState6("working");
|
|
742
994
|
const [taskId, setTaskId] = useState6("");
|
|
743
995
|
const [errorMsg, setErrorMsg] = useState6("");
|
|
744
|
-
|
|
996
|
+
useEffect6(() => {
|
|
745
997
|
try {
|
|
746
998
|
const auth = requireAuth();
|
|
747
999
|
const task = {
|
|
748
|
-
id:
|
|
1000
|
+
id: uuidv42(),
|
|
749
1001
|
description,
|
|
750
1002
|
priority,
|
|
751
1003
|
status: "pending",
|
|
@@ -832,7 +1084,7 @@ function registerAdd(program2) {
|
|
|
832
1084
|
}
|
|
833
1085
|
|
|
834
1086
|
// src/commands/list.tsx
|
|
835
|
-
import React9, { useEffect as
|
|
1087
|
+
import React9, { useEffect as useEffect7, useState as useState7 } from "react";
|
|
836
1088
|
import { Box as Box10, Text as Text10, useApp as useApp5 } from "ink";
|
|
837
1089
|
|
|
838
1090
|
// src/components/TaskTable.tsx
|
|
@@ -996,7 +1248,7 @@ import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
|
996
1248
|
function ListUI({ filter, format }) {
|
|
997
1249
|
const { exit } = useApp5();
|
|
998
1250
|
const [tasks, setTasks] = useState7([]);
|
|
999
|
-
|
|
1251
|
+
useEffect7(() => {
|
|
1000
1252
|
const results = filterTasks(filter);
|
|
1001
1253
|
results.sort((a, b) => {
|
|
1002
1254
|
const pOrder = { high: 0, medium: 1, low: 2 };
|
|
@@ -1064,15 +1316,15 @@ function registerList(program2) {
|
|
|
1064
1316
|
}
|
|
1065
1317
|
|
|
1066
1318
|
// src/commands/done.tsx
|
|
1067
|
-
import React10, { useEffect as
|
|
1319
|
+
import React10, { useEffect as useEffect8, useState as useState8 } from "react";
|
|
1068
1320
|
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";
|
|
1321
|
+
import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1070
1322
|
function DoneUI({ id, note }) {
|
|
1071
1323
|
const { exit } = useApp6();
|
|
1072
1324
|
const [status, setStatus] = useState8("success");
|
|
1073
1325
|
const [message, setMessage] = useState8("");
|
|
1074
1326
|
const [description, setDescription] = useState8("");
|
|
1075
|
-
|
|
1327
|
+
useEffect8(() => {
|
|
1076
1328
|
const task = getTask(id);
|
|
1077
1329
|
if (!task) {
|
|
1078
1330
|
setMessage(`No task found with ID starting with "${id}"`);
|
|
@@ -1100,7 +1352,7 @@ function DoneUI({ id, note }) {
|
|
|
1100
1352
|
}, []);
|
|
1101
1353
|
return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", padding: 1, children: [
|
|
1102
1354
|
/* @__PURE__ */ jsx11(MiniHeader, {}),
|
|
1103
|
-
/* @__PURE__ */ jsx11(Box11, { marginTop: 1, flexDirection: "column", children: status === "success" ? /* @__PURE__ */ jsxs11(
|
|
1355
|
+
/* @__PURE__ */ jsx11(Box11, { marginTop: 1, flexDirection: "column", children: status === "success" ? /* @__PURE__ */ jsxs11(Fragment2, { children: [
|
|
1104
1356
|
/* @__PURE__ */ jsx11(StarBurst, { label: message, color: "green" }),
|
|
1105
1357
|
description && /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, children: [
|
|
1106
1358
|
/* @__PURE__ */ jsx11(Text11, { color: "gray", children: " " }),
|
|
@@ -1122,7 +1374,7 @@ function registerDone(program2) {
|
|
|
1122
1374
|
}
|
|
1123
1375
|
|
|
1124
1376
|
// src/commands/remove.tsx
|
|
1125
|
-
import React11, { useEffect as
|
|
1377
|
+
import React11, { useEffect as useEffect9, useState as useState9 } from "react";
|
|
1126
1378
|
import { Box as Box12, Text as Text12, useApp as useApp7, useInput as useInput3 } from "ink";
|
|
1127
1379
|
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1128
1380
|
function RemoveUI({ id, force }) {
|
|
@@ -1130,7 +1382,7 @@ function RemoveUI({ id, force }) {
|
|
|
1130
1382
|
const [stage, setStage] = useState9("confirm");
|
|
1131
1383
|
const [message, setMessage] = useState9("");
|
|
1132
1384
|
const task = getTask(id);
|
|
1133
|
-
|
|
1385
|
+
useEffect9(() => {
|
|
1134
1386
|
if (!task) {
|
|
1135
1387
|
setMessage(`No task found with ID starting with "${id}"`);
|
|
1136
1388
|
setStage("error");
|
|
@@ -1203,15 +1455,15 @@ function registerRemove(program2) {
|
|
|
1203
1455
|
}
|
|
1204
1456
|
|
|
1205
1457
|
// src/commands/edit.tsx
|
|
1206
|
-
import React12, { useState as useState10, useEffect as
|
|
1458
|
+
import React12, { useState as useState10, useEffect as useEffect10 } from "react";
|
|
1207
1459
|
import { Box as Box13, Text as Text13, useApp as useApp8 } from "ink";
|
|
1208
|
-
import { Fragment as
|
|
1460
|
+
import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1209
1461
|
function EditUI({ id, updates }) {
|
|
1210
1462
|
const { exit } = useApp8();
|
|
1211
1463
|
const [status, setStatus] = useState10("success");
|
|
1212
1464
|
const [message, setMessage] = useState10("");
|
|
1213
1465
|
const [changes, setChanges] = useState10([]);
|
|
1214
|
-
|
|
1466
|
+
useEffect10(() => {
|
|
1215
1467
|
const task = getTask(id);
|
|
1216
1468
|
if (!task) {
|
|
1217
1469
|
setMessage(`No task found with ID starting with "${id}"`);
|
|
@@ -1264,7 +1516,7 @@ function EditUI({ id, updates }) {
|
|
|
1264
1516
|
}, []);
|
|
1265
1517
|
return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", padding: 1, children: [
|
|
1266
1518
|
/* @__PURE__ */ jsx13(MiniHeader, {}),
|
|
1267
|
-
/* @__PURE__ */ jsx13(Box13, { marginTop: 1, flexDirection: "column", children: status === "success" ? /* @__PURE__ */ jsxs13(
|
|
1519
|
+
/* @__PURE__ */ jsx13(Box13, { marginTop: 1, flexDirection: "column", children: status === "success" ? /* @__PURE__ */ jsxs13(Fragment3, { children: [
|
|
1268
1520
|
/* @__PURE__ */ jsx13(StarBurst, { label: message, color: "magenta" }),
|
|
1269
1521
|
changes.map((c) => /* @__PURE__ */ jsxs13(Box13, { marginTop: 0, children: [
|
|
1270
1522
|
/* @__PURE__ */ jsx13(Text13, { color: "gray", children: " \u21B3 " }),
|
|
@@ -1281,9 +1533,9 @@ function registerEdit(program2) {
|
|
|
1281
1533
|
}
|
|
1282
1534
|
|
|
1283
1535
|
// src/commands/stats.tsx
|
|
1284
|
-
import React13, { useEffect as
|
|
1536
|
+
import React13, { useEffect as useEffect11 } from "react";
|
|
1285
1537
|
import { Box as Box14, Text as Text14, useApp as useApp9 } from "ink";
|
|
1286
|
-
import { Fragment as
|
|
1538
|
+
import { Fragment as Fragment4, jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1287
1539
|
function ProgressBar({
|
|
1288
1540
|
value,
|
|
1289
1541
|
max,
|
|
@@ -1302,7 +1554,7 @@ function ProgressBar({
|
|
|
1302
1554
|
function StatsUI() {
|
|
1303
1555
|
const { exit } = useApp9();
|
|
1304
1556
|
const stats = computeStats();
|
|
1305
|
-
|
|
1557
|
+
useEffect11(() => {
|
|
1306
1558
|
setTimeout(() => exit(), 100);
|
|
1307
1559
|
}, []);
|
|
1308
1560
|
const rows = [
|
|
@@ -1338,7 +1590,7 @@ function StatsUI() {
|
|
|
1338
1590
|
row.label.padEnd(16)
|
|
1339
1591
|
] }),
|
|
1340
1592
|
/* @__PURE__ */ jsx14(Text14, { color: row.color, bold: true, children: String(row.value).padStart(4) }),
|
|
1341
|
-
stats.total > 0 && /* @__PURE__ */ jsxs14(
|
|
1593
|
+
stats.total > 0 && /* @__PURE__ */ jsxs14(Fragment4, { children: [
|
|
1342
1594
|
/* @__PURE__ */ jsx14(Text14, { color: "gray", children: " " }),
|
|
1343
1595
|
/* @__PURE__ */ jsx14(
|
|
1344
1596
|
ProgressBar,
|
|
@@ -1360,7 +1612,7 @@ function StatsUI() {
|
|
|
1360
1612
|
row.label.padEnd(20)
|
|
1361
1613
|
] }),
|
|
1362
1614
|
/* @__PURE__ */ jsx14(Text14, { color: row.color, bold: true, children: String(row.value).padStart(4) }),
|
|
1363
|
-
stats.total > 0 && /* @__PURE__ */ jsxs14(
|
|
1615
|
+
stats.total > 0 && /* @__PURE__ */ jsxs14(Fragment4, { children: [
|
|
1364
1616
|
/* @__PURE__ */ jsx14(Text14, { color: "gray", children: " " }),
|
|
1365
1617
|
/* @__PURE__ */ jsx14(
|
|
1366
1618
|
ProgressBar,
|
|
@@ -1398,7 +1650,7 @@ function registerStats(program2) {
|
|
|
1398
1650
|
}
|
|
1399
1651
|
|
|
1400
1652
|
// src/commands/sync.tsx
|
|
1401
|
-
import React14, { useState as useState11, useEffect as
|
|
1653
|
+
import React14, { useState as useState11, useEffect as useEffect12 } from "react";
|
|
1402
1654
|
import { Box as Box15, Text as Text15, useApp as useApp10 } from "ink";
|
|
1403
1655
|
|
|
1404
1656
|
// src/lib/crypto.ts
|
|
@@ -1446,13 +1698,13 @@ function checksum(plaintext) {
|
|
|
1446
1698
|
}
|
|
1447
1699
|
|
|
1448
1700
|
// src/commands/sync.tsx
|
|
1449
|
-
import { Fragment as
|
|
1701
|
+
import { Fragment as Fragment5, jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1450
1702
|
function SyncUI({ dryRun, password }) {
|
|
1451
1703
|
const { exit } = useApp10();
|
|
1452
1704
|
const [status, setStatus] = useState11("loading");
|
|
1453
1705
|
const [message, setMessage] = useState11("");
|
|
1454
1706
|
const [details, setDetails] = useState11([]);
|
|
1455
|
-
|
|
1707
|
+
useEffect12(() => {
|
|
1456
1708
|
async function doSync() {
|
|
1457
1709
|
try {
|
|
1458
1710
|
const auth = requireAuth();
|
|
@@ -1477,13 +1729,14 @@ function SyncUI({ dryRun, password }) {
|
|
|
1477
1729
|
setTimeout(() => exit(), 100);
|
|
1478
1730
|
return;
|
|
1479
1731
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
1732
|
+
const syncPassword = password || auth.password;
|
|
1733
|
+
if (!syncPassword) {
|
|
1734
|
+
setMessage("Encryption password required. Use --password flag or configure it.");
|
|
1482
1735
|
setStatus("error");
|
|
1483
1736
|
setTimeout(() => exit(new Error("Password required")), 1200);
|
|
1484
1737
|
return;
|
|
1485
1738
|
}
|
|
1486
|
-
const blob = encrypt(plaintext,
|
|
1739
|
+
const blob = encrypt(plaintext, syncPassword);
|
|
1487
1740
|
const res = await apiUploadSync(
|
|
1488
1741
|
auth.apiUrl,
|
|
1489
1742
|
auth.accessToken,
|
|
@@ -1500,6 +1753,15 @@ function SyncUI({ dryRun, password }) {
|
|
|
1500
1753
|
]);
|
|
1501
1754
|
setStatus("success");
|
|
1502
1755
|
setTimeout(() => exit(), 1500);
|
|
1756
|
+
} else if (res.error?.code === "NETWORK_ERROR") {
|
|
1757
|
+
setMessage("System is offline. Sync is queued!");
|
|
1758
|
+
setDetails([
|
|
1759
|
+
"Tasks stored locally in cache (tasks.json) at zero cost",
|
|
1760
|
+
`Sync queue contains ${tasks.length} pending task(s)`,
|
|
1761
|
+
"Will upload to cloud & Google Drive once internet is restored."
|
|
1762
|
+
]);
|
|
1763
|
+
setStatus("offline");
|
|
1764
|
+
setTimeout(() => exit(), 3e3);
|
|
1503
1765
|
} else {
|
|
1504
1766
|
setMessage(res.error?.message ?? "Sync failed");
|
|
1505
1767
|
setStatus("error");
|
|
@@ -1517,7 +1779,7 @@ function SyncUI({ dryRun, password }) {
|
|
|
1517
1779
|
/* @__PURE__ */ jsx15(MiniHeader, {}),
|
|
1518
1780
|
/* @__PURE__ */ jsxs15(Box15, { marginTop: 1, flexDirection: "column", children: [
|
|
1519
1781
|
status === "loading" && /* @__PURE__ */ jsx15(Spinner, { label: "Syncing to cloud (E2E encrypted)\u2026", type: "orbit", color: "magenta" }),
|
|
1520
|
-
status === "success" && /* @__PURE__ */ jsxs15(
|
|
1782
|
+
status === "success" && /* @__PURE__ */ jsxs15(Fragment5, { children: [
|
|
1521
1783
|
/* @__PURE__ */ jsx15(StarBurst, { label: message, color: "cyan" }),
|
|
1522
1784
|
details.map((d) => /* @__PURE__ */ jsxs15(Box15, { children: [
|
|
1523
1785
|
/* @__PURE__ */ jsx15(Text15, { color: "gray", children: " \xB7 " }),
|
|
@@ -1525,6 +1787,13 @@ function SyncUI({ dryRun, password }) {
|
|
|
1525
1787
|
] }, d))
|
|
1526
1788
|
] }),
|
|
1527
1789
|
status === "error" && /* @__PURE__ */ jsx15(StatusBadge, { type: "error", message }),
|
|
1790
|
+
status === "offline" && /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
|
|
1791
|
+
/* @__PURE__ */ jsx15(StatusBadge, { type: "warn", message }),
|
|
1792
|
+
details.map((d) => /* @__PURE__ */ jsxs15(Box15, { children: [
|
|
1793
|
+
/* @__PURE__ */ jsx15(Text15, { color: "gray", children: " \xB7 " }),
|
|
1794
|
+
/* @__PURE__ */ jsx15(Text15, { color: "yellow", dimColor: true, children: d })
|
|
1795
|
+
] }, d))
|
|
1796
|
+
] }),
|
|
1528
1797
|
status === "dry-run" && /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
|
|
1529
1798
|
/* @__PURE__ */ jsx15(StatusBadge, { type: "info", message: "Dry run \u2014 sync preview:" }),
|
|
1530
1799
|
details.map((d) => /* @__PURE__ */ jsxs15(Box15, { children: [
|
|
@@ -1603,14 +1872,113 @@ function registerExport(program2) {
|
|
|
1603
1872
|
});
|
|
1604
1873
|
}
|
|
1605
1874
|
|
|
1875
|
+
// src/commands/upgrade.tsx
|
|
1876
|
+
import React15, { useState as useState12, useEffect as useEffect13 } from "react";
|
|
1877
|
+
import { Box as Box16, Text as Text16 } from "ink";
|
|
1878
|
+
import { exec as exec2 } from "child_process";
|
|
1879
|
+
|
|
1880
|
+
// src/lib/version.ts
|
|
1881
|
+
var CLI_VERSION = "1.0.5";
|
|
1882
|
+
|
|
1883
|
+
// src/commands/upgrade.tsx
|
|
1884
|
+
import { jsx as jsx16, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1885
|
+
function isOutdated(current, latest) {
|
|
1886
|
+
const cParts = current.split(".").map(Number);
|
|
1887
|
+
const lParts = latest.split(".").map(Number);
|
|
1888
|
+
for (let i = 0; i < 3; i++) {
|
|
1889
|
+
const c = cParts[i] ?? 0;
|
|
1890
|
+
const l = lParts[i] ?? 0;
|
|
1891
|
+
if (l > c) return true;
|
|
1892
|
+
if (c > l) return false;
|
|
1893
|
+
}
|
|
1894
|
+
return false;
|
|
1895
|
+
}
|
|
1896
|
+
function UpgradeUI() {
|
|
1897
|
+
const [status, setStatus] = useState12("checking");
|
|
1898
|
+
const [latestVersion, setLatestVersion] = useState12("");
|
|
1899
|
+
const [errorMsg, setErrorMsg] = useState12("");
|
|
1900
|
+
useEffect13(() => {
|
|
1901
|
+
async function checkAndUpgrade() {
|
|
1902
|
+
try {
|
|
1903
|
+
const res = await fetch("https://registry.npmjs.org/taskair-cli/latest", {
|
|
1904
|
+
signal: AbortSignal.timeout(5e3)
|
|
1905
|
+
// 5s timeout
|
|
1906
|
+
});
|
|
1907
|
+
if (!res.ok) {
|
|
1908
|
+
throw new Error(`Failed to fetch latest version from npm registry. Status: ${res.status}`);
|
|
1909
|
+
}
|
|
1910
|
+
const data = await res.json();
|
|
1911
|
+
const latest = data.version || "1.0.4";
|
|
1912
|
+
setLatestVersion(latest);
|
|
1913
|
+
if (isOutdated(CLI_VERSION, latest)) {
|
|
1914
|
+
setStatus("upgrading");
|
|
1915
|
+
exec2("npm install -g taskair-cli", (error, stdout, stderr) => {
|
|
1916
|
+
if (error) {
|
|
1917
|
+
setErrorMsg(error.message || stderr || "Failed to install update");
|
|
1918
|
+
setStatus("error");
|
|
1919
|
+
} else {
|
|
1920
|
+
setStatus("success");
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
} else {
|
|
1924
|
+
setStatus("up-to-date");
|
|
1925
|
+
}
|
|
1926
|
+
} catch (err) {
|
|
1927
|
+
setErrorMsg(err.message || "Network error checking for updates.");
|
|
1928
|
+
setStatus("error");
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
checkAndUpgrade();
|
|
1932
|
+
}, []);
|
|
1933
|
+
return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", padding: 1, children: [
|
|
1934
|
+
/* @__PURE__ */ jsx16(MiniHeader, {}),
|
|
1935
|
+
/* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", borderStyle: "round", borderColor: "#7B61FF", paddingX: 2, paddingY: 1, marginTop: 1, children: [
|
|
1936
|
+
/* @__PURE__ */ jsx16(Box16, { marginBottom: 1, children: /* @__PURE__ */ jsx16(Text16, { color: "cyan", bold: true, children: "\u2726 TaskAir Upgrade" }) }),
|
|
1937
|
+
status === "checking" && /* @__PURE__ */ jsx16(Spinner, { label: `Checking npm registry for updates... (Current: v${CLI_VERSION})`, type: "orbit" }),
|
|
1938
|
+
status === "upgrading" && /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
|
|
1939
|
+
/* @__PURE__ */ jsxs16(Text16, { color: "yellow", bold: true, children: [
|
|
1940
|
+
"\u2726 New version detected: v",
|
|
1941
|
+
CLI_VERSION,
|
|
1942
|
+
" \u2192 v",
|
|
1943
|
+
latestVersion
|
|
1944
|
+
] }),
|
|
1945
|
+
/* @__PURE__ */ jsx16(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx16(Spinner, { label: "Upgrading globally via npm install -g taskair-cli...", type: "star", color: "yellow" }) })
|
|
1946
|
+
] }),
|
|
1947
|
+
status === "success" && /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
|
|
1948
|
+
/* @__PURE__ */ jsx16(StatusBadge, { type: "success", message: `TaskAir CLI successfully upgraded to v${latestVersion}!` }),
|
|
1949
|
+
/* @__PURE__ */ jsx16(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text16, { color: "gray", children: "You are now running the latest space-grade build." }) })
|
|
1950
|
+
] }),
|
|
1951
|
+
status === "up-to-date" && /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
|
|
1952
|
+
/* @__PURE__ */ jsx16(StatusBadge, { type: "success", message: `TaskAir CLI is already up to date (v${CLI_VERSION}).` }),
|
|
1953
|
+
/* @__PURE__ */ jsx16(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx16(Text16, { color: "gray", children: "No upgrade required at this time. Blast off! \u{1F680}" }) })
|
|
1954
|
+
] }),
|
|
1955
|
+
status === "error" && /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", children: [
|
|
1956
|
+
/* @__PURE__ */ jsx16(StatusBadge, { type: "error", message: `Upgrade failed: ${errorMsg}` }),
|
|
1957
|
+
/* @__PURE__ */ jsxs16(Box16, { marginTop: 1, flexDirection: "column", children: [
|
|
1958
|
+
/* @__PURE__ */ jsx16(Text16, { color: "gray", children: "You can try upgrading manually by running:" }),
|
|
1959
|
+
/* @__PURE__ */ jsx16(Text16, { color: "cyan", bold: true, children: " npm install -g taskair-cli" }),
|
|
1960
|
+
/* @__PURE__ */ jsx16(Text16, { color: "gray", marginTop: 1, children: "If permission errors occur, run with administrative rights (sudo)." })
|
|
1961
|
+
] })
|
|
1962
|
+
] })
|
|
1963
|
+
] })
|
|
1964
|
+
] });
|
|
1965
|
+
}
|
|
1966
|
+
function registerUpgrade(program2) {
|
|
1967
|
+
program2.command("upgrade").description("Upgrade TaskAir CLI to the latest version").action(async () => {
|
|
1968
|
+
const { render } = await import("ink");
|
|
1969
|
+
render(React15.createElement(UpgradeUI));
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1606
1973
|
// src/index.ts
|
|
1607
1974
|
program.name("taskair").description(
|
|
1608
1975
|
"\u2726 Space-themed task management with E2E encryption \xB7 AI-native \xB7 Privacy-first"
|
|
1609
|
-
).version(
|
|
1610
|
-
|
|
1976
|
+
).version(CLI_VERSION, "-v, --version", "Output the current version").helpOption("-h, --help", "Display help information");
|
|
1977
|
+
registerConfig(program);
|
|
1611
1978
|
registerLogin(program);
|
|
1612
1979
|
registerLogout(program);
|
|
1613
1980
|
registerWhoami(program);
|
|
1981
|
+
registerUpgrade(program);
|
|
1614
1982
|
registerAdd(program);
|
|
1615
1983
|
registerList(program);
|
|
1616
1984
|
registerDone(program);
|