taskair-cli 1.0.2 → 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.
- package/README.md +7 -15
- package/dist/index.js +422 -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,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/
|
|
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: #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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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." })
|
|
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
|
-
|
|
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."
|
|
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
|
-
|
|
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
|
|
308
|
-
program2.command("configure").description("
|
|
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
|
-
|
|
313
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
899
|
+
useEffect6(() => {
|
|
745
900
|
try {
|
|
746
901
|
const auth = requireAuth();
|
|
747
902
|
const task = {
|
|
748
|
-
id:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1481
|
-
|
|
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,
|
|
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(
|
|
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.
|
|
1610
|
-
|
|
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);
|