taskair-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1635 -0
- package/package.json +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1635 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/configure.tsx
|
|
7
|
+
import React2, { useState as useState2 } from "react";
|
|
8
|
+
import { Box as Box3, Text as Text3, useInput, useApp } from "ink";
|
|
9
|
+
|
|
10
|
+
// src/lib/auth.ts
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync } from "fs";
|
|
14
|
+
var TASKAIR_DIR = join(homedir(), ".taskair");
|
|
15
|
+
var CREDENTIALS_FILE = join(TASKAIR_DIR, "credentials");
|
|
16
|
+
function ensureTaskairDir() {
|
|
17
|
+
if (!existsSync(TASKAIR_DIR)) {
|
|
18
|
+
mkdirSync(TASKAIR_DIR, { recursive: true, mode: 448 });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function parseIni(content) {
|
|
22
|
+
const result = {};
|
|
23
|
+
for (const line of content.split("\n")) {
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (trimmed.startsWith("#") || trimmed.startsWith("[") || !trimmed) continue;
|
|
26
|
+
const eqIdx = trimmed.indexOf("=");
|
|
27
|
+
if (eqIdx === -1) continue;
|
|
28
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
29
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
30
|
+
result[key] = value;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function toIni(data) {
|
|
35
|
+
const lines = ["[default]"];
|
|
36
|
+
for (const [key, value] of Object.entries(data)) {
|
|
37
|
+
if (value !== void 0 && value !== "") {
|
|
38
|
+
lines.push(`${key} = ${value}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return lines.join("\n") + "\n";
|
|
42
|
+
}
|
|
43
|
+
function readCredentials() {
|
|
44
|
+
ensureTaskairDir();
|
|
45
|
+
if (!existsSync(CREDENTIALS_FILE)) return null;
|
|
46
|
+
try {
|
|
47
|
+
const content = readFileSync(CREDENTIALS_FILE, "utf8");
|
|
48
|
+
const raw = parseIni(content);
|
|
49
|
+
return {
|
|
50
|
+
api_url: raw.api_url || "http://localhost:3001",
|
|
51
|
+
email: raw.email || "",
|
|
52
|
+
access_token: raw.access_token || void 0,
|
|
53
|
+
refresh_token: raw.refresh_token || void 0,
|
|
54
|
+
expires_at: raw.expires_at ? parseInt(raw.expires_at) : void 0,
|
|
55
|
+
device_id: raw.device_id || void 0
|
|
56
|
+
};
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function writeCredentials(creds) {
|
|
62
|
+
ensureTaskairDir();
|
|
63
|
+
const data = {
|
|
64
|
+
api_url: creds.api_url,
|
|
65
|
+
email: creds.email,
|
|
66
|
+
access_token: creds.access_token,
|
|
67
|
+
refresh_token: creds.refresh_token,
|
|
68
|
+
expires_at: creds.expires_at,
|
|
69
|
+
device_id: creds.device_id
|
|
70
|
+
};
|
|
71
|
+
writeFileSync(CREDENTIALS_FILE, toIni(data), { encoding: "utf8" });
|
|
72
|
+
try {
|
|
73
|
+
chmodSync(CREDENTIALS_FILE, 384);
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function clearTokens() {
|
|
78
|
+
const creds = readCredentials();
|
|
79
|
+
if (!creds) return;
|
|
80
|
+
writeCredentials({
|
|
81
|
+
...creds,
|
|
82
|
+
access_token: void 0,
|
|
83
|
+
refresh_token: void 0,
|
|
84
|
+
expires_at: void 0,
|
|
85
|
+
device_id: void 0
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function requireAuth() {
|
|
89
|
+
const creds = readCredentials();
|
|
90
|
+
if (!creds || !creds.access_token) {
|
|
91
|
+
throw new Error("Not authenticated. Run `taskair login` first.");
|
|
92
|
+
}
|
|
93
|
+
if (creds.expires_at && Date.now() / 1e3 > creds.expires_at) {
|
|
94
|
+
throw new Error("Session expired. Run `taskair login` to re-authenticate.");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
email: creds.email,
|
|
98
|
+
deviceId: creds.device_id ?? "default",
|
|
99
|
+
accessToken: creds.access_token,
|
|
100
|
+
apiUrl: creds.api_url,
|
|
101
|
+
expiresAt: creds.expires_at ?? 0
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/components/StarBurst.tsx
|
|
106
|
+
import { useState, useEffect } from "react";
|
|
107
|
+
import { Box, Text } from "ink";
|
|
108
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
109
|
+
var STARS = ["\u2726", "\u2727", "\u2605", "\u2606", "\u2738", "\u2739", "\u273A", "\u273B", "\u273C", "\u273D"];
|
|
110
|
+
function randomStar() {
|
|
111
|
+
return STARS[Math.floor(Math.random() * STARS.length)] ?? "\u2726";
|
|
112
|
+
}
|
|
113
|
+
function randomStars(count) {
|
|
114
|
+
return Array.from({ length: count }, () => randomStar()).join(" ");
|
|
115
|
+
}
|
|
116
|
+
function StarBurst({
|
|
117
|
+
label,
|
|
118
|
+
color = "cyan"
|
|
119
|
+
}) {
|
|
120
|
+
const [frame, setFrame] = useState(0);
|
|
121
|
+
const [done, setDone] = useState(false);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
const timer = setInterval(() => {
|
|
124
|
+
setFrame((f) => {
|
|
125
|
+
if (f >= 5) {
|
|
126
|
+
clearInterval(timer);
|
|
127
|
+
setDone(true);
|
|
128
|
+
return f;
|
|
129
|
+
}
|
|
130
|
+
return f + 1;
|
|
131
|
+
});
|
|
132
|
+
}, 80);
|
|
133
|
+
return () => clearInterval(timer);
|
|
134
|
+
}, []);
|
|
135
|
+
if (done) {
|
|
136
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
137
|
+
/* @__PURE__ */ jsxs(Text, { color: "green", bold: true, children: [
|
|
138
|
+
"\u2713",
|
|
139
|
+
" "
|
|
140
|
+
] }),
|
|
141
|
+
/* @__PURE__ */ jsx(Text, { color: "white", bold: true, children: label })
|
|
142
|
+
] });
|
|
143
|
+
}
|
|
144
|
+
const count = Math.min(frame + 1, 4);
|
|
145
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Box, { children: [
|
|
146
|
+
/* @__PURE__ */ jsxs(Text, { color, children: [
|
|
147
|
+
randomStars(count),
|
|
148
|
+
" "
|
|
149
|
+
] }),
|
|
150
|
+
/* @__PURE__ */ jsx(Text, { color: "white", bold: true, children: label }),
|
|
151
|
+
/* @__PURE__ */ jsxs(Text, { color, children: [
|
|
152
|
+
" ",
|
|
153
|
+
randomStars(count)
|
|
154
|
+
] })
|
|
155
|
+
] }) });
|
|
156
|
+
}
|
|
157
|
+
function StatusBadge({
|
|
158
|
+
type,
|
|
159
|
+
message
|
|
160
|
+
}) {
|
|
161
|
+
const config = {
|
|
162
|
+
success: { icon: "\u2713", color: "green" },
|
|
163
|
+
error: { icon: "\u2717", color: "red" },
|
|
164
|
+
info: { icon: "\u2192", color: "cyan" },
|
|
165
|
+
warn: { icon: "\u26A0", color: "yellow" }
|
|
166
|
+
}[type];
|
|
167
|
+
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
168
|
+
/* @__PURE__ */ jsxs(Text, { color: config.color, bold: true, children: [
|
|
169
|
+
config.icon,
|
|
170
|
+
" "
|
|
171
|
+
] }),
|
|
172
|
+
/* @__PURE__ */ jsx(Text, { color: type === "error" ? "red" : "white", children: message })
|
|
173
|
+
] });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/components/AsciiHeader.tsx
|
|
177
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
178
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
179
|
+
var LOGO = [
|
|
180
|
+
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
181
|
+
"\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
182
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
183
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
184
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
185
|
+
" \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
|
|
186
|
+
];
|
|
187
|
+
var TAGLINE = " \u2726 Space-themed \xB7 Privacy-first \xB7 AI-native task management \u2726";
|
|
188
|
+
function AsciiHeader() {
|
|
189
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
|
|
190
|
+
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: LOGO.map((line, i) => /* @__PURE__ */ jsx2(Text2, { color: "magenta", bold: true, children: line }, i)) }),
|
|
191
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", dimColor: true, children: TAGLINE }),
|
|
192
|
+
/* @__PURE__ */ jsx2(Box2, { marginTop: 0, children: /* @__PURE__ */ jsx2(Text2, { color: "#7B61FF", children: "\u2500".repeat(60) }) })
|
|
193
|
+
] });
|
|
194
|
+
}
|
|
195
|
+
function MiniHeader() {
|
|
196
|
+
return /* @__PURE__ */ jsxs2(Box2, { marginBottom: 1, children: [
|
|
197
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "magenta", bold: true, children: [
|
|
198
|
+
"\u2726",
|
|
199
|
+
" "
|
|
200
|
+
] }),
|
|
201
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "TaskAir" }),
|
|
202
|
+
/* @__PURE__ */ jsx2(Text2, { color: "#7B61FF", children: " \u2014 Space-grade task management" })
|
|
203
|
+
] });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/commands/configure.tsx
|
|
207
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
208
|
+
function ConfigureUI({ initial }) {
|
|
209
|
+
const { exit } = useApp();
|
|
210
|
+
const fields = ["apiUrl", "email", "password"];
|
|
211
|
+
const [fieldIndex, setFieldIndex] = useState2(0);
|
|
212
|
+
const [values, setValues] = useState2({
|
|
213
|
+
apiUrl: initial.apiUrl,
|
|
214
|
+
email: initial.email,
|
|
215
|
+
password: ""
|
|
216
|
+
});
|
|
217
|
+
const [currentInput, setCurrentInput] = useState2(
|
|
218
|
+
initial.apiUrl
|
|
219
|
+
);
|
|
220
|
+
const [done, setDone] = useState2(false);
|
|
221
|
+
const [error, setError] = useState2("");
|
|
222
|
+
const labels = {
|
|
223
|
+
apiUrl: "API URL",
|
|
224
|
+
email: "Email address",
|
|
225
|
+
password: "Master password (used for E2E encryption)"
|
|
226
|
+
};
|
|
227
|
+
useInput((input, key) => {
|
|
228
|
+
if (done) return;
|
|
229
|
+
if (key.return) {
|
|
230
|
+
const field = fields[fieldIndex];
|
|
231
|
+
if (!field) return;
|
|
232
|
+
if (!currentInput.trim()) {
|
|
233
|
+
setError(`${labels[field]} cannot be empty`);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
setError("");
|
|
237
|
+
const updated = { ...values, [field]: currentInput.trim() };
|
|
238
|
+
setValues(updated);
|
|
239
|
+
if (fieldIndex < fields.length - 1) {
|
|
240
|
+
setFieldIndex(fieldIndex + 1);
|
|
241
|
+
const nextField = fields[fieldIndex + 1];
|
|
242
|
+
setCurrentInput(updated[nextField ?? "password"] ?? "");
|
|
243
|
+
} else {
|
|
244
|
+
writeCredentials({
|
|
245
|
+
api_url: updated.apiUrl,
|
|
246
|
+
email: updated.email
|
|
247
|
+
});
|
|
248
|
+
setDone(true);
|
|
249
|
+
setTimeout(() => exit(), 500);
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (key.backspace || key.delete) {
|
|
254
|
+
setCurrentInput((v) => v.slice(0, -1));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (key.escape) {
|
|
258
|
+
exit();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
setCurrentInput((v) => v + input);
|
|
262
|
+
});
|
|
263
|
+
const currentField = fields[fieldIndex];
|
|
264
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", padding: 1, children: [
|
|
265
|
+
/* @__PURE__ */ jsx3(AsciiHeader, {}),
|
|
266
|
+
/* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginBottom: 1, children: [
|
|
267
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "\u2726 Configure TaskAir" }),
|
|
268
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", children: "Set up your credentials. Press Enter to confirm each field." })
|
|
269
|
+
] }),
|
|
270
|
+
fields.map((field, i) => {
|
|
271
|
+
const isActive = i === fieldIndex && !done;
|
|
272
|
+
const isDone = i < fieldIndex || done;
|
|
273
|
+
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", marginBottom: 0, children: /* @__PURE__ */ jsxs3(Box3, { children: [
|
|
274
|
+
/* @__PURE__ */ jsx3(Text3, { color: isActive ? "cyan" : isDone ? "green" : "gray", bold: true, children: isDone ? "\u2713 " : isActive ? "\u2192 " : " " }),
|
|
275
|
+
/* @__PURE__ */ jsxs3(Text3, { color: isActive ? "white" : isDone ? "green" : "gray", children: [
|
|
276
|
+
labels[field],
|
|
277
|
+
":",
|
|
278
|
+
" "
|
|
279
|
+
] }),
|
|
280
|
+
isDone && field !== "password" && /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: values[field] }),
|
|
281
|
+
isDone && field === "password" && /* @__PURE__ */ jsx3(Text3, { color: "gray", children: "\u2022".repeat(8) }),
|
|
282
|
+
isActive && /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
|
|
283
|
+
field === "password" ? "\u2022".repeat(currentInput.length) : currentInput,
|
|
284
|
+
/* @__PURE__ */ jsx3(Text3, { color: "white", children: "\u258C" })
|
|
285
|
+
] })
|
|
286
|
+
] }) }, field);
|
|
287
|
+
}),
|
|
288
|
+
error && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(StatusBadge, { type: "error", message: error }) }),
|
|
289
|
+
done && /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
|
|
290
|
+
/* @__PURE__ */ jsx3(
|
|
291
|
+
StatusBadge,
|
|
292
|
+
{
|
|
293
|
+
type: "success",
|
|
294
|
+
message: "Configuration saved to ~/.taskair/credentials"
|
|
295
|
+
}
|
|
296
|
+
),
|
|
297
|
+
/* @__PURE__ */ jsxs3(Text3, { color: "gray", dimColor: true, children: [
|
|
298
|
+
"Run",
|
|
299
|
+
" ",
|
|
300
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "taskair login" }),
|
|
301
|
+
" to authenticate."
|
|
302
|
+
] })
|
|
303
|
+
] }),
|
|
304
|
+
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Press Esc to cancel" }) })
|
|
305
|
+
] });
|
|
306
|
+
}
|
|
307
|
+
function registerConfigure(program2) {
|
|
308
|
+
program2.command("configure").description("Set up API URL, email, and master password").action(async () => {
|
|
309
|
+
const existing = readCredentials();
|
|
310
|
+
const { render } = await import("ink");
|
|
311
|
+
render(
|
|
312
|
+
React2.createElement(ConfigureUI, {
|
|
313
|
+
initial: {
|
|
314
|
+
apiUrl: existing?.api_url ?? "http://localhost:3001",
|
|
315
|
+
email: existing?.email ?? ""
|
|
316
|
+
}
|
|
317
|
+
})
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/commands/login.tsx
|
|
323
|
+
import React4, { useState as useState4, useEffect as useEffect3 } from "react";
|
|
324
|
+
import { Box as Box5, Text as Text5, useInput as useInput2, useApp as useApp2 } from "ink";
|
|
325
|
+
import { v4 as uuidv4 } from "uuid";
|
|
326
|
+
|
|
327
|
+
// src/lib/api.ts
|
|
328
|
+
async function request(apiUrl, path, options = {}) {
|
|
329
|
+
const url = `${apiUrl.replace(/\/$/, "")}${path}`;
|
|
330
|
+
const headers = {
|
|
331
|
+
"Content-Type": "application/json",
|
|
332
|
+
"User-Agent": "taskair-cli/1.0.0"
|
|
333
|
+
};
|
|
334
|
+
if (options.token) {
|
|
335
|
+
headers["Authorization"] = `Bearer ${options.token}`;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const res = await fetch(url, {
|
|
339
|
+
method: options.method ?? "GET",
|
|
340
|
+
headers,
|
|
341
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
342
|
+
});
|
|
343
|
+
const json = await res.json();
|
|
344
|
+
return json;
|
|
345
|
+
} catch (err) {
|
|
346
|
+
const message = err instanceof Error ? err.message : "Network error";
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
error: {
|
|
350
|
+
code: "NETWORK_ERROR",
|
|
351
|
+
message: `Cannot reach API: ${message}`
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function apiLogin(apiUrl, email, password) {
|
|
357
|
+
return request(apiUrl, "/auth/login", {
|
|
358
|
+
method: "POST",
|
|
359
|
+
body: { email, password }
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async function apiLogout(apiUrl, token) {
|
|
363
|
+
return request(apiUrl, "/auth/logout", {
|
|
364
|
+
method: "POST",
|
|
365
|
+
token
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async function apiUploadSync(apiUrl, token, encryptedBlob, chk, deviceId) {
|
|
369
|
+
return request(apiUrl, "/sync/upload", {
|
|
370
|
+
method: "POST",
|
|
371
|
+
token,
|
|
372
|
+
body: { encrypted_blob: encryptedBlob, checksum: chk, device_id: deviceId }
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/components/Spinner.tsx
|
|
377
|
+
import { useState as useState3, useEffect as useEffect2 } from "react";
|
|
378
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
379
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
380
|
+
var ORBIT_FRAMES = ["\u25D0", "\u25D3", "\u25D1", "\u25D2"];
|
|
381
|
+
var STAR_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
382
|
+
function Spinner({
|
|
383
|
+
label = "Loading...",
|
|
384
|
+
type = "orbit",
|
|
385
|
+
color = "cyan"
|
|
386
|
+
}) {
|
|
387
|
+
const frames = type === "orbit" ? ORBIT_FRAMES : STAR_FRAMES;
|
|
388
|
+
const [frame, setFrame] = useState3(0);
|
|
389
|
+
useEffect2(() => {
|
|
390
|
+
const timer = setInterval(() => {
|
|
391
|
+
setFrame((f) => (f + 1) % frames.length);
|
|
392
|
+
}, 80);
|
|
393
|
+
return () => clearInterval(timer);
|
|
394
|
+
}, [frames.length]);
|
|
395
|
+
return /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
396
|
+
/* @__PURE__ */ jsxs4(Text4, { color, bold: true, children: [
|
|
397
|
+
frames[frame],
|
|
398
|
+
" "
|
|
399
|
+
] }),
|
|
400
|
+
/* @__PURE__ */ jsx4(Text4, { color: "white", children: label })
|
|
401
|
+
] });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/commands/login.tsx
|
|
405
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
406
|
+
function LoginUI() {
|
|
407
|
+
const { exit } = useApp2();
|
|
408
|
+
const creds = readCredentials();
|
|
409
|
+
const [email, setEmail] = useState4(creds?.email ?? "");
|
|
410
|
+
const [password, setPassword] = useState4("");
|
|
411
|
+
const [stage, setStage] = useState4("input_email");
|
|
412
|
+
const [currentInput, setCurrentInput] = useState4(creds?.email ?? "");
|
|
413
|
+
const [errorMsg, setErrorMsg] = useState4("");
|
|
414
|
+
useEffect3(() => {
|
|
415
|
+
if (stage === "loading") {
|
|
416
|
+
const apiUrl = creds?.api_url ?? "http://localhost:3001";
|
|
417
|
+
apiLogin(apiUrl, email, password).then((res) => {
|
|
418
|
+
if (res.success && res.data) {
|
|
419
|
+
const deviceId = creds?.device_id ?? uuidv4();
|
|
420
|
+
writeCredentials({
|
|
421
|
+
api_url: apiUrl,
|
|
422
|
+
email,
|
|
423
|
+
access_token: res.data.access_token,
|
|
424
|
+
refresh_token: res.data.refresh_token,
|
|
425
|
+
expires_at: res.data.expires_at,
|
|
426
|
+
device_id: deviceId
|
|
427
|
+
});
|
|
428
|
+
setStage("success");
|
|
429
|
+
setTimeout(() => exit(), 1200);
|
|
430
|
+
} else {
|
|
431
|
+
setErrorMsg(res.error?.message ?? "Login failed");
|
|
432
|
+
setStage("error");
|
|
433
|
+
setTimeout(() => exit(new Error(res.error?.message)), 1500);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}, [stage]);
|
|
438
|
+
useInput2((input, key) => {
|
|
439
|
+
if (stage === "loading" || stage === "success") return;
|
|
440
|
+
if (key.return) {
|
|
441
|
+
if (stage === "input_email") {
|
|
442
|
+
if (!currentInput.trim()) return;
|
|
443
|
+
setEmail(currentInput.trim());
|
|
444
|
+
setCurrentInput("");
|
|
445
|
+
setStage("input_password");
|
|
446
|
+
} else if (stage === "input_password") {
|
|
447
|
+
if (!currentInput.trim()) return;
|
|
448
|
+
setPassword(currentInput.trim());
|
|
449
|
+
setCurrentInput("");
|
|
450
|
+
setStage("loading");
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (key.backspace || key.delete) {
|
|
455
|
+
setCurrentInput((v) => v.slice(0, -1));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (key.escape) {
|
|
459
|
+
exit();
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
setCurrentInput((v) => v + input);
|
|
463
|
+
});
|
|
464
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", padding: 1, children: [
|
|
465
|
+
/* @__PURE__ */ jsx5(MiniHeader, {}),
|
|
466
|
+
/* @__PURE__ */ jsx5(Box5, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "cyan", bold: true, children: "\u2726 Login to TaskAir" }) }),
|
|
467
|
+
/* @__PURE__ */ jsxs5(Box5, { marginBottom: 0, children: [
|
|
468
|
+
/* @__PURE__ */ jsx5(Text5, { color: stage === "input_email" ? "cyan" : "green", bold: true, children: stage !== "input_email" ? "\u2713 " : "\u2192 " }),
|
|
469
|
+
/* @__PURE__ */ jsx5(Text5, { color: "white", children: "Email: " }),
|
|
470
|
+
stage === "input_email" ? /* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
471
|
+
currentInput,
|
|
472
|
+
/* @__PURE__ */ jsx5(Text5, { color: "white", children: "\u258C" })
|
|
473
|
+
] }) : /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: email })
|
|
474
|
+
] }),
|
|
475
|
+
(stage === "input_password" || stage === "loading" || stage === "success" || stage === "error") && /* @__PURE__ */ jsxs5(Box5, { marginBottom: 0, children: [
|
|
476
|
+
/* @__PURE__ */ jsx5(
|
|
477
|
+
Text5,
|
|
478
|
+
{
|
|
479
|
+
color: stage === "input_password" ? "cyan" : stage === "success" ? "green" : "gray",
|
|
480
|
+
bold: true,
|
|
481
|
+
children: stage !== "input_password" ? "\u2713 " : "\u2192 "
|
|
482
|
+
}
|
|
483
|
+
),
|
|
484
|
+
/* @__PURE__ */ jsx5(Text5, { color: "white", children: "Password: " }),
|
|
485
|
+
stage === "input_password" ? /* @__PURE__ */ jsxs5(Text5, { color: "cyan", children: [
|
|
486
|
+
"\u2022".repeat(currentInput.length),
|
|
487
|
+
/* @__PURE__ */ jsx5(Text5, { color: "white", children: "\u258C" })
|
|
488
|
+
] }) : /* @__PURE__ */ jsx5(Text5, { color: "gray", children: "\u2022".repeat(8) })
|
|
489
|
+
] }),
|
|
490
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, children: [
|
|
491
|
+
stage === "loading" && /* @__PURE__ */ jsx5(Spinner, { label: "Authenticating\u2026", type: "orbit" }),
|
|
492
|
+
stage === "success" && /* @__PURE__ */ jsx5(StatusBadge, { type: "success", message: `Logged in as ${email}` }),
|
|
493
|
+
stage === "error" && /* @__PURE__ */ jsx5(StatusBadge, { type: "error", message: errorMsg })
|
|
494
|
+
] }),
|
|
495
|
+
(stage === "input_email" || stage === "input_password") && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "gray", dimColor: true, children: "Press Enter to confirm \xB7 Esc to cancel" }) })
|
|
496
|
+
] });
|
|
497
|
+
}
|
|
498
|
+
function registerLogin(program2) {
|
|
499
|
+
program2.command("login").description("Authenticate with TaskAir API").action(async () => {
|
|
500
|
+
const { render } = await import("ink");
|
|
501
|
+
render(React4.createElement(LoginUI));
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/commands/logout.tsx
|
|
506
|
+
import React5, { useEffect as useEffect4, useState as useState5 } from "react";
|
|
507
|
+
import { Box as Box6, useApp as useApp3 } from "ink";
|
|
508
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
509
|
+
function LogoutUI() {
|
|
510
|
+
const { exit } = useApp3();
|
|
511
|
+
const [status, setStatus] = useState5("loading");
|
|
512
|
+
const [message, setMessage] = useState5("");
|
|
513
|
+
useEffect4(() => {
|
|
514
|
+
const creds = readCredentials();
|
|
515
|
+
if (!creds?.access_token) {
|
|
516
|
+
setMessage("Not logged in.");
|
|
517
|
+
setStatus("error");
|
|
518
|
+
setTimeout(() => exit(), 1e3);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
apiLogout(creds.api_url, creds.access_token).finally(() => {
|
|
522
|
+
clearTokens();
|
|
523
|
+
setMessage(`Logged out. Session cleared for ${creds.email}`);
|
|
524
|
+
setStatus("success");
|
|
525
|
+
setTimeout(() => exit(), 1200);
|
|
526
|
+
});
|
|
527
|
+
}, []);
|
|
528
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", padding: 1, children: [
|
|
529
|
+
/* @__PURE__ */ jsx6(MiniHeader, {}),
|
|
530
|
+
/* @__PURE__ */ jsxs6(Box6, { marginTop: 1, children: [
|
|
531
|
+
status === "loading" && /* @__PURE__ */ jsx6(Spinner, { label: "Logging out\u2026", type: "orbit" }),
|
|
532
|
+
status === "success" && /* @__PURE__ */ jsx6(StatusBadge, { type: "success", message }),
|
|
533
|
+
status === "error" && /* @__PURE__ */ jsx6(StatusBadge, { type: "error", message })
|
|
534
|
+
] })
|
|
535
|
+
] });
|
|
536
|
+
}
|
|
537
|
+
function registerLogout(program2) {
|
|
538
|
+
program2.command("logout").description("Clear local credentials and revoke session").action(async () => {
|
|
539
|
+
const { render } = await import("ink");
|
|
540
|
+
render(React5.createElement(LogoutUI));
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/commands/whoami.tsx
|
|
545
|
+
import React6 from "react";
|
|
546
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
547
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
548
|
+
function WhoamiUI() {
|
|
549
|
+
let auth = null;
|
|
550
|
+
let error = "";
|
|
551
|
+
try {
|
|
552
|
+
auth = requireAuth();
|
|
553
|
+
} catch (e) {
|
|
554
|
+
error = e.message;
|
|
555
|
+
}
|
|
556
|
+
const creds = readCredentials();
|
|
557
|
+
if (error || !auth) {
|
|
558
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
|
|
559
|
+
/* @__PURE__ */ jsx7(MiniHeader, {}),
|
|
560
|
+
/* @__PURE__ */ jsx7(StatusBadge, { type: "error", message: error || "Not authenticated" }),
|
|
561
|
+
/* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
|
|
562
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: "Run " }),
|
|
563
|
+
/* @__PURE__ */ jsx7(Text7, { color: "cyan", children: "taskair login" }),
|
|
564
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: " to authenticate." })
|
|
565
|
+
] })
|
|
566
|
+
] });
|
|
567
|
+
}
|
|
568
|
+
const expiresAt = new Date(auth.expiresAt * 1e3);
|
|
569
|
+
const now = /* @__PURE__ */ new Date();
|
|
570
|
+
const msLeft = expiresAt.getTime() - now.getTime();
|
|
571
|
+
const hoursLeft = Math.floor(msLeft / 1e3 / 60 / 60);
|
|
572
|
+
const minutesLeft = Math.floor(msLeft / 1e3 / 60 % 60);
|
|
573
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
|
|
574
|
+
/* @__PURE__ */ jsx7(AsciiHeader, {}),
|
|
575
|
+
/* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "#7B61FF", paddingX: 2, paddingY: 1, children: [
|
|
576
|
+
/* @__PURE__ */ jsx7(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "cyan", bold: true, children: "\u2726 Current Session" }) }),
|
|
577
|
+
/* @__PURE__ */ jsxs7(Box7, { marginBottom: 0, children: [
|
|
578
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: " Email: " }),
|
|
579
|
+
/* @__PURE__ */ jsx7(Text7, { color: "cyan", bold: true, children: auth.email })
|
|
580
|
+
] }),
|
|
581
|
+
/* @__PURE__ */ jsxs7(Box7, { marginBottom: 0, children: [
|
|
582
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: " Device ID: " }),
|
|
583
|
+
/* @__PURE__ */ jsx7(Text7, { color: "#7B61FF", children: auth.deviceId })
|
|
584
|
+
] }),
|
|
585
|
+
/* @__PURE__ */ jsxs7(Box7, { marginBottom: 0, children: [
|
|
586
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: " API URL: " }),
|
|
587
|
+
/* @__PURE__ */ jsx7(Text7, { color: "white", children: creds?.api_url ?? "\u2014" })
|
|
588
|
+
] }),
|
|
589
|
+
/* @__PURE__ */ jsxs7(Box7, { marginBottom: 0, children: [
|
|
590
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: " Expires: " }),
|
|
591
|
+
/* @__PURE__ */ jsx7(Text7, { color: hoursLeft < 1 ? "red" : "green", children: hoursLeft > 0 ? `${hoursLeft}h ${minutesLeft}m remaining` : `${minutesLeft}m remaining` })
|
|
592
|
+
] }),
|
|
593
|
+
/* @__PURE__ */ jsxs7(Box7, { marginTop: 1, children: [
|
|
594
|
+
/* @__PURE__ */ jsx7(Text7, { color: "green", bold: true, children: " \u2713 " }),
|
|
595
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: "End-to-end encryption active" }),
|
|
596
|
+
/* @__PURE__ */ jsx7(Text7, { color: "cyan", children: " \xB7" }),
|
|
597
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: " AES-256-GCM" })
|
|
598
|
+
] })
|
|
599
|
+
] })
|
|
600
|
+
] });
|
|
601
|
+
}
|
|
602
|
+
function registerWhoami(program2) {
|
|
603
|
+
program2.command("whoami").description("Display the currently authenticated user").action(async () => {
|
|
604
|
+
const { render } = await import("ink");
|
|
605
|
+
render(React6.createElement(WhoamiUI));
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/commands/add.tsx
|
|
610
|
+
import React7, { useEffect as useEffect5, useState as useState6 } from "react";
|
|
611
|
+
import { Box as Box8, Text as Text8, useApp as useApp4 } from "ink";
|
|
612
|
+
import { v4 as uuidv43 } from "uuid";
|
|
613
|
+
|
|
614
|
+
// src/lib/storage.ts
|
|
615
|
+
import { join as join2 } from "path";
|
|
616
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
617
|
+
import { v4 as uuidv42 } from "uuid";
|
|
618
|
+
var STORE_FILE = join2(TASKAIR_DIR, "tasks.json");
|
|
619
|
+
function defaultStore() {
|
|
620
|
+
return { tasks: [], sync_queue: [] };
|
|
621
|
+
}
|
|
622
|
+
function ensureDir() {
|
|
623
|
+
if (!existsSync2(TASKAIR_DIR)) {
|
|
624
|
+
mkdirSync2(TASKAIR_DIR, { recursive: true, mode: 448 });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function readStore() {
|
|
628
|
+
ensureDir();
|
|
629
|
+
if (!existsSync2(STORE_FILE)) return defaultStore();
|
|
630
|
+
try {
|
|
631
|
+
return JSON.parse(readFileSync2(STORE_FILE, "utf8"));
|
|
632
|
+
} catch {
|
|
633
|
+
return defaultStore();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function writeStore(store) {
|
|
637
|
+
ensureDir();
|
|
638
|
+
writeFileSync2(STORE_FILE, JSON.stringify(store, null, 2), {
|
|
639
|
+
encoding: "utf8",
|
|
640
|
+
mode: 384
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
function getAllTasks() {
|
|
644
|
+
return readStore().tasks;
|
|
645
|
+
}
|
|
646
|
+
function getTask(id) {
|
|
647
|
+
return readStore().tasks.find((t) => t.id === id || t.id.startsWith(id));
|
|
648
|
+
}
|
|
649
|
+
function insertTask(task) {
|
|
650
|
+
const store = readStore();
|
|
651
|
+
store.tasks.push(task);
|
|
652
|
+
queueOperation(store, "add", task.id);
|
|
653
|
+
writeStore(store);
|
|
654
|
+
}
|
|
655
|
+
function updateTask(id, updates) {
|
|
656
|
+
const store = readStore();
|
|
657
|
+
const idx = store.tasks.findIndex((t) => t.id === id || t.id.startsWith(id));
|
|
658
|
+
if (idx === -1) return null;
|
|
659
|
+
store.tasks[idx] = {
|
|
660
|
+
...store.tasks[idx],
|
|
661
|
+
...updates,
|
|
662
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
663
|
+
};
|
|
664
|
+
queueOperation(store, "update", store.tasks[idx].id);
|
|
665
|
+
writeStore(store);
|
|
666
|
+
return store.tasks[idx];
|
|
667
|
+
}
|
|
668
|
+
function deleteTask(id) {
|
|
669
|
+
const store = readStore();
|
|
670
|
+
const idx = store.tasks.findIndex((t) => t.id === id || t.id.startsWith(id));
|
|
671
|
+
if (idx === -1) return false;
|
|
672
|
+
const taskId = store.tasks[idx].id;
|
|
673
|
+
store.tasks.splice(idx, 1);
|
|
674
|
+
queueOperation(store, "delete", taskId);
|
|
675
|
+
writeStore(store);
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
function queueOperation(store, operation, taskId) {
|
|
679
|
+
store.sync_queue.push({
|
|
680
|
+
id: uuidv42(),
|
|
681
|
+
operation,
|
|
682
|
+
task_id: taskId,
|
|
683
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
function clearSyncQueue() {
|
|
687
|
+
const store = readStore();
|
|
688
|
+
store.sync_queue = [];
|
|
689
|
+
store.last_synced_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
690
|
+
writeStore(store);
|
|
691
|
+
}
|
|
692
|
+
function getLastSyncedAt() {
|
|
693
|
+
return readStore().last_synced_at;
|
|
694
|
+
}
|
|
695
|
+
function filterTasks(filter) {
|
|
696
|
+
let tasks = getAllTasks();
|
|
697
|
+
if (filter.status && filter.status !== "all") {
|
|
698
|
+
tasks = tasks.filter((t) => t.status === filter.status);
|
|
699
|
+
}
|
|
700
|
+
if (filter.priority && filter.priority !== "all") {
|
|
701
|
+
tasks = tasks.filter((t) => t.priority === filter.priority);
|
|
702
|
+
}
|
|
703
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
704
|
+
tasks = tasks.filter(
|
|
705
|
+
(t) => filter.tags.some((tag) => t.tags.includes(tag))
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return tasks;
|
|
709
|
+
}
|
|
710
|
+
function computeStats() {
|
|
711
|
+
const tasks = getAllTasks();
|
|
712
|
+
const now = /* @__PURE__ */ new Date();
|
|
713
|
+
const pending = tasks.filter((t) => t.status === "pending").length;
|
|
714
|
+
const in_progress = tasks.filter((t) => t.status === "in_progress").length;
|
|
715
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
716
|
+
const overdue = tasks.filter(
|
|
717
|
+
(t) => t.due_date && t.status !== "completed" && new Date(t.due_date) < now
|
|
718
|
+
).length;
|
|
719
|
+
return {
|
|
720
|
+
total: tasks.length,
|
|
721
|
+
pending,
|
|
722
|
+
in_progress,
|
|
723
|
+
completed,
|
|
724
|
+
high_priority: tasks.filter((t) => t.priority === "high").length,
|
|
725
|
+
medium_priority: tasks.filter((t) => t.priority === "medium").length,
|
|
726
|
+
low_priority: tasks.filter((t) => t.priority === "low").length,
|
|
727
|
+
overdue,
|
|
728
|
+
completion_rate: tasks.length > 0 ? Math.round(completed / tasks.length * 100) : 0
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/commands/add.tsx
|
|
733
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
734
|
+
function AddUI({
|
|
735
|
+
description,
|
|
736
|
+
priority,
|
|
737
|
+
due,
|
|
738
|
+
tags
|
|
739
|
+
}) {
|
|
740
|
+
const { exit } = useApp4();
|
|
741
|
+
const [status, setStatus] = useState6("working");
|
|
742
|
+
const [taskId, setTaskId] = useState6("");
|
|
743
|
+
const [errorMsg, setErrorMsg] = useState6("");
|
|
744
|
+
useEffect5(() => {
|
|
745
|
+
try {
|
|
746
|
+
const auth = requireAuth();
|
|
747
|
+
const task = {
|
|
748
|
+
id: uuidv43(),
|
|
749
|
+
description,
|
|
750
|
+
priority,
|
|
751
|
+
status: "pending",
|
|
752
|
+
tags,
|
|
753
|
+
due_date: due,
|
|
754
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
755
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
756
|
+
device_id: auth.deviceId,
|
|
757
|
+
vector_clock: { [auth.deviceId]: 1 }
|
|
758
|
+
};
|
|
759
|
+
insertTask(task);
|
|
760
|
+
setTaskId(task.id.slice(0, 8));
|
|
761
|
+
setStatus("success");
|
|
762
|
+
setTimeout(() => exit(), 1500);
|
|
763
|
+
} catch (e) {
|
|
764
|
+
setErrorMsg(e.message);
|
|
765
|
+
setStatus("error");
|
|
766
|
+
setTimeout(() => exit(e), 1500);
|
|
767
|
+
}
|
|
768
|
+
}, []);
|
|
769
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", padding: 1, children: [
|
|
770
|
+
/* @__PURE__ */ jsx8(MiniHeader, {}),
|
|
771
|
+
status === "working" && /* @__PURE__ */ jsx8(Spinner, { label: "Creating task\u2026", type: "star", color: "magenta" }),
|
|
772
|
+
status === "success" && /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
|
|
773
|
+
/* @__PURE__ */ jsx8(StarBurst, { label: `Task created! ID: ${taskId}`, color: "cyan" }),
|
|
774
|
+
/* @__PURE__ */ jsxs8(Box8, { marginTop: 1, flexDirection: "column", children: [
|
|
775
|
+
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
776
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: " Description: " }),
|
|
777
|
+
/* @__PURE__ */ jsx8(Text8, { color: "white", children: description })
|
|
778
|
+
] }),
|
|
779
|
+
/* @__PURE__ */ jsxs8(Box8, { children: [
|
|
780
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: " Priority: " }),
|
|
781
|
+
/* @__PURE__ */ jsx8(
|
|
782
|
+
Text8,
|
|
783
|
+
{
|
|
784
|
+
color: priority === "high" ? "red" : priority === "medium" ? "yellow" : "green",
|
|
785
|
+
bold: true,
|
|
786
|
+
children: priority.toUpperCase()
|
|
787
|
+
}
|
|
788
|
+
)
|
|
789
|
+
] }),
|
|
790
|
+
due && /* @__PURE__ */ jsxs8(Box8, { children: [
|
|
791
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: " Due date: " }),
|
|
792
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", children: due })
|
|
793
|
+
] }),
|
|
794
|
+
tags.length > 0 && /* @__PURE__ */ jsxs8(Box8, { children: [
|
|
795
|
+
/* @__PURE__ */ jsx8(Text8, { color: "gray", children: " Tags: " }),
|
|
796
|
+
/* @__PURE__ */ jsx8(Text8, { color: "#7B61FF", children: tags.join(", ") })
|
|
797
|
+
] }),
|
|
798
|
+
/* @__PURE__ */ jsxs8(Box8, { marginTop: 1, children: [
|
|
799
|
+
/* @__PURE__ */ jsxs8(Text8, { color: "gray", dimColor: true, children: [
|
|
800
|
+
"Queued for sync \xB7 Run",
|
|
801
|
+
" "
|
|
802
|
+
] }),
|
|
803
|
+
/* @__PURE__ */ jsx8(Text8, { color: "cyan", dimColor: true, children: "taskair sync" }),
|
|
804
|
+
/* @__PURE__ */ jsxs8(Text8, { color: "gray", dimColor: true, children: [
|
|
805
|
+
" ",
|
|
806
|
+
"to push to cloud"
|
|
807
|
+
] })
|
|
808
|
+
] })
|
|
809
|
+
] })
|
|
810
|
+
] }),
|
|
811
|
+
status === "error" && /* @__PURE__ */ jsx8(StatusBadge, { type: "error", message: errorMsg })
|
|
812
|
+
] });
|
|
813
|
+
}
|
|
814
|
+
function registerAdd(program2) {
|
|
815
|
+
program2.command("add [description]").alias("-a").description("Create a new task").option("-p, --priority <level>", "Priority: high | medium | low", "medium").option("-d, --due <datetime>", "Due date (ISO 8601 or YYYY-MM-DD)").option("-t, --tags <tags>", "Comma-separated tags").action(async (description, options) => {
|
|
816
|
+
if (!description) {
|
|
817
|
+
console.error('\u2717 Description is required. Usage: taskair add "description"');
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
const priority = ["high", "medium", "low"].includes(options.priority) ? options.priority : "medium";
|
|
821
|
+
const tags = options.tags ? options.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
822
|
+
const { render } = await import("ink");
|
|
823
|
+
render(
|
|
824
|
+
React7.createElement(AddUI, {
|
|
825
|
+
description,
|
|
826
|
+
priority,
|
|
827
|
+
due: options.due,
|
|
828
|
+
tags
|
|
829
|
+
})
|
|
830
|
+
);
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/commands/list.tsx
|
|
835
|
+
import React9, { useEffect as useEffect6, useState as useState7 } from "react";
|
|
836
|
+
import { Box as Box10, Text as Text10, useApp as useApp5 } from "ink";
|
|
837
|
+
|
|
838
|
+
// src/components/TaskTable.tsx
|
|
839
|
+
import React8 from "react";
|
|
840
|
+
import { Box as Box9, Text as Text9 } from "ink";
|
|
841
|
+
import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
842
|
+
function priorityColor(p) {
|
|
843
|
+
return { high: "red", medium: "yellow", low: "green" }[p] ?? "white";
|
|
844
|
+
}
|
|
845
|
+
function statusColor(s) {
|
|
846
|
+
return {
|
|
847
|
+
pending: "white",
|
|
848
|
+
in_progress: "cyan",
|
|
849
|
+
completed: "green"
|
|
850
|
+
}[s] ?? "white";
|
|
851
|
+
}
|
|
852
|
+
function statusLabel(s) {
|
|
853
|
+
return {
|
|
854
|
+
pending: "PENDING",
|
|
855
|
+
in_progress: "IN PROG",
|
|
856
|
+
completed: "DONE"
|
|
857
|
+
}[s] ?? s;
|
|
858
|
+
}
|
|
859
|
+
function truncate(str, max) {
|
|
860
|
+
return str.length > max ? str.slice(0, max - 1) + "\u2026" : str;
|
|
861
|
+
}
|
|
862
|
+
function formatDate(iso) {
|
|
863
|
+
if (!iso) return "\u2014";
|
|
864
|
+
const d = new Date(iso);
|
|
865
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
866
|
+
}
|
|
867
|
+
function isOverdue(task) {
|
|
868
|
+
if (!task.due_date || task.status === "completed") return false;
|
|
869
|
+
return new Date(task.due_date) < /* @__PURE__ */ new Date();
|
|
870
|
+
}
|
|
871
|
+
var COL_WIDTHS = {
|
|
872
|
+
id: 8,
|
|
873
|
+
desc: 38,
|
|
874
|
+
priority: 8,
|
|
875
|
+
status: 8,
|
|
876
|
+
due: 12,
|
|
877
|
+
tags: 16
|
|
878
|
+
};
|
|
879
|
+
function TableRow({ task }) {
|
|
880
|
+
const overdue = isOverdue(task);
|
|
881
|
+
const id = task.id.slice(0, COL_WIDTHS.id);
|
|
882
|
+
const desc = truncate(task.description, COL_WIDTHS.desc);
|
|
883
|
+
const tags = task.tags.length > 0 ? task.tags.slice(0, 2).join(",") : "\u2014";
|
|
884
|
+
return /* @__PURE__ */ jsxs9(Box9, { children: [
|
|
885
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: "\u2502 " }),
|
|
886
|
+
/* @__PURE__ */ jsx9(Text9, { color: "cyan", dimColor: true, children: id.padEnd(COL_WIDTHS.id) }),
|
|
887
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502 " }),
|
|
888
|
+
/* @__PURE__ */ jsx9(Text9, { color: overdue ? "red" : "white", children: desc.padEnd(COL_WIDTHS.desc) }),
|
|
889
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502 " }),
|
|
890
|
+
/* @__PURE__ */ jsx9(Text9, { color: priorityColor(task.priority), bold: true, children: task.priority.toUpperCase().padEnd(COL_WIDTHS.priority) }),
|
|
891
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502 " }),
|
|
892
|
+
/* @__PURE__ */ jsx9(Text9, { color: statusColor(task.status), children: statusLabel(task.status).padEnd(COL_WIDTHS.status) }),
|
|
893
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502 " }),
|
|
894
|
+
/* @__PURE__ */ jsx9(Text9, { color: overdue ? "red" : "gray", children: formatDate(task.due_date).padEnd(COL_WIDTHS.due) }),
|
|
895
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502 " }),
|
|
896
|
+
/* @__PURE__ */ jsx9(Text9, { color: "#7B61FF", children: truncate(tags, COL_WIDTHS.tags).padEnd(COL_WIDTHS.tags) }),
|
|
897
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502" })
|
|
898
|
+
] });
|
|
899
|
+
}
|
|
900
|
+
function Separator() {
|
|
901
|
+
const line = "\u251C\u2500" + "\u2500".repeat(COL_WIDTHS.id) + "\u2500\u253C\u2500" + "\u2500".repeat(COL_WIDTHS.desc) + "\u2500\u253C\u2500" + "\u2500".repeat(COL_WIDTHS.priority) + "\u2500\u253C\u2500" + "\u2500".repeat(COL_WIDTHS.status) + "\u2500\u253C\u2500" + "\u2500".repeat(COL_WIDTHS.due) + "\u2500\u253C\u2500" + "\u2500".repeat(COL_WIDTHS.tags) + "\u2500\u2524";
|
|
902
|
+
return /* @__PURE__ */ jsx9(Text9, { color: "gray", children: line });
|
|
903
|
+
}
|
|
904
|
+
function TopBorder() {
|
|
905
|
+
const line = "\u256D\u2500" + "\u2500".repeat(COL_WIDTHS.id) + "\u2500\u252C\u2500" + "\u2500".repeat(COL_WIDTHS.desc) + "\u2500\u252C\u2500" + "\u2500".repeat(COL_WIDTHS.priority) + "\u2500\u252C\u2500" + "\u2500".repeat(COL_WIDTHS.status) + "\u2500\u252C\u2500" + "\u2500".repeat(COL_WIDTHS.due) + "\u2500\u252C\u2500" + "\u2500".repeat(COL_WIDTHS.tags) + "\u2500\u256E";
|
|
906
|
+
return /* @__PURE__ */ jsx9(Text9, { color: "gray", children: line });
|
|
907
|
+
}
|
|
908
|
+
function BottomBorder() {
|
|
909
|
+
const line = "\u2570\u2500" + "\u2500".repeat(COL_WIDTHS.id) + "\u2500\u2534\u2500" + "\u2500".repeat(COL_WIDTHS.desc) + "\u2500\u2534\u2500" + "\u2500".repeat(COL_WIDTHS.priority) + "\u2500\u2534\u2500" + "\u2500".repeat(COL_WIDTHS.status) + "\u2500\u2534\u2500" + "\u2500".repeat(COL_WIDTHS.due) + "\u2500\u2534\u2500" + "\u2500".repeat(COL_WIDTHS.tags) + "\u2500\u256F";
|
|
910
|
+
return /* @__PURE__ */ jsx9(Text9, { color: "gray", children: line });
|
|
911
|
+
}
|
|
912
|
+
function Header() {
|
|
913
|
+
return /* @__PURE__ */ jsxs9(Box9, { children: [
|
|
914
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: "\u2502 " }),
|
|
915
|
+
/* @__PURE__ */ jsx9(Text9, { color: "#7B61FF", bold: true, children: "ID".padEnd(COL_WIDTHS.id) }),
|
|
916
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502 " }),
|
|
917
|
+
/* @__PURE__ */ jsx9(Text9, { color: "#7B61FF", bold: true, children: "DESCRIPTION".padEnd(COL_WIDTHS.desc) }),
|
|
918
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502 " }),
|
|
919
|
+
/* @__PURE__ */ jsx9(Text9, { color: "#7B61FF", bold: true, children: "PRIORITY".padEnd(COL_WIDTHS.priority) }),
|
|
920
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502 " }),
|
|
921
|
+
/* @__PURE__ */ jsx9(Text9, { color: "#7B61FF", bold: true, children: "STATUS".padEnd(COL_WIDTHS.status) }),
|
|
922
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502 " }),
|
|
923
|
+
/* @__PURE__ */ jsx9(Text9, { color: "#7B61FF", bold: true, children: "DUE DATE".padEnd(COL_WIDTHS.due) }),
|
|
924
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502 " }),
|
|
925
|
+
/* @__PURE__ */ jsx9(Text9, { color: "#7B61FF", bold: true, children: "TAGS".padEnd(COL_WIDTHS.tags) }),
|
|
926
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " \u2502" })
|
|
927
|
+
] });
|
|
928
|
+
}
|
|
929
|
+
function TaskTable({ tasks }) {
|
|
930
|
+
if (tasks.length === 0) {
|
|
931
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", paddingY: 1, children: [
|
|
932
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " No tasks found. Run " }),
|
|
933
|
+
/* @__PURE__ */ jsx9(Text9, { color: "cyan", children: 'taskair add "description"' }),
|
|
934
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: " to create one." })
|
|
935
|
+
] });
|
|
936
|
+
}
|
|
937
|
+
return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
|
|
938
|
+
/* @__PURE__ */ jsx9(TopBorder, {}),
|
|
939
|
+
/* @__PURE__ */ jsx9(Header, {}),
|
|
940
|
+
/* @__PURE__ */ jsx9(Separator, {}),
|
|
941
|
+
tasks.map((task, i) => /* @__PURE__ */ jsxs9(React8.Fragment, { children: [
|
|
942
|
+
/* @__PURE__ */ jsx9(TableRow, { task }),
|
|
943
|
+
i < tasks.length - 1 && /* @__PURE__ */ jsx9(Separator, {})
|
|
944
|
+
] }, task.id)),
|
|
945
|
+
/* @__PURE__ */ jsx9(BottomBorder, {}),
|
|
946
|
+
/* @__PURE__ */ jsxs9(Box9, { marginTop: 1, children: [
|
|
947
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "gray", children: [
|
|
948
|
+
" ",
|
|
949
|
+
tasks.length,
|
|
950
|
+
" task",
|
|
951
|
+
tasks.length !== 1 ? "s" : "",
|
|
952
|
+
" "
|
|
953
|
+
] }),
|
|
954
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "cyan", children: [
|
|
955
|
+
"\xB7 ",
|
|
956
|
+
tasks.filter((t) => t.status === "completed").length,
|
|
957
|
+
" completed"
|
|
958
|
+
] }),
|
|
959
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "gray", children: [
|
|
960
|
+
" ",
|
|
961
|
+
"\xB7 ",
|
|
962
|
+
tasks.filter(isOverdue).length,
|
|
963
|
+
" overdue"
|
|
964
|
+
] })
|
|
965
|
+
] })
|
|
966
|
+
] });
|
|
967
|
+
}
|
|
968
|
+
function TaskCompact({ tasks }) {
|
|
969
|
+
if (tasks.length === 0) {
|
|
970
|
+
return /* @__PURE__ */ jsx9(Text9, { color: "gray", children: "No tasks found." });
|
|
971
|
+
}
|
|
972
|
+
return /* @__PURE__ */ jsx9(Box9, { flexDirection: "column", children: tasks.map((task) => {
|
|
973
|
+
const overdue = isOverdue(task);
|
|
974
|
+
return /* @__PURE__ */ jsxs9(Box9, { marginBottom: 0, children: [
|
|
975
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "cyan", dimColor: true, children: [
|
|
976
|
+
task.id.slice(0, 8),
|
|
977
|
+
" "
|
|
978
|
+
] }),
|
|
979
|
+
/* @__PURE__ */ jsxs9(Text9, { color: priorityColor(task.priority), bold: true, children: [
|
|
980
|
+
"[",
|
|
981
|
+
task.priority.slice(0, 1).toUpperCase(),
|
|
982
|
+
"]",
|
|
983
|
+
" "
|
|
984
|
+
] }),
|
|
985
|
+
/* @__PURE__ */ jsxs9(Text9, { color: overdue ? "red" : "white", children: [
|
|
986
|
+
truncate(task.description, 50),
|
|
987
|
+
" "
|
|
988
|
+
] }),
|
|
989
|
+
/* @__PURE__ */ jsx9(Text9, { color: "gray", children: task.status === "completed" ? "\u2713" : task.status === "in_progress" ? "\u27F3" : "\u25CB" })
|
|
990
|
+
] }, task.id);
|
|
991
|
+
}) });
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/commands/list.tsx
|
|
995
|
+
import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
996
|
+
function ListUI({ filter, format }) {
|
|
997
|
+
const { exit } = useApp5();
|
|
998
|
+
const [tasks, setTasks] = useState7([]);
|
|
999
|
+
useEffect6(() => {
|
|
1000
|
+
const results = filterTasks(filter);
|
|
1001
|
+
results.sort((a, b) => {
|
|
1002
|
+
const pOrder = { high: 0, medium: 1, low: 2 };
|
|
1003
|
+
const pDiff = pOrder[a.priority] - pOrder[b.priority];
|
|
1004
|
+
if (pDiff !== 0) return pDiff;
|
|
1005
|
+
return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
|
|
1006
|
+
});
|
|
1007
|
+
setTasks(results);
|
|
1008
|
+
if (format === "json") {
|
|
1009
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1010
|
+
exit();
|
|
1011
|
+
}
|
|
1012
|
+
}, []);
|
|
1013
|
+
if (format === "json") {
|
|
1014
|
+
return /* @__PURE__ */ jsx10(Box10, {});
|
|
1015
|
+
}
|
|
1016
|
+
return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", padding: 1, children: [
|
|
1017
|
+
/* @__PURE__ */ jsx10(MiniHeader, {}),
|
|
1018
|
+
(filter.status || filter.priority || filter.tags?.length) && /* @__PURE__ */ jsxs10(Box10, { marginBottom: 1, children: [
|
|
1019
|
+
/* @__PURE__ */ jsxs10(Text10, { color: "gray", dimColor: true, children: [
|
|
1020
|
+
"Filters:",
|
|
1021
|
+
" "
|
|
1022
|
+
] }),
|
|
1023
|
+
filter.status && /* @__PURE__ */ jsxs10(Text10, { color: "cyan", dimColor: true, children: [
|
|
1024
|
+
"status=",
|
|
1025
|
+
filter.status,
|
|
1026
|
+
" "
|
|
1027
|
+
] }),
|
|
1028
|
+
filter.priority && /* @__PURE__ */ jsxs10(Text10, { color: "yellow", dimColor: true, children: [
|
|
1029
|
+
"priority=",
|
|
1030
|
+
filter.priority,
|
|
1031
|
+
" "
|
|
1032
|
+
] }),
|
|
1033
|
+
filter.tags?.map((t) => /* @__PURE__ */ jsxs10(Text10, { color: "#7B61FF", dimColor: true, children: [
|
|
1034
|
+
"#",
|
|
1035
|
+
t,
|
|
1036
|
+
" "
|
|
1037
|
+
] }, t))
|
|
1038
|
+
] }),
|
|
1039
|
+
format === "table" && /* @__PURE__ */ jsx10(TaskTable, { tasks }),
|
|
1040
|
+
format === "compact" && /* @__PURE__ */ jsx10(TaskCompact, { tasks })
|
|
1041
|
+
] });
|
|
1042
|
+
}
|
|
1043
|
+
function registerList(program2) {
|
|
1044
|
+
program2.command("list").alias("-l").description("List tasks with optional filters").option("-s, --status <status>", "Filter by status: pending | in_progress | completed | all", "all").option("-p, --priority <priority>", "Filter by priority: high | medium | low | all", "all").option("-t, --tags <tags>", "Filter by comma-separated tags").option("-f, --format <format>", "Output format: table | compact | json", "table").action(async (options) => {
|
|
1045
|
+
const filter = {};
|
|
1046
|
+
if (options.status && options.status !== "all") {
|
|
1047
|
+
filter.status = options.status;
|
|
1048
|
+
}
|
|
1049
|
+
if (options.priority && options.priority !== "all") {
|
|
1050
|
+
filter.priority = options.priority;
|
|
1051
|
+
}
|
|
1052
|
+
if (options.tags) {
|
|
1053
|
+
filter.tags = options.tags.split(",").map((t) => t.trim());
|
|
1054
|
+
}
|
|
1055
|
+
const format = ["table", "compact", "json"].includes(options.format) ? options.format : "table";
|
|
1056
|
+
if (format === "json") {
|
|
1057
|
+
const results = filterTasks(filter);
|
|
1058
|
+
console.log(JSON.stringify(results, null, 2));
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const { render } = await import("ink");
|
|
1062
|
+
render(React9.createElement(ListUI, { filter, format }));
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// src/commands/done.tsx
|
|
1067
|
+
import React10, { useEffect as useEffect7, useState as useState8 } from "react";
|
|
1068
|
+
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";
|
|
1070
|
+
function DoneUI({ id, note }) {
|
|
1071
|
+
const { exit } = useApp6();
|
|
1072
|
+
const [status, setStatus] = useState8("success");
|
|
1073
|
+
const [message, setMessage] = useState8("");
|
|
1074
|
+
const [description, setDescription] = useState8("");
|
|
1075
|
+
useEffect7(() => {
|
|
1076
|
+
const task = getTask(id);
|
|
1077
|
+
if (!task) {
|
|
1078
|
+
setMessage(`No task found with ID starting with "${id}"`);
|
|
1079
|
+
setStatus("error");
|
|
1080
|
+
setTimeout(() => exit(new Error("Task not found")), 1200);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
if (task.status === "completed") {
|
|
1084
|
+
setMessage(`Task is already marked as completed.`);
|
|
1085
|
+
setStatus("error");
|
|
1086
|
+
setTimeout(() => exit(), 1200);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const updated = updateTask(task.id, {
|
|
1090
|
+
status: "completed",
|
|
1091
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1092
|
+
completion_note: note
|
|
1093
|
+
});
|
|
1094
|
+
if (updated) {
|
|
1095
|
+
setDescription(task.description);
|
|
1096
|
+
setMessage(`Task marked complete!`);
|
|
1097
|
+
setStatus("success");
|
|
1098
|
+
setTimeout(() => exit(), 1500);
|
|
1099
|
+
}
|
|
1100
|
+
}, []);
|
|
1101
|
+
return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", padding: 1, children: [
|
|
1102
|
+
/* @__PURE__ */ jsx11(MiniHeader, {}),
|
|
1103
|
+
/* @__PURE__ */ jsx11(Box11, { marginTop: 1, flexDirection: "column", children: status === "success" ? /* @__PURE__ */ jsxs11(Fragment, { children: [
|
|
1104
|
+
/* @__PURE__ */ jsx11(StarBurst, { label: message, color: "green" }),
|
|
1105
|
+
description && /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, children: [
|
|
1106
|
+
/* @__PURE__ */ jsx11(Text11, { color: "gray", children: " " }),
|
|
1107
|
+
/* @__PURE__ */ jsx11(Text11, { color: "green", children: "\u2713 " }),
|
|
1108
|
+
/* @__PURE__ */ jsx11(Text11, { color: "white", children: description })
|
|
1109
|
+
] }),
|
|
1110
|
+
note && /* @__PURE__ */ jsxs11(Box11, { children: [
|
|
1111
|
+
/* @__PURE__ */ jsx11(Text11, { color: "gray", children: " Note: " }),
|
|
1112
|
+
/* @__PURE__ */ jsx11(Text11, { color: "cyan", children: note })
|
|
1113
|
+
] })
|
|
1114
|
+
] }) : /* @__PURE__ */ jsx11(StatusBadge, { type: "error", message }) })
|
|
1115
|
+
] });
|
|
1116
|
+
}
|
|
1117
|
+
function registerDone(program2) {
|
|
1118
|
+
program2.command("done <id>").alias("-d").description("Mark a task as complete").option("-n, --note <note>", "Completion note").action(async (id, options) => {
|
|
1119
|
+
const { render } = await import("ink");
|
|
1120
|
+
render(React10.createElement(DoneUI, { id, note: options.note }));
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// src/commands/remove.tsx
|
|
1125
|
+
import React11, { useEffect as useEffect8, useState as useState9 } from "react";
|
|
1126
|
+
import { Box as Box12, Text as Text12, useApp as useApp7, useInput as useInput3 } from "ink";
|
|
1127
|
+
import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1128
|
+
function RemoveUI({ id, force }) {
|
|
1129
|
+
const { exit } = useApp7();
|
|
1130
|
+
const [stage, setStage] = useState9("confirm");
|
|
1131
|
+
const [message, setMessage] = useState9("");
|
|
1132
|
+
const task = getTask(id);
|
|
1133
|
+
useEffect8(() => {
|
|
1134
|
+
if (!task) {
|
|
1135
|
+
setMessage(`No task found with ID starting with "${id}"`);
|
|
1136
|
+
setStage("error");
|
|
1137
|
+
setTimeout(() => exit(new Error("Task not found")), 1200);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
if (force) {
|
|
1141
|
+
const deleted = deleteTask(task.id);
|
|
1142
|
+
if (deleted) {
|
|
1143
|
+
setMessage(`Task removed: ${task.description}`);
|
|
1144
|
+
setStage("success");
|
|
1145
|
+
setTimeout(() => exit(), 1200);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}, []);
|
|
1149
|
+
useInput3((input) => {
|
|
1150
|
+
if (stage !== "confirm" || !task) return;
|
|
1151
|
+
if (input.toLowerCase() === "y") {
|
|
1152
|
+
const deleted = deleteTask(task.id);
|
|
1153
|
+
if (deleted) {
|
|
1154
|
+
setMessage(`Task removed: ${task.description}`);
|
|
1155
|
+
setStage("success");
|
|
1156
|
+
setTimeout(() => exit(), 1200);
|
|
1157
|
+
}
|
|
1158
|
+
} else if (input.toLowerCase() === "n" || input === "") {
|
|
1159
|
+
setStage("cancelled");
|
|
1160
|
+
setTimeout(() => exit(), 800);
|
|
1161
|
+
}
|
|
1162
|
+
});
|
|
1163
|
+
return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", padding: 1, children: [
|
|
1164
|
+
/* @__PURE__ */ jsx12(MiniHeader, {}),
|
|
1165
|
+
!task && stage !== "error" && /* @__PURE__ */ jsx12(StatusBadge, { type: "error", message: `No task found with ID "${id}"` }),
|
|
1166
|
+
task && stage === "confirm" && /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", children: [
|
|
1167
|
+
/* @__PURE__ */ jsxs12(Box12, { marginBottom: 1, children: [
|
|
1168
|
+
/* @__PURE__ */ jsx12(Text12, { color: "yellow", bold: true, children: "\u26A0 " }),
|
|
1169
|
+
/* @__PURE__ */ jsx12(Text12, { color: "white", children: "Delete this task?" })
|
|
1170
|
+
] }),
|
|
1171
|
+
/* @__PURE__ */ jsxs12(Box12, { marginBottom: 1, paddingX: 2, flexDirection: "column", children: [
|
|
1172
|
+
/* @__PURE__ */ jsxs12(Box12, { children: [
|
|
1173
|
+
/* @__PURE__ */ jsx12(Text12, { color: "gray", children: "ID: " }),
|
|
1174
|
+
/* @__PURE__ */ jsx12(Text12, { color: "cyan", children: task.id.slice(0, 8) })
|
|
1175
|
+
] }),
|
|
1176
|
+
/* @__PURE__ */ jsxs12(Box12, { children: [
|
|
1177
|
+
/* @__PURE__ */ jsx12(Text12, { color: "gray", children: "Description: " }),
|
|
1178
|
+
/* @__PURE__ */ jsx12(Text12, { color: "white", children: task.description })
|
|
1179
|
+
] }),
|
|
1180
|
+
/* @__PURE__ */ jsxs12(Box12, { children: [
|
|
1181
|
+
/* @__PURE__ */ jsx12(Text12, { color: "gray", children: "Status: " }),
|
|
1182
|
+
/* @__PURE__ */ jsx12(Text12, { color: "yellow", children: task.status })
|
|
1183
|
+
] })
|
|
1184
|
+
] }),
|
|
1185
|
+
/* @__PURE__ */ jsxs12(Box12, { children: [
|
|
1186
|
+
/* @__PURE__ */ jsx12(Text12, { color: "gray", children: "Type " }),
|
|
1187
|
+
/* @__PURE__ */ jsx12(Text12, { color: "green", bold: true, children: "y" }),
|
|
1188
|
+
/* @__PURE__ */ jsx12(Text12, { color: "gray", children: " to confirm or " }),
|
|
1189
|
+
/* @__PURE__ */ jsx12(Text12, { color: "red", bold: true, children: "n" }),
|
|
1190
|
+
/* @__PURE__ */ jsx12(Text12, { color: "gray", children: " to cancel: " })
|
|
1191
|
+
] })
|
|
1192
|
+
] }),
|
|
1193
|
+
stage === "success" && /* @__PURE__ */ jsx12(StarBurst, { label: message, color: "cyan" }),
|
|
1194
|
+
stage === "error" && /* @__PURE__ */ jsx12(StatusBadge, { type: "error", message }),
|
|
1195
|
+
stage === "cancelled" && /* @__PURE__ */ jsx12(StatusBadge, { type: "info", message: "Deletion cancelled." })
|
|
1196
|
+
] });
|
|
1197
|
+
}
|
|
1198
|
+
function registerRemove(program2) {
|
|
1199
|
+
program2.command("remove <id>").alias("-r").description("Delete a task").option("--force", "Skip confirmation prompt", false).action(async (id, options) => {
|
|
1200
|
+
const { render } = await import("ink");
|
|
1201
|
+
render(React11.createElement(RemoveUI, { id, force: options.force }));
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/commands/edit.tsx
|
|
1206
|
+
import React12, { useState as useState10, useEffect as useEffect9 } from "react";
|
|
1207
|
+
import { Box as Box13, Text as Text13, useApp as useApp8 } from "ink";
|
|
1208
|
+
import { Fragment as Fragment2, jsx as jsx13, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1209
|
+
function EditUI({ id, updates }) {
|
|
1210
|
+
const { exit } = useApp8();
|
|
1211
|
+
const [status, setStatus] = useState10("success");
|
|
1212
|
+
const [message, setMessage] = useState10("");
|
|
1213
|
+
const [changes, setChanges] = useState10([]);
|
|
1214
|
+
useEffect9(() => {
|
|
1215
|
+
const task = getTask(id);
|
|
1216
|
+
if (!task) {
|
|
1217
|
+
setMessage(`No task found with ID starting with "${id}"`);
|
|
1218
|
+
setStatus("error");
|
|
1219
|
+
setTimeout(() => exit(new Error("Task not found")), 1200);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
const taskUpdates = {};
|
|
1223
|
+
const changesLog = [];
|
|
1224
|
+
if (updates.description) {
|
|
1225
|
+
taskUpdates.description = updates.description;
|
|
1226
|
+
changesLog.push(`description \u2192 "${updates.description}"`);
|
|
1227
|
+
}
|
|
1228
|
+
if (updates.priority) {
|
|
1229
|
+
taskUpdates.priority = updates.priority;
|
|
1230
|
+
changesLog.push(`priority \u2192 ${updates.priority}`);
|
|
1231
|
+
}
|
|
1232
|
+
if (updates.status) {
|
|
1233
|
+
taskUpdates.status = updates.status;
|
|
1234
|
+
if (updates.status === "completed") {
|
|
1235
|
+
taskUpdates.completed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1236
|
+
}
|
|
1237
|
+
changesLog.push(`status \u2192 ${updates.status}`);
|
|
1238
|
+
}
|
|
1239
|
+
if (updates.due) {
|
|
1240
|
+
taskUpdates.due_date = updates.due;
|
|
1241
|
+
changesLog.push(`due date \u2192 ${updates.due}`);
|
|
1242
|
+
}
|
|
1243
|
+
if (updates.tags) {
|
|
1244
|
+
taskUpdates.tags = updates.tags.split(",").map((t) => t.trim());
|
|
1245
|
+
changesLog.push(`tags \u2192 [${taskUpdates.tags.join(", ")}]`);
|
|
1246
|
+
}
|
|
1247
|
+
if (updates.note) {
|
|
1248
|
+
taskUpdates.completion_note = updates.note;
|
|
1249
|
+
changesLog.push(`note \u2192 "${updates.note}"`);
|
|
1250
|
+
}
|
|
1251
|
+
if (changesLog.length === 0) {
|
|
1252
|
+
setMessage("No changes specified. Use options like --description, --priority, etc.");
|
|
1253
|
+
setStatus("error");
|
|
1254
|
+
setTimeout(() => exit(), 1200);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const updated = updateTask(task.id, taskUpdates);
|
|
1258
|
+
if (updated) {
|
|
1259
|
+
setChanges(changesLog);
|
|
1260
|
+
setMessage(`Task ${task.id.slice(0, 8)} updated!`);
|
|
1261
|
+
setStatus("success");
|
|
1262
|
+
setTimeout(() => exit(), 1500);
|
|
1263
|
+
}
|
|
1264
|
+
}, []);
|
|
1265
|
+
return /* @__PURE__ */ jsxs13(Box13, { flexDirection: "column", padding: 1, children: [
|
|
1266
|
+
/* @__PURE__ */ jsx13(MiniHeader, {}),
|
|
1267
|
+
/* @__PURE__ */ jsx13(Box13, { marginTop: 1, flexDirection: "column", children: status === "success" ? /* @__PURE__ */ jsxs13(Fragment2, { children: [
|
|
1268
|
+
/* @__PURE__ */ jsx13(StarBurst, { label: message, color: "magenta" }),
|
|
1269
|
+
changes.map((c) => /* @__PURE__ */ jsxs13(Box13, { marginTop: 0, children: [
|
|
1270
|
+
/* @__PURE__ */ jsx13(Text13, { color: "gray", children: " \u21B3 " }),
|
|
1271
|
+
/* @__PURE__ */ jsx13(Text13, { color: "white", children: c })
|
|
1272
|
+
] }, c))
|
|
1273
|
+
] }) : /* @__PURE__ */ jsx13(StatusBadge, { type: "error", message }) })
|
|
1274
|
+
] });
|
|
1275
|
+
}
|
|
1276
|
+
function registerEdit(program2) {
|
|
1277
|
+
program2.command("edit <id>").alias("-e").description("Update one or more fields of a task").option("--description <text>", "New description").option("--priority <level>", "New priority: high | medium | low").option("--status <status>", "New status: pending | in_progress | completed").option("--due <datetime>", "New due date").option("--tags <tags>", "New comma-separated tags").option("--note <note>", "Add a completion note").action(async (id, options) => {
|
|
1278
|
+
const { render } = await import("ink");
|
|
1279
|
+
render(React12.createElement(EditUI, { id, updates: options }));
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/commands/stats.tsx
|
|
1284
|
+
import React13, { useEffect as useEffect10 } from "react";
|
|
1285
|
+
import { Box as Box14, Text as Text14, useApp as useApp9 } from "ink";
|
|
1286
|
+
import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1287
|
+
function ProgressBar({
|
|
1288
|
+
value,
|
|
1289
|
+
max,
|
|
1290
|
+
width = 30,
|
|
1291
|
+
color = "cyan"
|
|
1292
|
+
}) {
|
|
1293
|
+
const filled = max > 0 ? Math.round(value / max * width) : 0;
|
|
1294
|
+
const empty = width - filled;
|
|
1295
|
+
return /* @__PURE__ */ jsxs14(Box14, { children: [
|
|
1296
|
+
/* @__PURE__ */ jsx14(Text14, { color: "gray", children: "\u2502" }),
|
|
1297
|
+
/* @__PURE__ */ jsx14(Text14, { color, children: "\u2588".repeat(filled) }),
|
|
1298
|
+
/* @__PURE__ */ jsx14(Text14, { color: "gray", children: "\u2591".repeat(empty) }),
|
|
1299
|
+
/* @__PURE__ */ jsx14(Text14, { color: "gray", children: "\u2502" })
|
|
1300
|
+
] });
|
|
1301
|
+
}
|
|
1302
|
+
function StatsUI() {
|
|
1303
|
+
const { exit } = useApp9();
|
|
1304
|
+
const stats = computeStats();
|
|
1305
|
+
useEffect10(() => {
|
|
1306
|
+
setTimeout(() => exit(), 100);
|
|
1307
|
+
}, []);
|
|
1308
|
+
const rows = [
|
|
1309
|
+
{ label: "Total tasks", value: stats.total, color: "white" },
|
|
1310
|
+
{ label: "Pending", value: stats.pending, color: "white" },
|
|
1311
|
+
{ label: "In Progress", value: stats.in_progress, color: "cyan" },
|
|
1312
|
+
{ label: "Completed", value: stats.completed, color: "green" },
|
|
1313
|
+
{ label: "Overdue", value: stats.overdue, color: stats.overdue > 0 ? "red" : "green" }
|
|
1314
|
+
];
|
|
1315
|
+
const priorityRows = [
|
|
1316
|
+
{ label: "\u{1F534} High Priority", value: stats.high_priority, color: "red" },
|
|
1317
|
+
{ label: "\u{1F7E1} Medium Priority", value: stats.medium_priority, color: "yellow" },
|
|
1318
|
+
{ label: "\u{1F7E2} Low Priority", value: stats.low_priority, color: "green" }
|
|
1319
|
+
];
|
|
1320
|
+
return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", padding: 1, children: [
|
|
1321
|
+
/* @__PURE__ */ jsx14(MiniHeader, {}),
|
|
1322
|
+
/* @__PURE__ */ jsx14(Box14, { marginBottom: 1, children: /* @__PURE__ */ jsx14(Text14, { color: "cyan", bold: true, children: "\u2726 Task Analytics" }) }),
|
|
1323
|
+
/* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", marginBottom: 1, children: [
|
|
1324
|
+
/* @__PURE__ */ jsxs14(Box14, { marginBottom: 0, children: [
|
|
1325
|
+
/* @__PURE__ */ jsx14(Text14, { color: "gray", children: "Completion Rate " }),
|
|
1326
|
+
/* @__PURE__ */ jsxs14(Text14, { color: "cyan", bold: true, children: [
|
|
1327
|
+
stats.completion_rate,
|
|
1328
|
+
"%"
|
|
1329
|
+
] })
|
|
1330
|
+
] }),
|
|
1331
|
+
/* @__PURE__ */ jsx14(ProgressBar, { value: stats.completion_rate, max: 100, color: "cyan" })
|
|
1332
|
+
] }),
|
|
1333
|
+
/* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", borderStyle: "round", borderColor: "#7B61FF", paddingX: 2, paddingY: 1, marginBottom: 1, children: [
|
|
1334
|
+
/* @__PURE__ */ jsx14(Text14, { color: "#7B61FF", bold: true, children: "Status Breakdown" }),
|
|
1335
|
+
rows.map((row) => /* @__PURE__ */ jsxs14(Box14, { children: [
|
|
1336
|
+
/* @__PURE__ */ jsxs14(Text14, { color: "gray", children: [
|
|
1337
|
+
" ",
|
|
1338
|
+
row.label.padEnd(16)
|
|
1339
|
+
] }),
|
|
1340
|
+
/* @__PURE__ */ jsx14(Text14, { color: row.color, bold: true, children: String(row.value).padStart(4) }),
|
|
1341
|
+
stats.total > 0 && /* @__PURE__ */ jsxs14(Fragment3, { children: [
|
|
1342
|
+
/* @__PURE__ */ jsx14(Text14, { color: "gray", children: " " }),
|
|
1343
|
+
/* @__PURE__ */ jsx14(
|
|
1344
|
+
ProgressBar,
|
|
1345
|
+
{
|
|
1346
|
+
value: row.value,
|
|
1347
|
+
max: stats.total,
|
|
1348
|
+
width: 20,
|
|
1349
|
+
color: row.color
|
|
1350
|
+
}
|
|
1351
|
+
)
|
|
1352
|
+
] })
|
|
1353
|
+
] }, row.label))
|
|
1354
|
+
] }),
|
|
1355
|
+
/* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", borderStyle: "round", borderColor: "#7B61FF", paddingX: 2, paddingY: 1, children: [
|
|
1356
|
+
/* @__PURE__ */ jsx14(Text14, { color: "#7B61FF", bold: true, children: "Priority Breakdown" }),
|
|
1357
|
+
priorityRows.map((row) => /* @__PURE__ */ jsxs14(Box14, { children: [
|
|
1358
|
+
/* @__PURE__ */ jsxs14(Text14, { color: "gray", children: [
|
|
1359
|
+
" ",
|
|
1360
|
+
row.label.padEnd(20)
|
|
1361
|
+
] }),
|
|
1362
|
+
/* @__PURE__ */ jsx14(Text14, { color: row.color, bold: true, children: String(row.value).padStart(4) }),
|
|
1363
|
+
stats.total > 0 && /* @__PURE__ */ jsxs14(Fragment3, { children: [
|
|
1364
|
+
/* @__PURE__ */ jsx14(Text14, { color: "gray", children: " " }),
|
|
1365
|
+
/* @__PURE__ */ jsx14(
|
|
1366
|
+
ProgressBar,
|
|
1367
|
+
{
|
|
1368
|
+
value: row.value,
|
|
1369
|
+
max: stats.total,
|
|
1370
|
+
width: 20,
|
|
1371
|
+
color: row.color
|
|
1372
|
+
}
|
|
1373
|
+
)
|
|
1374
|
+
] })
|
|
1375
|
+
] }, row.label))
|
|
1376
|
+
] }),
|
|
1377
|
+
stats.overdue > 0 && /* @__PURE__ */ jsxs14(Box14, { marginTop: 1, children: [
|
|
1378
|
+
/* @__PURE__ */ jsx14(Text14, { color: "red", bold: true, children: "\u26A0 " }),
|
|
1379
|
+
/* @__PURE__ */ jsxs14(Text14, { color: "red", children: [
|
|
1380
|
+
stats.overdue,
|
|
1381
|
+
" overdue task",
|
|
1382
|
+
stats.overdue > 1 ? "s" : "",
|
|
1383
|
+
" \u2014 check your schedule!"
|
|
1384
|
+
] })
|
|
1385
|
+
] }),
|
|
1386
|
+
stats.total === 0 && /* @__PURE__ */ jsxs14(Box14, { marginTop: 1, children: [
|
|
1387
|
+
/* @__PURE__ */ jsx14(Text14, { color: "gray", children: "No tasks yet. Run " }),
|
|
1388
|
+
/* @__PURE__ */ jsx14(Text14, { color: "cyan", children: 'taskair add "description"' }),
|
|
1389
|
+
/* @__PURE__ */ jsx14(Text14, { color: "gray", children: " to get started." })
|
|
1390
|
+
] })
|
|
1391
|
+
] });
|
|
1392
|
+
}
|
|
1393
|
+
function registerStats(program2) {
|
|
1394
|
+
program2.command("stats").description("Show task analytics and completion stats").action(async () => {
|
|
1395
|
+
const { render } = await import("ink");
|
|
1396
|
+
render(React13.createElement(StatsUI));
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/commands/sync.tsx
|
|
1401
|
+
import React14, { useState as useState11, useEffect as useEffect11 } from "react";
|
|
1402
|
+
import { Box as Box15, Text as Text15, useApp as useApp10 } from "ink";
|
|
1403
|
+
|
|
1404
|
+
// src/lib/crypto.ts
|
|
1405
|
+
import {
|
|
1406
|
+
pbkdf2Sync,
|
|
1407
|
+
createCipheriv,
|
|
1408
|
+
createDecipheriv,
|
|
1409
|
+
randomBytes,
|
|
1410
|
+
createHash
|
|
1411
|
+
} from "crypto";
|
|
1412
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
1413
|
+
var KEY_LENGTH = 32;
|
|
1414
|
+
var SALT_LENGTH = 16;
|
|
1415
|
+
var IV_LENGTH = 16;
|
|
1416
|
+
function deriveMasterKey(password, salt) {
|
|
1417
|
+
return pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
1418
|
+
}
|
|
1419
|
+
function deriveEncryptionKey(masterKey) {
|
|
1420
|
+
const hash = createHash("sha256");
|
|
1421
|
+
hash.update(masterKey);
|
|
1422
|
+
hash.update("taskair-encryption");
|
|
1423
|
+
return hash.digest();
|
|
1424
|
+
}
|
|
1425
|
+
function encrypt(plaintext, password) {
|
|
1426
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
1427
|
+
const iv = randomBytes(IV_LENGTH);
|
|
1428
|
+
const masterKey = deriveMasterKey(password, salt);
|
|
1429
|
+
const encKey = deriveEncryptionKey(masterKey);
|
|
1430
|
+
const cipher = createCipheriv("aes-256-gcm", encKey, iv);
|
|
1431
|
+
const encrypted = Buffer.concat([
|
|
1432
|
+
cipher.update(plaintext, "utf8"),
|
|
1433
|
+
cipher.final()
|
|
1434
|
+
]);
|
|
1435
|
+
const authTag = cipher.getAuthTag();
|
|
1436
|
+
return {
|
|
1437
|
+
version: "1",
|
|
1438
|
+
salt: salt.toString("base64"),
|
|
1439
|
+
iv: iv.toString("base64"),
|
|
1440
|
+
ciphertext: encrypted.toString("base64"),
|
|
1441
|
+
authTag: authTag.toString("base64")
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
function checksum(plaintext) {
|
|
1445
|
+
return createHash("sha256").update(plaintext).digest("hex");
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// src/commands/sync.tsx
|
|
1449
|
+
import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1450
|
+
function SyncUI({ dryRun, password }) {
|
|
1451
|
+
const { exit } = useApp10();
|
|
1452
|
+
const [status, setStatus] = useState11("loading");
|
|
1453
|
+
const [message, setMessage] = useState11("");
|
|
1454
|
+
const [details, setDetails] = useState11([]);
|
|
1455
|
+
useEffect11(() => {
|
|
1456
|
+
async function doSync() {
|
|
1457
|
+
try {
|
|
1458
|
+
const auth = requireAuth();
|
|
1459
|
+
const tasks = getAllTasks();
|
|
1460
|
+
const bundle = {
|
|
1461
|
+
tasks,
|
|
1462
|
+
schema_version: "1",
|
|
1463
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1464
|
+
};
|
|
1465
|
+
const plaintext = JSON.stringify(bundle);
|
|
1466
|
+
const blobChecksum = checksum(plaintext);
|
|
1467
|
+
if (dryRun) {
|
|
1468
|
+
const lastSync = getLastSyncedAt();
|
|
1469
|
+
setDetails([
|
|
1470
|
+
`Tasks to sync: ${tasks.length}`,
|
|
1471
|
+
`Checksum: ${blobChecksum.slice(0, 16)}\u2026`,
|
|
1472
|
+
`Last synced: ${lastSync ?? "never"}`,
|
|
1473
|
+
`API endpoint: ${auth.apiUrl}/sync/upload`,
|
|
1474
|
+
"(dry-run: no changes made)"
|
|
1475
|
+
]);
|
|
1476
|
+
setStatus("dry-run");
|
|
1477
|
+
setTimeout(() => exit(), 100);
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
if (!password) {
|
|
1481
|
+
setMessage("Encryption password required. Use --password flag.");
|
|
1482
|
+
setStatus("error");
|
|
1483
|
+
setTimeout(() => exit(new Error("Password required")), 1200);
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
const blob = encrypt(plaintext, password);
|
|
1487
|
+
const res = await apiUploadSync(
|
|
1488
|
+
auth.apiUrl,
|
|
1489
|
+
auth.accessToken,
|
|
1490
|
+
blob,
|
|
1491
|
+
blobChecksum,
|
|
1492
|
+
auth.deviceId
|
|
1493
|
+
);
|
|
1494
|
+
if (res.success) {
|
|
1495
|
+
clearSyncQueue();
|
|
1496
|
+
setMessage(`Synced ${tasks.length} task${tasks.length !== 1 ? "s" : ""} to cloud`);
|
|
1497
|
+
setDetails([
|
|
1498
|
+
`Encrypted with AES-256-GCM`,
|
|
1499
|
+
`Checksum: ${blobChecksum.slice(0, 16)}\u2026`
|
|
1500
|
+
]);
|
|
1501
|
+
setStatus("success");
|
|
1502
|
+
setTimeout(() => exit(), 1500);
|
|
1503
|
+
} else {
|
|
1504
|
+
setMessage(res.error?.message ?? "Sync failed");
|
|
1505
|
+
setStatus("error");
|
|
1506
|
+
setTimeout(() => exit(new Error(res.error?.message)), 1200);
|
|
1507
|
+
}
|
|
1508
|
+
} catch (e) {
|
|
1509
|
+
setMessage(e.message);
|
|
1510
|
+
setStatus("error");
|
|
1511
|
+
setTimeout(() => exit(e), 1200);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
doSync();
|
|
1515
|
+
}, []);
|
|
1516
|
+
return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", padding: 1, children: [
|
|
1517
|
+
/* @__PURE__ */ jsx15(MiniHeader, {}),
|
|
1518
|
+
/* @__PURE__ */ jsxs15(Box15, { marginTop: 1, flexDirection: "column", children: [
|
|
1519
|
+
status === "loading" && /* @__PURE__ */ jsx15(Spinner, { label: "Syncing to cloud (E2E encrypted)\u2026", type: "orbit", color: "magenta" }),
|
|
1520
|
+
status === "success" && /* @__PURE__ */ jsxs15(Fragment4, { children: [
|
|
1521
|
+
/* @__PURE__ */ jsx15(StarBurst, { label: message, color: "cyan" }),
|
|
1522
|
+
details.map((d) => /* @__PURE__ */ jsxs15(Box15, { children: [
|
|
1523
|
+
/* @__PURE__ */ jsx15(Text15, { color: "gray", children: " \xB7 " }),
|
|
1524
|
+
/* @__PURE__ */ jsx15(Text15, { color: "gray", dimColor: true, children: d })
|
|
1525
|
+
] }, d))
|
|
1526
|
+
] }),
|
|
1527
|
+
status === "error" && /* @__PURE__ */ jsx15(StatusBadge, { type: "error", message }),
|
|
1528
|
+
status === "dry-run" && /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
|
|
1529
|
+
/* @__PURE__ */ jsx15(StatusBadge, { type: "info", message: "Dry run \u2014 sync preview:" }),
|
|
1530
|
+
details.map((d) => /* @__PURE__ */ jsxs15(Box15, { children: [
|
|
1531
|
+
/* @__PURE__ */ jsx15(Text15, { color: "gray", children: " \xB7 " }),
|
|
1532
|
+
/* @__PURE__ */ jsx15(Text15, { color: "cyan", dimColor: true, children: d })
|
|
1533
|
+
] }, d))
|
|
1534
|
+
] })
|
|
1535
|
+
] })
|
|
1536
|
+
] });
|
|
1537
|
+
}
|
|
1538
|
+
function registerSync(program2) {
|
|
1539
|
+
program2.command("sync").description("Sync tasks with cloud (E2E encrypted)").option("--dry-run", "Preview what would be synced without making changes", false).option("--password <password>", "Master password for encryption").action(async (options) => {
|
|
1540
|
+
const { render } = await import("ink");
|
|
1541
|
+
render(React14.createElement(SyncUI, { dryRun: options.dryRun, password: options.password }));
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// src/commands/export.ts
|
|
1546
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
1547
|
+
function tasksToCSV(bundle) {
|
|
1548
|
+
const headers = ["id", "description", "priority", "status", "tags", "due_date", "created_at", "updated_at", "completed_at", "completion_note"];
|
|
1549
|
+
const rows = bundle.tasks.map((t) => [
|
|
1550
|
+
t.id,
|
|
1551
|
+
`"${t.description.replace(/"/g, '""')}"`,
|
|
1552
|
+
t.priority,
|
|
1553
|
+
t.status,
|
|
1554
|
+
`"${t.tags.join(";")}"`,
|
|
1555
|
+
t.due_date ?? "",
|
|
1556
|
+
t.created_at,
|
|
1557
|
+
t.updated_at,
|
|
1558
|
+
t.completed_at ?? "",
|
|
1559
|
+
t.completion_note ? `"${t.completion_note.replace(/"/g, '""')}"` : ""
|
|
1560
|
+
]);
|
|
1561
|
+
return [headers.join(","), ...rows.map((r) => r.join(","))].join("\n");
|
|
1562
|
+
}
|
|
1563
|
+
function tasksToMarkdown(bundle) {
|
|
1564
|
+
const lines = [
|
|
1565
|
+
"# TaskAir Export",
|
|
1566
|
+
`_Exported at: ${bundle.exported_at}_`,
|
|
1567
|
+
"",
|
|
1568
|
+
"| ID | Description | Priority | Status | Due | Tags |",
|
|
1569
|
+
"|---|---|---|---|---|---|"
|
|
1570
|
+
];
|
|
1571
|
+
for (const t of bundle.tasks) {
|
|
1572
|
+
lines.push(
|
|
1573
|
+
`| ${t.id.slice(0, 8)} | ${t.description} | ${t.priority} | ${t.status} | ${t.due_date ?? "\u2014"} | ${t.tags.join(", ")} |`
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
return lines.join("\n");
|
|
1577
|
+
}
|
|
1578
|
+
function registerExport(program2) {
|
|
1579
|
+
program2.command("export").description("Export tasks to JSON, CSV, or Markdown").option("-f, --format <format>", "Export format: json | csv | markdown", "json").option("-o, --output <file>", "Output file path (default: stdout)").action((options) => {
|
|
1580
|
+
const tasks = getAllTasks();
|
|
1581
|
+
const bundle = {
|
|
1582
|
+
tasks,
|
|
1583
|
+
schema_version: "1",
|
|
1584
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1585
|
+
};
|
|
1586
|
+
let content;
|
|
1587
|
+
switch (options.format) {
|
|
1588
|
+
case "csv":
|
|
1589
|
+
content = tasksToCSV(bundle);
|
|
1590
|
+
break;
|
|
1591
|
+
case "markdown":
|
|
1592
|
+
content = tasksToMarkdown(bundle);
|
|
1593
|
+
break;
|
|
1594
|
+
default:
|
|
1595
|
+
content = JSON.stringify(bundle, null, 2);
|
|
1596
|
+
}
|
|
1597
|
+
if (options.output) {
|
|
1598
|
+
writeFileSync3(options.output, content, "utf8");
|
|
1599
|
+
console.log(`\u2713 Exported ${tasks.length} tasks to ${options.output}`);
|
|
1600
|
+
} else {
|
|
1601
|
+
console.log(content);
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// src/index.ts
|
|
1607
|
+
program.name("taskair").description(
|
|
1608
|
+
"\u2726 Space-themed task management with E2E encryption \xB7 AI-native \xB7 Privacy-first"
|
|
1609
|
+
).version("1.0.0", "-v, --version", "Output the current version").helpOption("-h, --help", "Display help information");
|
|
1610
|
+
registerConfigure(program);
|
|
1611
|
+
registerLogin(program);
|
|
1612
|
+
registerLogout(program);
|
|
1613
|
+
registerWhoami(program);
|
|
1614
|
+
registerAdd(program);
|
|
1615
|
+
registerList(program);
|
|
1616
|
+
registerDone(program);
|
|
1617
|
+
registerRemove(program);
|
|
1618
|
+
registerEdit(program);
|
|
1619
|
+
registerStats(program);
|
|
1620
|
+
registerSync(program);
|
|
1621
|
+
registerExport(program);
|
|
1622
|
+
program.addHelpText(
|
|
1623
|
+
"beforeAll",
|
|
1624
|
+
`
|
|
1625
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
1626
|
+
\u2551 \u2726 T A S K A I R \u2014 Space-grade task management \u2726 \u2551
|
|
1627
|
+
\u2551 End-to-end encrypted \xB7 AI-native \xB7 Developer-first \u2551
|
|
1628
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
1629
|
+
`
|
|
1630
|
+
);
|
|
1631
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1632
|
+
console.error(`
|
|
1633
|
+
\u2717 ${err.message}`);
|
|
1634
|
+
process.exit(1);
|
|
1635
|
+
});
|