@tasklumina/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/dist/index.d.ts +1 -0
- package/dist/index.js +2865 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2865 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command13 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/auth.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/lib/config.ts
|
|
10
|
+
import { resolve } from "path";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { existsSync, readFileSync } from "fs";
|
|
13
|
+
function loadEnvFile(path) {
|
|
14
|
+
if (!existsSync(path)) return;
|
|
15
|
+
const content = readFileSync(path, "utf8");
|
|
16
|
+
for (const line of content.split("\n")) {
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
19
|
+
const eqIdx = trimmed.indexOf("=");
|
|
20
|
+
if (eqIdx === -1) continue;
|
|
21
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
22
|
+
const value = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, "");
|
|
23
|
+
if (!process.env[key]) process.env[key] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
loadEnvFile(resolve(homedir(), ".tasklumina", ".env"));
|
|
27
|
+
function requireEnv(key) {
|
|
28
|
+
const value = process.env[key];
|
|
29
|
+
if (!value) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Missing required environment variable: ${key}
|
|
32
|
+
Set it in ~/.tasklumina/.env or as an environment variable.
|
|
33
|
+
See cli/.env.example for required variables.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
var _envConfig = null;
|
|
39
|
+
function getEnvConfig() {
|
|
40
|
+
if (!_envConfig) {
|
|
41
|
+
_envConfig = {
|
|
42
|
+
region: requireEnv("TASKLUMINA_AWS_REGION"),
|
|
43
|
+
appsyncEndpoint: requireEnv("TASKLUMINA_APPSYNC_ENDPOINT")
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return _envConfig;
|
|
47
|
+
}
|
|
48
|
+
var CONFIG = {
|
|
49
|
+
get region() {
|
|
50
|
+
return getEnvConfig().region;
|
|
51
|
+
},
|
|
52
|
+
get appsyncEndpoint() {
|
|
53
|
+
return getEnvConfig().appsyncEndpoint;
|
|
54
|
+
},
|
|
55
|
+
tokenStorageKey: "tasklumina-cli",
|
|
56
|
+
autoRotateIntervalHours: 24
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/lib/crypto.ts
|
|
60
|
+
import {
|
|
61
|
+
createCipheriv,
|
|
62
|
+
createDecipheriv,
|
|
63
|
+
scryptSync,
|
|
64
|
+
randomBytes
|
|
65
|
+
} from "crypto";
|
|
66
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
67
|
+
import { resolve as resolve2 } from "path";
|
|
68
|
+
import { homedir as homedir2 } from "os";
|
|
69
|
+
var ALGO = "aes-256-gcm";
|
|
70
|
+
var SCRYPT_KEYLEN = 32;
|
|
71
|
+
var SCRYPT_COST = { N: 32768, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
|
|
72
|
+
function getMachineSecret() {
|
|
73
|
+
const dir = resolve2(homedir2(), ".tasklumina");
|
|
74
|
+
const keyFile = resolve2(dir, "machine-key");
|
|
75
|
+
try {
|
|
76
|
+
const secret2 = readFileSync2(keyFile, "utf8").trim();
|
|
77
|
+
if (secret2.length >= 32) return secret2;
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
81
|
+
const secret = randomBytes(32).toString("hex");
|
|
82
|
+
try {
|
|
83
|
+
writeFileSync(keyFile, secret, { mode: 384, flag: "wx" });
|
|
84
|
+
return secret;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err.code === "EEXIST") {
|
|
87
|
+
const existing = readFileSync2(keyFile, "utf8").trim();
|
|
88
|
+
if (existing.length >= 32) return existing;
|
|
89
|
+
}
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
var _cachedSecret = null;
|
|
94
|
+
function deriveKey(salt) {
|
|
95
|
+
if (!_cachedSecret) {
|
|
96
|
+
_cachedSecret = getMachineSecret();
|
|
97
|
+
}
|
|
98
|
+
return scryptSync(_cachedSecret, salt, SCRYPT_KEYLEN, SCRYPT_COST);
|
|
99
|
+
}
|
|
100
|
+
function encrypt(plaintext) {
|
|
101
|
+
const salt = randomBytes(16);
|
|
102
|
+
const key = deriveKey(salt);
|
|
103
|
+
const iv = randomBytes(16);
|
|
104
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
105
|
+
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
|
106
|
+
encrypted += cipher.final("hex");
|
|
107
|
+
const tag = cipher.getAuthTag();
|
|
108
|
+
return `enc2:${salt.toString("hex")}:${iv.toString("hex")}:${tag.toString("hex")}:${encrypted}`;
|
|
109
|
+
}
|
|
110
|
+
function decrypt(ciphertext) {
|
|
111
|
+
if (ciphertext.startsWith("enc2:")) {
|
|
112
|
+
const parts = ciphertext.slice(5).split(":");
|
|
113
|
+
if (parts.length !== 4) {
|
|
114
|
+
throw new Error("Malformed encrypted value");
|
|
115
|
+
}
|
|
116
|
+
const [saltHex, ivHex, tagHex, encrypted] = parts;
|
|
117
|
+
const key = deriveKey(Buffer.from(saltHex, "hex"));
|
|
118
|
+
const decipher = createDecipheriv(ALGO, key, Buffer.from(ivHex, "hex"));
|
|
119
|
+
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
120
|
+
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
|
121
|
+
decrypted += decipher.final("utf8");
|
|
122
|
+
return decrypted;
|
|
123
|
+
}
|
|
124
|
+
if (ciphertext.startsWith("enc:")) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"Token encrypted with legacy format (v1). Run `tasklumina login` to re-authenticate."
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
throw new Error("Not an encrypted value");
|
|
130
|
+
}
|
|
131
|
+
function isPlaintext(value) {
|
|
132
|
+
return !value.startsWith("enc2:") && !value.startsWith("enc:") && value.startsWith("{");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/lib/token-store.ts
|
|
136
|
+
var keytarModule = null;
|
|
137
|
+
var keytarFailed = false;
|
|
138
|
+
async function getKeytar() {
|
|
139
|
+
if (keytarFailed) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
"System keychain (keytar) is unavailable. Install it with: npm install keytar\nTask Lumina CLI requires a secure keychain \u2014 plaintext token storage is not supported."
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (keytarModule) return keytarModule;
|
|
145
|
+
try {
|
|
146
|
+
const mod = await import("keytar");
|
|
147
|
+
keytarModule = mod.default ?? mod;
|
|
148
|
+
return keytarModule;
|
|
149
|
+
} catch {
|
|
150
|
+
keytarFailed = true;
|
|
151
|
+
throw new Error(
|
|
152
|
+
"System keychain (keytar) is unavailable. Install it with: npm install keytar\nTask Lumina CLI requires a secure keychain \u2014 plaintext token storage is not supported."
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function loadApiKeyV1() {
|
|
157
|
+
const keytar = await getKeytar();
|
|
158
|
+
const raw = await keytar.getPassword(CONFIG.tokenStorageKey, "api-key");
|
|
159
|
+
if (!raw) return null;
|
|
160
|
+
if (isPlaintext(raw)) return raw;
|
|
161
|
+
return decrypt(raw);
|
|
162
|
+
}
|
|
163
|
+
async function clearApiKeyV1() {
|
|
164
|
+
const keytar = await getKeytar();
|
|
165
|
+
await keytar.deletePassword(CONFIG.tokenStorageKey, "api-key");
|
|
166
|
+
}
|
|
167
|
+
async function saveApiKeyData(data) {
|
|
168
|
+
const json = JSON.stringify(data);
|
|
169
|
+
const encrypted = encrypt(json);
|
|
170
|
+
const keytar = await getKeytar();
|
|
171
|
+
await keytar.setPassword(CONFIG.tokenStorageKey, "api-key-v2", encrypted);
|
|
172
|
+
}
|
|
173
|
+
async function loadApiKeyData() {
|
|
174
|
+
const keytar = await getKeytar();
|
|
175
|
+
const raw = await keytar.getPassword(CONFIG.tokenStorageKey, "api-key-v2");
|
|
176
|
+
if (raw) {
|
|
177
|
+
const json = decrypt(raw);
|
|
178
|
+
return JSON.parse(json);
|
|
179
|
+
}
|
|
180
|
+
const legacyKey = await loadApiKeyV1();
|
|
181
|
+
if (legacyKey) {
|
|
182
|
+
const data = { key: legacyKey, lastRotatedAt: (/* @__PURE__ */ new Date(0)).toISOString() };
|
|
183
|
+
await saveApiKeyData(data);
|
|
184
|
+
await clearApiKeyV1();
|
|
185
|
+
return data;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/lib/graphql-client.ts
|
|
191
|
+
async function getAuthToken() {
|
|
192
|
+
const envKey = process.env.TASKLUMINA_API_KEY;
|
|
193
|
+
if (envKey) return envKey;
|
|
194
|
+
const data = await loadApiKeyData();
|
|
195
|
+
if (data) return data.key;
|
|
196
|
+
throw new Error("No API key configured. Run `tasklumina configure` to set up authentication.");
|
|
197
|
+
}
|
|
198
|
+
var _verboseWarningShown = false;
|
|
199
|
+
function isVerbose() {
|
|
200
|
+
const verbose = process.argv.includes("--verbose");
|
|
201
|
+
if (verbose && !_verboseWarningShown) {
|
|
202
|
+
console.error("\u26A0 Verbose mode: debug output may expose operation names and timing in terminal/logs.");
|
|
203
|
+
_verboseWarningShown = true;
|
|
204
|
+
}
|
|
205
|
+
return verbose;
|
|
206
|
+
}
|
|
207
|
+
function extractOperationName(query) {
|
|
208
|
+
const match = query.match(/(?:query|mutation)\s+(\w+)/);
|
|
209
|
+
return match?.[1] || "unknown";
|
|
210
|
+
}
|
|
211
|
+
function maskEndpoint(url) {
|
|
212
|
+
return url.replace(/^(https:\/\/)[^.]+/, "$1***");
|
|
213
|
+
}
|
|
214
|
+
var SENSITIVE_KEYS = ["email", "password", "token", "accessToken", "idToken", "refreshToken", "sharedWithEmail", "ownerEmail", "key"];
|
|
215
|
+
function redactVariables(vars) {
|
|
216
|
+
const redacted = {};
|
|
217
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
218
|
+
if (SENSITIVE_KEYS.includes(key)) {
|
|
219
|
+
redacted[key] = "[REDACTED]";
|
|
220
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
221
|
+
redacted[key] = redactVariables(value);
|
|
222
|
+
} else {
|
|
223
|
+
redacted[key] = value;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return redacted;
|
|
227
|
+
}
|
|
228
|
+
async function graphql(query, variables) {
|
|
229
|
+
const token = await getAuthToken();
|
|
230
|
+
const verbose = isVerbose();
|
|
231
|
+
const opName = extractOperationName(query);
|
|
232
|
+
const startTime = Date.now();
|
|
233
|
+
if (verbose) {
|
|
234
|
+
console.error(`\u2192 POST ${maskEndpoint(CONFIG.appsyncEndpoint)}`);
|
|
235
|
+
console.error(` Operation: ${opName}`);
|
|
236
|
+
if (variables) console.error(` Variables: ${JSON.stringify(redactVariables(variables))}`);
|
|
237
|
+
}
|
|
238
|
+
let res;
|
|
239
|
+
try {
|
|
240
|
+
res = await fetch(CONFIG.appsyncEndpoint, {
|
|
241
|
+
method: "POST",
|
|
242
|
+
headers: {
|
|
243
|
+
"Content-Type": "application/json",
|
|
244
|
+
Authorization: token
|
|
245
|
+
},
|
|
246
|
+
body: JSON.stringify({ query, variables }),
|
|
247
|
+
signal: AbortSignal.timeout(3e4)
|
|
248
|
+
});
|
|
249
|
+
} catch (err) {
|
|
250
|
+
if (err instanceof DOMException && err.name === "TimeoutError") {
|
|
251
|
+
throw new Error("Request timed out after 30s. Check your connection.");
|
|
252
|
+
}
|
|
253
|
+
throw new Error("Cannot reach Task Lumina API. Check your connection.");
|
|
254
|
+
}
|
|
255
|
+
if (verbose) {
|
|
256
|
+
console.error(`\u2190 ${res.status} ${res.statusText} (${Date.now() - startTime}ms)`);
|
|
257
|
+
}
|
|
258
|
+
if (!res.ok) {
|
|
259
|
+
throw new Error(`GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
260
|
+
}
|
|
261
|
+
const json = await res.json();
|
|
262
|
+
if (json.errors?.length) {
|
|
263
|
+
const msg = json.errors[0].message;
|
|
264
|
+
if (msg.includes("Not Authorized") || msg.includes("Unauthorized")) {
|
|
265
|
+
throw new Error("Not authorized. Your API key may be revoked \u2014 run `tasklumina configure`.");
|
|
266
|
+
}
|
|
267
|
+
const MAX_ERROR_LEN = 200;
|
|
268
|
+
const sanitized = msg.length > MAX_ERROR_LEN ? msg.slice(0, MAX_ERROR_LEN) + "..." : msg;
|
|
269
|
+
if (verbose) {
|
|
270
|
+
console.error(` Server error: ${msg}`);
|
|
271
|
+
}
|
|
272
|
+
throw new Error(sanitized);
|
|
273
|
+
}
|
|
274
|
+
if (!json.data) {
|
|
275
|
+
throw new Error("No data returned from GraphQL");
|
|
276
|
+
}
|
|
277
|
+
return json.data;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/lib/output.ts
|
|
281
|
+
import chalk from "chalk";
|
|
282
|
+
import Table from "cli-table3";
|
|
283
|
+
function isJsonMode() {
|
|
284
|
+
return process.argv.includes("--json");
|
|
285
|
+
}
|
|
286
|
+
function isRedactMode() {
|
|
287
|
+
return process.argv.includes("--redact");
|
|
288
|
+
}
|
|
289
|
+
function printTable(headers, rows) {
|
|
290
|
+
if (isJsonMode()) return;
|
|
291
|
+
const table = new Table({ head: headers.map((h) => chalk.bold(h)) });
|
|
292
|
+
for (const row of rows) {
|
|
293
|
+
table.push(row);
|
|
294
|
+
}
|
|
295
|
+
console.log(table.toString());
|
|
296
|
+
}
|
|
297
|
+
function printJson(data) {
|
|
298
|
+
console.log(JSON.stringify(data, null, 2));
|
|
299
|
+
}
|
|
300
|
+
function printError(msg) {
|
|
301
|
+
console.error(chalk.red(`Error: ${msg}`));
|
|
302
|
+
}
|
|
303
|
+
function printSuccess(msg) {
|
|
304
|
+
console.log(chalk.green(msg));
|
|
305
|
+
}
|
|
306
|
+
function printWarning(msg) {
|
|
307
|
+
console.log(chalk.yellow(msg));
|
|
308
|
+
}
|
|
309
|
+
function formatDate(iso) {
|
|
310
|
+
if (!iso) return "\u2014";
|
|
311
|
+
const d = new Date(iso);
|
|
312
|
+
return d.toLocaleDateString("en-US", {
|
|
313
|
+
year: "numeric",
|
|
314
|
+
month: "short",
|
|
315
|
+
day: "numeric"
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function formatPriority(p) {
|
|
319
|
+
switch (p) {
|
|
320
|
+
case "critical":
|
|
321
|
+
return chalk.red.bold("CRITICAL");
|
|
322
|
+
case "high":
|
|
323
|
+
return chalk.hex("#f97316").bold("HIGH");
|
|
324
|
+
case "medium":
|
|
325
|
+
return chalk.yellow("MEDIUM");
|
|
326
|
+
case "low":
|
|
327
|
+
return chalk.gray("LOW");
|
|
328
|
+
default:
|
|
329
|
+
return p;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function formatStatus(s) {
|
|
333
|
+
switch (s) {
|
|
334
|
+
case "todo":
|
|
335
|
+
return chalk.white("To Do");
|
|
336
|
+
case "in_progress":
|
|
337
|
+
return chalk.blue("In Progress");
|
|
338
|
+
case "review":
|
|
339
|
+
return chalk.magenta("Review");
|
|
340
|
+
case "done":
|
|
341
|
+
return chalk.green("Done");
|
|
342
|
+
default:
|
|
343
|
+
return s;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function truncateId(id) {
|
|
347
|
+
return id.substring(0, 8);
|
|
348
|
+
}
|
|
349
|
+
function maskEmail(email) {
|
|
350
|
+
if (!isRedactMode()) return email;
|
|
351
|
+
const [local, domain] = email.split("@");
|
|
352
|
+
if (!domain) return email;
|
|
353
|
+
const [domainName, ...tld] = domain.split(".");
|
|
354
|
+
const maskedLocal = local[0] + "***";
|
|
355
|
+
const maskedDomain = domainName[0] + "***";
|
|
356
|
+
return `${maskedLocal}@${maskedDomain}.${tld.join(".")}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ../shared/graphql/queries.ts
|
|
360
|
+
var listTasks = (
|
|
361
|
+
/* GraphQL */
|
|
362
|
+
`
|
|
363
|
+
query ListTasks($status: TaskStatus, $archived: Boolean) {
|
|
364
|
+
listTasks(status: $status, archived: $archived) {
|
|
365
|
+
id
|
|
366
|
+
title
|
|
367
|
+
description
|
|
368
|
+
status
|
|
369
|
+
archived
|
|
370
|
+
deletedAt
|
|
371
|
+
priority
|
|
372
|
+
categoryId
|
|
373
|
+
color
|
|
374
|
+
tags
|
|
375
|
+
dueDate
|
|
376
|
+
dueTime
|
|
377
|
+
position
|
|
378
|
+
createdAt
|
|
379
|
+
updatedAt
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
`
|
|
383
|
+
);
|
|
384
|
+
var listCategories = (
|
|
385
|
+
/* GraphQL */
|
|
386
|
+
`
|
|
387
|
+
query ListCategories {
|
|
388
|
+
listCategories {
|
|
389
|
+
id
|
|
390
|
+
name
|
|
391
|
+
color
|
|
392
|
+
position
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
`
|
|
396
|
+
);
|
|
397
|
+
var listTags = (
|
|
398
|
+
/* GraphQL */
|
|
399
|
+
`
|
|
400
|
+
query ListTags {
|
|
401
|
+
listTags {
|
|
402
|
+
id
|
|
403
|
+
name
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
`
|
|
407
|
+
);
|
|
408
|
+
var listTaskShares = (
|
|
409
|
+
/* GraphQL */
|
|
410
|
+
`
|
|
411
|
+
query ListTaskShares($taskId: ID!) {
|
|
412
|
+
listTaskShares(taskId: $taskId) {
|
|
413
|
+
taskId
|
|
414
|
+
ownerId
|
|
415
|
+
ownerEmail
|
|
416
|
+
sharedWithId
|
|
417
|
+
sharedWithEmail
|
|
418
|
+
permission
|
|
419
|
+
status
|
|
420
|
+
sharedAt
|
|
421
|
+
respondedAt
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
`
|
|
425
|
+
);
|
|
426
|
+
var listSharedWithMe = (
|
|
427
|
+
/* GraphQL */
|
|
428
|
+
`
|
|
429
|
+
query ListSharedWithMe {
|
|
430
|
+
listSharedWithMe {
|
|
431
|
+
taskId
|
|
432
|
+
ownerId
|
|
433
|
+
ownerEmail
|
|
434
|
+
sharedWithId
|
|
435
|
+
sharedWithEmail
|
|
436
|
+
permission
|
|
437
|
+
status
|
|
438
|
+
sharedAt
|
|
439
|
+
respondedAt
|
|
440
|
+
task {
|
|
441
|
+
id
|
|
442
|
+
userId
|
|
443
|
+
title
|
|
444
|
+
description
|
|
445
|
+
status
|
|
446
|
+
archived
|
|
447
|
+
deletedAt
|
|
448
|
+
priority
|
|
449
|
+
categoryId
|
|
450
|
+
color
|
|
451
|
+
tags
|
|
452
|
+
dueDate
|
|
453
|
+
position
|
|
454
|
+
createdAt
|
|
455
|
+
updatedAt
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
`
|
|
460
|
+
);
|
|
461
|
+
var listMyOutgoingTaskShares = (
|
|
462
|
+
/* GraphQL */
|
|
463
|
+
`
|
|
464
|
+
query ListMyOutgoingTaskShares {
|
|
465
|
+
listMyOutgoingTaskShares {
|
|
466
|
+
taskId
|
|
467
|
+
ownerId
|
|
468
|
+
ownerEmail
|
|
469
|
+
sharedWithId
|
|
470
|
+
sharedWithEmail
|
|
471
|
+
permission
|
|
472
|
+
status
|
|
473
|
+
sharedAt
|
|
474
|
+
respondedAt
|
|
475
|
+
task {
|
|
476
|
+
id
|
|
477
|
+
userId
|
|
478
|
+
title
|
|
479
|
+
description
|
|
480
|
+
status
|
|
481
|
+
priority
|
|
482
|
+
dueDate
|
|
483
|
+
createdAt
|
|
484
|
+
updatedAt
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
`
|
|
489
|
+
);
|
|
490
|
+
var listLists = (
|
|
491
|
+
/* GraphQL */
|
|
492
|
+
`
|
|
493
|
+
query ListLists {
|
|
494
|
+
listLists {
|
|
495
|
+
id
|
|
496
|
+
name
|
|
497
|
+
description
|
|
498
|
+
icon
|
|
499
|
+
color
|
|
500
|
+
itemCount
|
|
501
|
+
createdAt
|
|
502
|
+
updatedAt
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
`
|
|
506
|
+
);
|
|
507
|
+
var listListItems = (
|
|
508
|
+
/* GraphQL */
|
|
509
|
+
`
|
|
510
|
+
query ListListItems($listId: ID!) {
|
|
511
|
+
listListItems(listId: $listId) {
|
|
512
|
+
id
|
|
513
|
+
listId
|
|
514
|
+
name
|
|
515
|
+
description
|
|
516
|
+
quantity
|
|
517
|
+
category
|
|
518
|
+
tags
|
|
519
|
+
link
|
|
520
|
+
customFields
|
|
521
|
+
position
|
|
522
|
+
createdAt
|
|
523
|
+
updatedAt
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
`
|
|
527
|
+
);
|
|
528
|
+
var listListShares = (
|
|
529
|
+
/* GraphQL */
|
|
530
|
+
`
|
|
531
|
+
query ListListShares($listId: ID!) {
|
|
532
|
+
listListShares(listId: $listId) {
|
|
533
|
+
listId
|
|
534
|
+
ownerId
|
|
535
|
+
ownerEmail
|
|
536
|
+
sharedWithId
|
|
537
|
+
sharedWithEmail
|
|
538
|
+
permission
|
|
539
|
+
status
|
|
540
|
+
sharedAt
|
|
541
|
+
respondedAt
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
`
|
|
545
|
+
);
|
|
546
|
+
var listListsSharedWithMe = (
|
|
547
|
+
/* GraphQL */
|
|
548
|
+
`
|
|
549
|
+
query ListListsSharedWithMe {
|
|
550
|
+
listListsSharedWithMe {
|
|
551
|
+
listId
|
|
552
|
+
ownerId
|
|
553
|
+
ownerEmail
|
|
554
|
+
sharedWithId
|
|
555
|
+
sharedWithEmail
|
|
556
|
+
permission
|
|
557
|
+
status
|
|
558
|
+
sharedAt
|
|
559
|
+
respondedAt
|
|
560
|
+
list {
|
|
561
|
+
id
|
|
562
|
+
name
|
|
563
|
+
description
|
|
564
|
+
icon
|
|
565
|
+
color
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
`
|
|
570
|
+
);
|
|
571
|
+
var listMyOutgoingListShares = (
|
|
572
|
+
/* GraphQL */
|
|
573
|
+
`
|
|
574
|
+
query ListMyOutgoingListShares {
|
|
575
|
+
listMyOutgoingListShares {
|
|
576
|
+
listId
|
|
577
|
+
ownerId
|
|
578
|
+
ownerEmail
|
|
579
|
+
sharedWithId
|
|
580
|
+
sharedWithEmail
|
|
581
|
+
permission
|
|
582
|
+
status
|
|
583
|
+
sharedAt
|
|
584
|
+
respondedAt
|
|
585
|
+
list {
|
|
586
|
+
id
|
|
587
|
+
name
|
|
588
|
+
description
|
|
589
|
+
icon
|
|
590
|
+
color
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
`
|
|
595
|
+
);
|
|
596
|
+
var listContacts = (
|
|
597
|
+
/* GraphQL */
|
|
598
|
+
`
|
|
599
|
+
query ListContacts {
|
|
600
|
+
listContacts {
|
|
601
|
+
id
|
|
602
|
+
name
|
|
603
|
+
email
|
|
604
|
+
createdAt
|
|
605
|
+
updatedAt
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
`
|
|
609
|
+
);
|
|
610
|
+
var listNotes = `
|
|
611
|
+
query ListNotes($taskId: ID!) {
|
|
612
|
+
listNotes(taskId: $taskId) {
|
|
613
|
+
id
|
|
614
|
+
taskId
|
|
615
|
+
content
|
|
616
|
+
authorId
|
|
617
|
+
authorName
|
|
618
|
+
createdAt
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
`;
|
|
622
|
+
var listApiKeys = (
|
|
623
|
+
/* GraphQL */
|
|
624
|
+
`
|
|
625
|
+
query ListApiKeys {
|
|
626
|
+
listApiKeys {
|
|
627
|
+
id
|
|
628
|
+
name
|
|
629
|
+
keyPrefix
|
|
630
|
+
status
|
|
631
|
+
createdAt
|
|
632
|
+
lastUsedAt
|
|
633
|
+
revokedAt
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
`
|
|
637
|
+
);
|
|
638
|
+
var getPreferences = `
|
|
639
|
+
query GetPreferences {
|
|
640
|
+
getPreferences {
|
|
641
|
+
theme
|
|
642
|
+
defaultView
|
|
643
|
+
archiveDelay
|
|
644
|
+
colorPalette
|
|
645
|
+
colorRules
|
|
646
|
+
notificationPrefs
|
|
647
|
+
filterPresets
|
|
648
|
+
defaultPriority
|
|
649
|
+
defaultCategoryId
|
|
650
|
+
defaultColor
|
|
651
|
+
updatedAt
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
`;
|
|
655
|
+
|
|
656
|
+
// src/commands/auth.ts
|
|
657
|
+
var whoamiCommand = new Command("whoami").description("Verify API key and show key info").action(async () => {
|
|
658
|
+
try {
|
|
659
|
+
const data = await loadApiKeyData();
|
|
660
|
+
if (!data) {
|
|
661
|
+
printError("No API key configured. Run `tasklumina configure` first.");
|
|
662
|
+
process.exitCode = 1;
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const result = await graphql(listApiKeys);
|
|
666
|
+
const activeKeys = result.listApiKeys.filter((k) => k.status === "active");
|
|
667
|
+
const prefix = data.keyPrefix ?? data.key.slice(0, 12);
|
|
668
|
+
if (isJsonMode()) {
|
|
669
|
+
printJson({
|
|
670
|
+
keyPrefix: prefix,
|
|
671
|
+
activeKeyCount: activeKeys.length,
|
|
672
|
+
lastRotatedAt: data.lastRotatedAt
|
|
673
|
+
});
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
printSuccess("API key is valid.");
|
|
677
|
+
console.log(` Key prefix: ${prefix}...`);
|
|
678
|
+
console.log(` Last rotated: ${data.lastRotatedAt}`);
|
|
679
|
+
console.log(` Active keys: ${activeKeys.length}`);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
printError(err.message);
|
|
682
|
+
process.exitCode = 1;
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// src/commands/configure.ts
|
|
687
|
+
import { Command as Command2 } from "commander";
|
|
688
|
+
import { password } from "@inquirer/prompts";
|
|
689
|
+
|
|
690
|
+
// src/lib/auto-rotate.ts
|
|
691
|
+
import { resolve as resolve3 } from "path";
|
|
692
|
+
import { homedir as homedir3 } from "os";
|
|
693
|
+
import { mkdirSync as mkdirSync2, rmSync, statSync } from "fs";
|
|
694
|
+
|
|
695
|
+
// ../shared/graphql/mutations.ts
|
|
696
|
+
var createTask = (
|
|
697
|
+
/* GraphQL */
|
|
698
|
+
`
|
|
699
|
+
mutation CreateTask($input: CreateTaskInput!) {
|
|
700
|
+
createTask(input: $input) {
|
|
701
|
+
id
|
|
702
|
+
title
|
|
703
|
+
description
|
|
704
|
+
status
|
|
705
|
+
archived
|
|
706
|
+
deletedAt
|
|
707
|
+
priority
|
|
708
|
+
categoryId
|
|
709
|
+
color
|
|
710
|
+
tags
|
|
711
|
+
dueDate
|
|
712
|
+
dueTime
|
|
713
|
+
position
|
|
714
|
+
createdAt
|
|
715
|
+
updatedAt
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
`
|
|
719
|
+
);
|
|
720
|
+
var updateTask = (
|
|
721
|
+
/* GraphQL */
|
|
722
|
+
`
|
|
723
|
+
mutation UpdateTask($input: UpdateTaskInput!) {
|
|
724
|
+
updateTask(input: $input) {
|
|
725
|
+
id
|
|
726
|
+
title
|
|
727
|
+
description
|
|
728
|
+
status
|
|
729
|
+
archived
|
|
730
|
+
deletedAt
|
|
731
|
+
priority
|
|
732
|
+
categoryId
|
|
733
|
+
color
|
|
734
|
+
tags
|
|
735
|
+
dueDate
|
|
736
|
+
dueTime
|
|
737
|
+
position
|
|
738
|
+
createdAt
|
|
739
|
+
updatedAt
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
`
|
|
743
|
+
);
|
|
744
|
+
var deleteTask = (
|
|
745
|
+
/* GraphQL */
|
|
746
|
+
`
|
|
747
|
+
mutation DeleteTask($id: ID!) {
|
|
748
|
+
deleteTask(id: $id)
|
|
749
|
+
}
|
|
750
|
+
`
|
|
751
|
+
);
|
|
752
|
+
var createCategory = (
|
|
753
|
+
/* GraphQL */
|
|
754
|
+
`
|
|
755
|
+
mutation CreateCategory($input: CreateCategoryInput!) {
|
|
756
|
+
createCategory(input: $input) {
|
|
757
|
+
id
|
|
758
|
+
name
|
|
759
|
+
color
|
|
760
|
+
position
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
`
|
|
764
|
+
);
|
|
765
|
+
var updateCategory = (
|
|
766
|
+
/* GraphQL */
|
|
767
|
+
`
|
|
768
|
+
mutation UpdateCategory($input: UpdateCategoryInput!) {
|
|
769
|
+
updateCategory(input: $input) {
|
|
770
|
+
id
|
|
771
|
+
name
|
|
772
|
+
color
|
|
773
|
+
position
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
`
|
|
777
|
+
);
|
|
778
|
+
var deleteCategory = (
|
|
779
|
+
/* GraphQL */
|
|
780
|
+
`
|
|
781
|
+
mutation DeleteCategory($id: ID!) {
|
|
782
|
+
deleteCategory(id: $id)
|
|
783
|
+
}
|
|
784
|
+
`
|
|
785
|
+
);
|
|
786
|
+
var createTag = (
|
|
787
|
+
/* GraphQL */
|
|
788
|
+
`
|
|
789
|
+
mutation CreateTag($input: CreateTagInput!) {
|
|
790
|
+
createTag(input: $input) {
|
|
791
|
+
id
|
|
792
|
+
name
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
`
|
|
796
|
+
);
|
|
797
|
+
var updateTag = (
|
|
798
|
+
/* GraphQL */
|
|
799
|
+
`
|
|
800
|
+
mutation UpdateTag($input: UpdateTagInput!) {
|
|
801
|
+
updateTag(input: $input) {
|
|
802
|
+
id
|
|
803
|
+
name
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
`
|
|
807
|
+
);
|
|
808
|
+
var deleteTagMutation = (
|
|
809
|
+
/* GraphQL */
|
|
810
|
+
`
|
|
811
|
+
mutation DeleteTag($id: ID!) {
|
|
812
|
+
deleteTag(id: $id)
|
|
813
|
+
}
|
|
814
|
+
`
|
|
815
|
+
);
|
|
816
|
+
var shareTask = (
|
|
817
|
+
/* GraphQL */
|
|
818
|
+
`
|
|
819
|
+
mutation ShareTask($input: ShareTaskInput!) {
|
|
820
|
+
shareTask(input: $input) {
|
|
821
|
+
taskId
|
|
822
|
+
ownerId
|
|
823
|
+
ownerEmail
|
|
824
|
+
sharedWithId
|
|
825
|
+
sharedWithEmail
|
|
826
|
+
permission
|
|
827
|
+
status
|
|
828
|
+
sharedAt
|
|
829
|
+
respondedAt
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
`
|
|
833
|
+
);
|
|
834
|
+
var acceptShare = (
|
|
835
|
+
/* GraphQL */
|
|
836
|
+
`
|
|
837
|
+
mutation AcceptShare($taskId: ID!, $ownerId: ID!) {
|
|
838
|
+
acceptShare(taskId: $taskId, ownerId: $ownerId) {
|
|
839
|
+
taskId
|
|
840
|
+
ownerId
|
|
841
|
+
ownerEmail
|
|
842
|
+
sharedWithId
|
|
843
|
+
sharedWithEmail
|
|
844
|
+
permission
|
|
845
|
+
status
|
|
846
|
+
sharedAt
|
|
847
|
+
respondedAt
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
`
|
|
851
|
+
);
|
|
852
|
+
var declineShare = (
|
|
853
|
+
/* GraphQL */
|
|
854
|
+
`
|
|
855
|
+
mutation DeclineShare($taskId: ID!, $ownerId: ID!) {
|
|
856
|
+
declineShare(taskId: $taskId, ownerId: $ownerId) {
|
|
857
|
+
taskId
|
|
858
|
+
ownerId
|
|
859
|
+
ownerEmail
|
|
860
|
+
sharedWithId
|
|
861
|
+
sharedWithEmail
|
|
862
|
+
permission
|
|
863
|
+
status
|
|
864
|
+
sharedAt
|
|
865
|
+
respondedAt
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
`
|
|
869
|
+
);
|
|
870
|
+
var revokeShare = (
|
|
871
|
+
/* GraphQL */
|
|
872
|
+
`
|
|
873
|
+
mutation RevokeShare($taskId: ID!, $sharedWithId: ID!) {
|
|
874
|
+
revokeShare(taskId: $taskId, sharedWithId: $sharedWithId)
|
|
875
|
+
}
|
|
876
|
+
`
|
|
877
|
+
);
|
|
878
|
+
var updateSharePermission = (
|
|
879
|
+
/* GraphQL */
|
|
880
|
+
`
|
|
881
|
+
mutation UpdateSharePermission($input: UpdateSharePermissionInput!) {
|
|
882
|
+
updateSharePermission(input: $input) {
|
|
883
|
+
taskId
|
|
884
|
+
ownerId
|
|
885
|
+
ownerEmail
|
|
886
|
+
sharedWithId
|
|
887
|
+
sharedWithEmail
|
|
888
|
+
permission
|
|
889
|
+
status
|
|
890
|
+
sharedAt
|
|
891
|
+
respondedAt
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
`
|
|
895
|
+
);
|
|
896
|
+
var createList = (
|
|
897
|
+
/* GraphQL */
|
|
898
|
+
`
|
|
899
|
+
mutation CreateList($input: CreateListInput!) {
|
|
900
|
+
createList(input: $input) {
|
|
901
|
+
id
|
|
902
|
+
name
|
|
903
|
+
description
|
|
904
|
+
icon
|
|
905
|
+
color
|
|
906
|
+
itemCount
|
|
907
|
+
createdAt
|
|
908
|
+
updatedAt
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
`
|
|
912
|
+
);
|
|
913
|
+
var updateList = (
|
|
914
|
+
/* GraphQL */
|
|
915
|
+
`
|
|
916
|
+
mutation UpdateList($input: UpdateListInput!) {
|
|
917
|
+
updateList(input: $input) {
|
|
918
|
+
id
|
|
919
|
+
name
|
|
920
|
+
description
|
|
921
|
+
icon
|
|
922
|
+
color
|
|
923
|
+
itemCount
|
|
924
|
+
createdAt
|
|
925
|
+
updatedAt
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
`
|
|
929
|
+
);
|
|
930
|
+
var deleteListMutation = (
|
|
931
|
+
/* GraphQL */
|
|
932
|
+
`
|
|
933
|
+
mutation DeleteList($id: ID!) {
|
|
934
|
+
deleteList(id: $id)
|
|
935
|
+
}
|
|
936
|
+
`
|
|
937
|
+
);
|
|
938
|
+
var createListItem = (
|
|
939
|
+
/* GraphQL */
|
|
940
|
+
`
|
|
941
|
+
mutation CreateListItem($input: CreateListItemInput!) {
|
|
942
|
+
createListItem(input: $input) {
|
|
943
|
+
id
|
|
944
|
+
listId
|
|
945
|
+
name
|
|
946
|
+
description
|
|
947
|
+
quantity
|
|
948
|
+
category
|
|
949
|
+
tags
|
|
950
|
+
link
|
|
951
|
+
customFields
|
|
952
|
+
position
|
|
953
|
+
createdAt
|
|
954
|
+
updatedAt
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
`
|
|
958
|
+
);
|
|
959
|
+
var updateListItem = (
|
|
960
|
+
/* GraphQL */
|
|
961
|
+
`
|
|
962
|
+
mutation UpdateListItem($input: UpdateListItemInput!) {
|
|
963
|
+
updateListItem(input: $input) {
|
|
964
|
+
id
|
|
965
|
+
listId
|
|
966
|
+
name
|
|
967
|
+
description
|
|
968
|
+
quantity
|
|
969
|
+
category
|
|
970
|
+
tags
|
|
971
|
+
link
|
|
972
|
+
customFields
|
|
973
|
+
position
|
|
974
|
+
createdAt
|
|
975
|
+
updatedAt
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
`
|
|
979
|
+
);
|
|
980
|
+
var deleteListItemMutation = (
|
|
981
|
+
/* GraphQL */
|
|
982
|
+
`
|
|
983
|
+
mutation DeleteListItem($id: ID!, $listId: ID!) {
|
|
984
|
+
deleteListItem(id: $id, listId: $listId)
|
|
985
|
+
}
|
|
986
|
+
`
|
|
987
|
+
);
|
|
988
|
+
var bulkCreateListItems = (
|
|
989
|
+
/* GraphQL */
|
|
990
|
+
`
|
|
991
|
+
mutation BulkCreateListItems($input: BulkCreateListItemsInput!) {
|
|
992
|
+
bulkCreateListItems(input: $input) {
|
|
993
|
+
id
|
|
994
|
+
listId
|
|
995
|
+
name
|
|
996
|
+
description
|
|
997
|
+
quantity
|
|
998
|
+
category
|
|
999
|
+
tags
|
|
1000
|
+
link
|
|
1001
|
+
customFields
|
|
1002
|
+
position
|
|
1003
|
+
createdAt
|
|
1004
|
+
updatedAt
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
`
|
|
1008
|
+
);
|
|
1009
|
+
var shareList = (
|
|
1010
|
+
/* GraphQL */
|
|
1011
|
+
`
|
|
1012
|
+
mutation ShareList($input: ShareListInput!) {
|
|
1013
|
+
shareList(input: $input) {
|
|
1014
|
+
listId
|
|
1015
|
+
ownerId
|
|
1016
|
+
ownerEmail
|
|
1017
|
+
sharedWithId
|
|
1018
|
+
sharedWithEmail
|
|
1019
|
+
permission
|
|
1020
|
+
status
|
|
1021
|
+
sharedAt
|
|
1022
|
+
respondedAt
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
`
|
|
1026
|
+
);
|
|
1027
|
+
var acceptListShare = (
|
|
1028
|
+
/* GraphQL */
|
|
1029
|
+
`
|
|
1030
|
+
mutation AcceptListShare($listId: ID!, $ownerId: ID!) {
|
|
1031
|
+
acceptListShare(listId: $listId, ownerId: $ownerId) {
|
|
1032
|
+
listId
|
|
1033
|
+
ownerId
|
|
1034
|
+
ownerEmail
|
|
1035
|
+
sharedWithId
|
|
1036
|
+
sharedWithEmail
|
|
1037
|
+
permission
|
|
1038
|
+
status
|
|
1039
|
+
sharedAt
|
|
1040
|
+
respondedAt
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
`
|
|
1044
|
+
);
|
|
1045
|
+
var declineListShare = (
|
|
1046
|
+
/* GraphQL */
|
|
1047
|
+
`
|
|
1048
|
+
mutation DeclineListShare($listId: ID!, $ownerId: ID!) {
|
|
1049
|
+
declineListShare(listId: $listId, ownerId: $ownerId) {
|
|
1050
|
+
listId
|
|
1051
|
+
ownerId
|
|
1052
|
+
ownerEmail
|
|
1053
|
+
sharedWithId
|
|
1054
|
+
sharedWithEmail
|
|
1055
|
+
permission
|
|
1056
|
+
status
|
|
1057
|
+
sharedAt
|
|
1058
|
+
respondedAt
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
`
|
|
1062
|
+
);
|
|
1063
|
+
var revokeListShare = (
|
|
1064
|
+
/* GraphQL */
|
|
1065
|
+
`
|
|
1066
|
+
mutation RevokeListShare($listId: ID!, $sharedWithId: ID!) {
|
|
1067
|
+
revokeListShare(listId: $listId, sharedWithId: $sharedWithId)
|
|
1068
|
+
}
|
|
1069
|
+
`
|
|
1070
|
+
);
|
|
1071
|
+
var updateListSharePermission = (
|
|
1072
|
+
/* GraphQL */
|
|
1073
|
+
`
|
|
1074
|
+
mutation UpdateListSharePermission($input: UpdateListSharePermissionInput!) {
|
|
1075
|
+
updateListSharePermission(input: $input) {
|
|
1076
|
+
listId
|
|
1077
|
+
ownerId
|
|
1078
|
+
ownerEmail
|
|
1079
|
+
sharedWithId
|
|
1080
|
+
sharedWithEmail
|
|
1081
|
+
permission
|
|
1082
|
+
status
|
|
1083
|
+
sharedAt
|
|
1084
|
+
respondedAt
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
`
|
|
1088
|
+
);
|
|
1089
|
+
var createNote = `
|
|
1090
|
+
mutation CreateNote($input: CreateNoteInput!) {
|
|
1091
|
+
createNote(input: $input) {
|
|
1092
|
+
id
|
|
1093
|
+
taskId
|
|
1094
|
+
content
|
|
1095
|
+
authorId
|
|
1096
|
+
authorName
|
|
1097
|
+
createdAt
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
`;
|
|
1101
|
+
var deleteNote = `
|
|
1102
|
+
mutation DeleteNote($taskId: ID!, $noteId: ID!, $createdAt: AWSDateTime!) {
|
|
1103
|
+
deleteNote(taskId: $taskId, noteId: $noteId, createdAt: $createdAt)
|
|
1104
|
+
}
|
|
1105
|
+
`;
|
|
1106
|
+
var createContact = (
|
|
1107
|
+
/* GraphQL */
|
|
1108
|
+
`
|
|
1109
|
+
mutation CreateContact($input: CreateContactInput!) {
|
|
1110
|
+
createContact(input: $input) {
|
|
1111
|
+
id
|
|
1112
|
+
name
|
|
1113
|
+
email
|
|
1114
|
+
createdAt
|
|
1115
|
+
updatedAt
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
`
|
|
1119
|
+
);
|
|
1120
|
+
var updateContact = (
|
|
1121
|
+
/* GraphQL */
|
|
1122
|
+
`
|
|
1123
|
+
mutation UpdateContact($input: UpdateContactInput!) {
|
|
1124
|
+
updateContact(input: $input) {
|
|
1125
|
+
id
|
|
1126
|
+
name
|
|
1127
|
+
email
|
|
1128
|
+
createdAt
|
|
1129
|
+
updatedAt
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
`
|
|
1133
|
+
);
|
|
1134
|
+
var deleteContactMutation = (
|
|
1135
|
+
/* GraphQL */
|
|
1136
|
+
`
|
|
1137
|
+
mutation DeleteContact($id: ID!) {
|
|
1138
|
+
deleteContact(id: $id)
|
|
1139
|
+
}
|
|
1140
|
+
`
|
|
1141
|
+
);
|
|
1142
|
+
var revokeApiKey = (
|
|
1143
|
+
/* GraphQL */
|
|
1144
|
+
`
|
|
1145
|
+
mutation RevokeApiKey($id: ID!) {
|
|
1146
|
+
revokeApiKey(id: $id) {
|
|
1147
|
+
id
|
|
1148
|
+
name
|
|
1149
|
+
keyPrefix
|
|
1150
|
+
status
|
|
1151
|
+
createdAt
|
|
1152
|
+
lastUsedAt
|
|
1153
|
+
revokedAt
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
`
|
|
1157
|
+
);
|
|
1158
|
+
var rotateSelfApiKey = (
|
|
1159
|
+
/* GraphQL */
|
|
1160
|
+
`
|
|
1161
|
+
mutation RotateSelfApiKey {
|
|
1162
|
+
rotateSelfApiKey {
|
|
1163
|
+
revokedKeyId
|
|
1164
|
+
newKey {
|
|
1165
|
+
id
|
|
1166
|
+
name
|
|
1167
|
+
keyPrefix
|
|
1168
|
+
key
|
|
1169
|
+
createdAt
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
`
|
|
1174
|
+
);
|
|
1175
|
+
var updatePreferences = `
|
|
1176
|
+
mutation UpdatePreferences($input: UpdatePreferencesInput!) {
|
|
1177
|
+
updatePreferences(input: $input) {
|
|
1178
|
+
theme
|
|
1179
|
+
defaultView
|
|
1180
|
+
archiveDelay
|
|
1181
|
+
colorPalette
|
|
1182
|
+
colorRules
|
|
1183
|
+
notificationPrefs
|
|
1184
|
+
filterPresets
|
|
1185
|
+
defaultPriority
|
|
1186
|
+
defaultCategoryId
|
|
1187
|
+
defaultColor
|
|
1188
|
+
updatedAt
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
`;
|
|
1192
|
+
|
|
1193
|
+
// src/lib/auto-rotate.ts
|
|
1194
|
+
var LOCK_DIR = resolve3(homedir3(), ".tasklumina", ".rotate-lock");
|
|
1195
|
+
var LOCK_STALE_MS = 6e4;
|
|
1196
|
+
function acquireLock() {
|
|
1197
|
+
try {
|
|
1198
|
+
try {
|
|
1199
|
+
const stat = statSync(LOCK_DIR);
|
|
1200
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
1201
|
+
rmSync(LOCK_DIR, { recursive: true, force: true });
|
|
1202
|
+
}
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
mkdirSync2(LOCK_DIR, { recursive: false });
|
|
1206
|
+
return true;
|
|
1207
|
+
} catch {
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
function releaseLock() {
|
|
1212
|
+
try {
|
|
1213
|
+
rmSync(LOCK_DIR, { recursive: true, force: true });
|
|
1214
|
+
} catch {
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
async function performSelfRotation() {
|
|
1218
|
+
const data = await graphql(rotateSelfApiKey);
|
|
1219
|
+
const result = data.rotateSelfApiKey.newKey;
|
|
1220
|
+
await saveApiKeyData({
|
|
1221
|
+
key: result.key,
|
|
1222
|
+
keyPrefix: result.keyPrefix,
|
|
1223
|
+
lastRotatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1224
|
+
});
|
|
1225
|
+
return result.key;
|
|
1226
|
+
}
|
|
1227
|
+
async function maybeAutoRotate() {
|
|
1228
|
+
try {
|
|
1229
|
+
if (process.env.TASKLUMINA_API_KEY) return;
|
|
1230
|
+
const data = await loadApiKeyData();
|
|
1231
|
+
if (!data) return;
|
|
1232
|
+
const lastRotated = new Date(data.lastRotatedAt).getTime();
|
|
1233
|
+
const hoursSince = (Date.now() - lastRotated) / (1e3 * 60 * 60);
|
|
1234
|
+
if (hoursSince < CONFIG.autoRotateIntervalHours) return;
|
|
1235
|
+
if (!acquireLock()) return;
|
|
1236
|
+
try {
|
|
1237
|
+
const freshData = await loadApiKeyData();
|
|
1238
|
+
if (freshData) {
|
|
1239
|
+
const freshHours = (Date.now() - new Date(freshData.lastRotatedAt).getTime()) / (1e3 * 60 * 60);
|
|
1240
|
+
if (freshHours < CONFIG.autoRotateIntervalHours) return;
|
|
1241
|
+
}
|
|
1242
|
+
await performSelfRotation();
|
|
1243
|
+
} finally {
|
|
1244
|
+
releaseLock();
|
|
1245
|
+
}
|
|
1246
|
+
} catch {
|
|
1247
|
+
releaseLock();
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/commands/configure.ts
|
|
1252
|
+
var configureCommand = new Command2("configure").description("Configure CLI authentication with an API key").action(async () => {
|
|
1253
|
+
try {
|
|
1254
|
+
const key = await password({ message: "Enter your API key:" });
|
|
1255
|
+
if (!key.startsWith("tl_live_")) {
|
|
1256
|
+
printError("Invalid API key format. Keys must start with tl_live_");
|
|
1257
|
+
process.exitCode = 1;
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
await saveApiKeyData({
|
|
1261
|
+
key,
|
|
1262
|
+
keyPrefix: key.slice(0, 12),
|
|
1263
|
+
lastRotatedAt: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
1264
|
+
});
|
|
1265
|
+
printWarning("Rotating key for security (the key you entered will be revoked)...");
|
|
1266
|
+
try {
|
|
1267
|
+
const newKey = await performSelfRotation();
|
|
1268
|
+
printSuccess("API key configured and rotated successfully.");
|
|
1269
|
+
console.log(` Active key prefix: ${newKey.slice(0, 12)}...`);
|
|
1270
|
+
printWarning("The key you entered has been revoked. Your CLI now uses a new key stored securely in the OS keychain.");
|
|
1271
|
+
} catch (rotateErr) {
|
|
1272
|
+
const msg = rotateErr instanceof Error ? rotateErr.message : "Unknown error";
|
|
1273
|
+
printWarning(`Key saved but auto-rotation failed: ${msg}`);
|
|
1274
|
+
printWarning("The original key is still active and stored in your OS keychain.");
|
|
1275
|
+
printWarning("Rotation will be attempted automatically on your next command.");
|
|
1276
|
+
}
|
|
1277
|
+
} catch (err) {
|
|
1278
|
+
printError(err.message);
|
|
1279
|
+
process.exitCode = 1;
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
// src/commands/tasks.ts
|
|
1284
|
+
import { Command as Command3 } from "commander";
|
|
1285
|
+
import { confirm } from "@inquirer/prompts";
|
|
1286
|
+
|
|
1287
|
+
// src/lib/validators.ts
|
|
1288
|
+
var VALID_STATUSES = ["todo", "in_progress", "review", "done"];
|
|
1289
|
+
var VALID_PRIORITIES = ["critical", "high", "medium", "low"];
|
|
1290
|
+
var VALID_PERMISSIONS = ["view", "edit"];
|
|
1291
|
+
var VALID_THEMES = ["light", "dark", "system"];
|
|
1292
|
+
var VALID_VIEWS = ["list", "kanban", "priority", "category"];
|
|
1293
|
+
function validateEnum(value, allowed, label) {
|
|
1294
|
+
if (!allowed.includes(value)) {
|
|
1295
|
+
throw new Error(`Invalid ${label}: "${value}". Must be one of: ${allowed.join(", ")}`);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
function validateStatus(value) {
|
|
1299
|
+
validateEnum(value, VALID_STATUSES, "status");
|
|
1300
|
+
}
|
|
1301
|
+
function validatePriority(value) {
|
|
1302
|
+
validateEnum(value, VALID_PRIORITIES, "priority");
|
|
1303
|
+
}
|
|
1304
|
+
function validatePermission(value) {
|
|
1305
|
+
validateEnum(value, VALID_PERMISSIONS, "permission");
|
|
1306
|
+
}
|
|
1307
|
+
function validateTheme(value) {
|
|
1308
|
+
validateEnum(value, VALID_THEMES, "theme");
|
|
1309
|
+
}
|
|
1310
|
+
function validateView(value) {
|
|
1311
|
+
validateEnum(value, VALID_VIEWS, "default view");
|
|
1312
|
+
}
|
|
1313
|
+
function validateTime(value) {
|
|
1314
|
+
if (!/^\d{2}:\d{2}$/.test(value)) {
|
|
1315
|
+
throw new Error(`Invalid time: "${value}". Expected format: HH:MM (e.g. 09:30)`);
|
|
1316
|
+
}
|
|
1317
|
+
const [h, m] = value.split(":").map(Number);
|
|
1318
|
+
if (h > 23 || m > 59) {
|
|
1319
|
+
throw new Error(`Invalid time: "${value}". Hours must be 0-23 and minutes 0-59.`);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function validateDate(value) {
|
|
1323
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
1324
|
+
throw new Error(`Invalid date: "${value}". Expected format: YYYY-MM-DD`);
|
|
1325
|
+
}
|
|
1326
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
1327
|
+
const d = new Date(value);
|
|
1328
|
+
if (isNaN(d.getTime()) || d.getUTCFullYear() !== year || d.getUTCMonth() + 1 !== month || d.getUTCDate() !== day) {
|
|
1329
|
+
throw new Error(`Invalid date: "${value}". Day does not exist in that month.`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function validateEmail(value) {
|
|
1333
|
+
if (value.length > 254) {
|
|
1334
|
+
throw new Error(`Email too long (max 254 characters): "${value}"`);
|
|
1335
|
+
}
|
|
1336
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
|
|
1337
|
+
throw new Error(`Invalid email address: "${value}"`);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
function validateUrl(value) {
|
|
1341
|
+
try {
|
|
1342
|
+
const url = new URL(value);
|
|
1343
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
1344
|
+
throw new Error("Only http and https URLs are allowed");
|
|
1345
|
+
}
|
|
1346
|
+
} catch {
|
|
1347
|
+
throw new Error(`Invalid URL: "${value}". Must be a valid http or https URL.`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
function validateUuid(value, label = "ID") {
|
|
1351
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
|
|
1352
|
+
if (!/^[0-9a-f]{6,}$/i.test(value)) {
|
|
1353
|
+
throw new Error(`Invalid ${label}: "${value}". Must be a UUID or at least a 6-character hex prefix.`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function validateStringLength(value, field, max) {
|
|
1358
|
+
if (value.length > max) {
|
|
1359
|
+
throw new Error(`${field} too long (${value.length} chars, max ${max}).`);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function validateHexColor(value) {
|
|
1363
|
+
if (!/^#[0-9A-Fa-f]{6}$/.test(value)) {
|
|
1364
|
+
throw new Error(`Invalid color: "${value}". Must be a hex color like #FF5733.`);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function validateQuantity(value) {
|
|
1368
|
+
const n = parseInt(value, 10);
|
|
1369
|
+
if (isNaN(n) || n < 0 || n > 99999) {
|
|
1370
|
+
throw new Error(`Invalid quantity: "${value}". Must be a number between 0 and 99999.`);
|
|
1371
|
+
}
|
|
1372
|
+
return n;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/commands/tasks.ts
|
|
1376
|
+
var PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
1377
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1378
|
+
async function resolveTaskId(idOrPrefix) {
|
|
1379
|
+
if (UUID_RE.test(idOrPrefix)) return idOrPrefix;
|
|
1380
|
+
const data = await graphql(listTasks, {});
|
|
1381
|
+
const archivedData = await graphql(listTasks, { archived: true });
|
|
1382
|
+
const allTasks = [...data.listTasks, ...archivedData.listTasks];
|
|
1383
|
+
const matches = allTasks.filter((t) => t.id.startsWith(idOrPrefix));
|
|
1384
|
+
if (matches.length === 0) throw new Error(`No task found matching ID prefix: ${idOrPrefix}`);
|
|
1385
|
+
if (matches.length > 1) {
|
|
1386
|
+
const list = matches.map((t) => ` ${truncateId(t.id)} \u2014 ${t.title}`).join("\n");
|
|
1387
|
+
throw new Error(`Ambiguous ID prefix "${idOrPrefix}" matches ${matches.length} tasks:
|
|
1388
|
+
${list}`);
|
|
1389
|
+
}
|
|
1390
|
+
return matches[0].id;
|
|
1391
|
+
}
|
|
1392
|
+
function escapeCsv(value) {
|
|
1393
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n")) {
|
|
1394
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
1395
|
+
}
|
|
1396
|
+
return value;
|
|
1397
|
+
}
|
|
1398
|
+
function tasksToCsv(tasks) {
|
|
1399
|
+
const headers = ["ID", "Title", "Status", "Priority", "Due Date", "Due Time", "Category", "Tags", "Created", "Updated"];
|
|
1400
|
+
const rows = tasks.map((t) => [
|
|
1401
|
+
t.id,
|
|
1402
|
+
t.title,
|
|
1403
|
+
t.status,
|
|
1404
|
+
t.priority,
|
|
1405
|
+
t.dueDate || "",
|
|
1406
|
+
t.dueTime || "",
|
|
1407
|
+
t.categoryId || "",
|
|
1408
|
+
t.tags.join(";"),
|
|
1409
|
+
t.createdAt,
|
|
1410
|
+
t.updatedAt
|
|
1411
|
+
].map(escapeCsv).join(","));
|
|
1412
|
+
return [headers.join(","), ...rows].join("\n");
|
|
1413
|
+
}
|
|
1414
|
+
var tasksCommand = new Command3("tasks").description("Manage tasks").option("--status <status>", "Filter by status (todo, in_progress, review, done)").option("--priority <priority>", "Filter by priority (critical, high, medium, low)").option("--completed", "Show completed (archived) tasks instead of active tasks").option("--archived", "Show all archived tasks including trash").option("--category <id>", "Filter by category ID").option("--tag <tag>", "Filter by tag name").option("--due-on <date>", "Tasks due on exactly this date (YYYY-MM-DD)").option("--due-before <date>", "Tasks due before date (YYYY-MM-DD)").option("--due-after <date>", "Tasks due after date (YYYY-MM-DD)").option("--overdue", "Show only overdue tasks").option("--sort <field>", "Sort by: due, priority, created, updated, title").option("--desc", "Sort descending (default: ascending)").option("--csv", "Export as CSV").action(async (opts) => {
|
|
1415
|
+
try {
|
|
1416
|
+
if (opts.status) validateStatus(opts.status);
|
|
1417
|
+
if (opts.priority) validatePriority(opts.priority);
|
|
1418
|
+
if (opts.category) validateUuid(opts.category, "Category ID");
|
|
1419
|
+
if (opts.dueOn) validateDate(opts.dueOn);
|
|
1420
|
+
if (opts.dueBefore) validateDate(opts.dueBefore);
|
|
1421
|
+
if (opts.dueAfter) validateDate(opts.dueAfter);
|
|
1422
|
+
const variables = {};
|
|
1423
|
+
if (opts.status) variables.status = opts.status;
|
|
1424
|
+
if (opts.completed || opts.archived) {
|
|
1425
|
+
variables.archived = true;
|
|
1426
|
+
} else {
|
|
1427
|
+
variables.archived = false;
|
|
1428
|
+
}
|
|
1429
|
+
const data = await graphql(listTasks, variables);
|
|
1430
|
+
let tasks = data.listTasks;
|
|
1431
|
+
if (opts.completed) {
|
|
1432
|
+
tasks = tasks.filter((t) => !t.deletedAt);
|
|
1433
|
+
}
|
|
1434
|
+
if (opts.dueOn) {
|
|
1435
|
+
tasks = tasks.filter((t) => t.dueDate === opts.dueOn);
|
|
1436
|
+
}
|
|
1437
|
+
if (opts.priority) {
|
|
1438
|
+
tasks = tasks.filter((t) => t.priority === opts.priority);
|
|
1439
|
+
}
|
|
1440
|
+
if (opts.category) {
|
|
1441
|
+
tasks = tasks.filter((t) => t.categoryId === opts.category);
|
|
1442
|
+
}
|
|
1443
|
+
if (opts.tag) {
|
|
1444
|
+
const tag = opts.tag.toLowerCase();
|
|
1445
|
+
tasks = tasks.filter((t) => t.tags.some((tg) => tg.toLowerCase() === tag));
|
|
1446
|
+
}
|
|
1447
|
+
if (opts.dueBefore) {
|
|
1448
|
+
const before = new Date(opts.dueBefore);
|
|
1449
|
+
tasks = tasks.filter((t) => t.dueDate && new Date(t.dueDate) <= before);
|
|
1450
|
+
}
|
|
1451
|
+
if (opts.dueAfter) {
|
|
1452
|
+
const after = new Date(opts.dueAfter);
|
|
1453
|
+
tasks = tasks.filter((t) => t.dueDate && new Date(t.dueDate) >= after);
|
|
1454
|
+
}
|
|
1455
|
+
if (opts.overdue) {
|
|
1456
|
+
const now = /* @__PURE__ */ new Date();
|
|
1457
|
+
tasks = tasks.filter((t) => {
|
|
1458
|
+
if (t.status === "done" || !t.dueDate) return false;
|
|
1459
|
+
const dueMs = t.dueTime ? (/* @__PURE__ */ new Date(`${t.dueDate}T${t.dueTime}`)).getTime() : (/* @__PURE__ */ new Date(`${t.dueDate}T23:59:59`)).getTime();
|
|
1460
|
+
return dueMs < now.getTime();
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
if (opts.sort) {
|
|
1464
|
+
const dir = opts.desc ? -1 : 1;
|
|
1465
|
+
tasks.sort((a, b) => {
|
|
1466
|
+
switch (opts.sort) {
|
|
1467
|
+
case "due": {
|
|
1468
|
+
if (!a.dueDate && !b.dueDate) return 0;
|
|
1469
|
+
if (!a.dueDate) return 1;
|
|
1470
|
+
if (!b.dueDate) return -1;
|
|
1471
|
+
return dir * (new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime());
|
|
1472
|
+
}
|
|
1473
|
+
case "priority":
|
|
1474
|
+
return dir * ((PRIORITY_ORDER[a.priority] ?? 9) - (PRIORITY_ORDER[b.priority] ?? 9));
|
|
1475
|
+
case "created":
|
|
1476
|
+
return dir * (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
1477
|
+
case "updated":
|
|
1478
|
+
return dir * (new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime());
|
|
1479
|
+
case "title":
|
|
1480
|
+
return dir * a.title.localeCompare(b.title);
|
|
1481
|
+
default:
|
|
1482
|
+
return 0;
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
if (opts.csv) {
|
|
1487
|
+
console.log(tasksToCsv(tasks));
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
if (isJsonMode()) {
|
|
1491
|
+
printJson(tasks);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
if (tasks.length === 0) {
|
|
1495
|
+
console.log("No tasks found.");
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
printTable(
|
|
1499
|
+
["ID", "Title", "Status", "Priority", "Due Date"],
|
|
1500
|
+
tasks.map((t) => [
|
|
1501
|
+
truncateId(t.id),
|
|
1502
|
+
t.title.length > 40 ? t.title.substring(0, 37) + "..." : t.title,
|
|
1503
|
+
formatStatus(t.status),
|
|
1504
|
+
formatPriority(t.priority),
|
|
1505
|
+
formatDate(t.dueDate)
|
|
1506
|
+
])
|
|
1507
|
+
);
|
|
1508
|
+
} catch (err) {
|
|
1509
|
+
printError(err.message);
|
|
1510
|
+
process.exitCode = 1;
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
tasksCommand.command("create").description("Create a new task").argument("<title>", "Task title").option("--desc <description>", "Task description").option("--status <status>", "Task status", "todo").option("--priority <priority>", "Task priority", "medium").option("--category <id>", "Category ID").option("--tags <tags>", "Comma-separated tags").option("--due <date>", "Due date (YYYY-MM-DD)").option("--due-time <time>", "Due time (HH:MM, 24-hour). Requires --due.").option("--color <color>", "Task color").action(async (title, opts) => {
|
|
1514
|
+
try {
|
|
1515
|
+
validateStringLength(title, "Title", 500);
|
|
1516
|
+
validateStatus(opts.status);
|
|
1517
|
+
validatePriority(opts.priority);
|
|
1518
|
+
if (opts.due) validateDate(opts.due);
|
|
1519
|
+
if (opts.dueTime) {
|
|
1520
|
+
if (!opts.due) {
|
|
1521
|
+
printError("--due-time requires --due to be set.");
|
|
1522
|
+
process.exitCode = 1;
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
validateTime(opts.dueTime);
|
|
1526
|
+
}
|
|
1527
|
+
if (opts.color) validateHexColor(opts.color);
|
|
1528
|
+
if (opts.desc) validateStringLength(opts.desc, "Description", 5e3);
|
|
1529
|
+
const taskInput = {
|
|
1530
|
+
title,
|
|
1531
|
+
status: opts.status,
|
|
1532
|
+
priority: opts.priority,
|
|
1533
|
+
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : []
|
|
1534
|
+
};
|
|
1535
|
+
if (opts.desc) taskInput.description = opts.desc;
|
|
1536
|
+
if (opts.category) taskInput.categoryId = opts.category;
|
|
1537
|
+
if (opts.due) taskInput.dueDate = opts.due;
|
|
1538
|
+
if (opts.dueTime) taskInput.dueTime = opts.dueTime;
|
|
1539
|
+
if (opts.color) taskInput.color = opts.color;
|
|
1540
|
+
const data = await graphql(createTask, { input: taskInput });
|
|
1541
|
+
if (isJsonMode()) {
|
|
1542
|
+
printJson(data.createTask);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
printSuccess(`Created task: ${data.createTask.title} (id: ${truncateId(data.createTask.id)})`);
|
|
1546
|
+
} catch (err) {
|
|
1547
|
+
printError(err.message);
|
|
1548
|
+
process.exitCode = 1;
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
tasksCommand.command("update").description("Update a task").argument("<id>", "Task ID").option("--title <title>", "New title").option("--desc <description>", "New description").option("--status <status>", "New status").option("--priority <priority>", "New priority").option("--category <id>", "New category ID").option("--tags <tags>", "New tags (comma-separated)").option("--due <date>", "New due date (YYYY-MM-DD)").option("--due-time <time>", "Set due time (HH:MM, 24-hour)").option("--all-day", "Remove due time (make all-day)").option("--color <color>", "New color").action(async (id, opts) => {
|
|
1552
|
+
try {
|
|
1553
|
+
validateUuid(id, "Task ID");
|
|
1554
|
+
const resolvedId = await resolveTaskId(id);
|
|
1555
|
+
if (opts.status) validateStatus(opts.status);
|
|
1556
|
+
if (opts.priority) validatePriority(opts.priority);
|
|
1557
|
+
if (opts.due) validateDate(opts.due);
|
|
1558
|
+
if (opts.dueTime) validateTime(opts.dueTime);
|
|
1559
|
+
if (opts.dueTime && opts.allDay) {
|
|
1560
|
+
printError("--due-time and --all-day are mutually exclusive.");
|
|
1561
|
+
process.exitCode = 1;
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
if (opts.color) validateHexColor(opts.color);
|
|
1565
|
+
if (opts.title) validateStringLength(opts.title, "Title", 500);
|
|
1566
|
+
if (opts.desc) validateStringLength(opts.desc, "Description", 5e3);
|
|
1567
|
+
const taskInput = { id: resolvedId };
|
|
1568
|
+
if (opts.title) taskInput.title = opts.title;
|
|
1569
|
+
if (opts.desc) taskInput.description = opts.desc;
|
|
1570
|
+
if (opts.status) taskInput.status = opts.status;
|
|
1571
|
+
if (opts.priority) taskInput.priority = opts.priority;
|
|
1572
|
+
if (opts.category) taskInput.categoryId = opts.category;
|
|
1573
|
+
if (opts.tags) taskInput.tags = opts.tags.split(",").map((t) => t.trim());
|
|
1574
|
+
if (opts.due) taskInput.dueDate = opts.due;
|
|
1575
|
+
if (opts.dueTime) taskInput.dueTime = opts.dueTime;
|
|
1576
|
+
if (opts.allDay) taskInput.dueTime = null;
|
|
1577
|
+
if (opts.color) taskInput.color = opts.color;
|
|
1578
|
+
const data = await graphql(updateTask, { input: taskInput });
|
|
1579
|
+
if (isJsonMode()) {
|
|
1580
|
+
printJson(data.updateTask);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
printSuccess(`Updated task: ${data.updateTask.title}`);
|
|
1584
|
+
} catch (err) {
|
|
1585
|
+
printError(err.message);
|
|
1586
|
+
process.exitCode = 1;
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
function addStatusShortcut(name, targetStatus, pastTense) {
|
|
1590
|
+
tasksCommand.command(name).description(`Mark a task as ${targetStatus.replace("_", " ")}`).argument("<id>", "Task ID").action(async (id) => {
|
|
1591
|
+
try {
|
|
1592
|
+
validateUuid(id, "Task ID");
|
|
1593
|
+
const resolvedId = await resolveTaskId(id);
|
|
1594
|
+
const data = await graphql(updateTask, {
|
|
1595
|
+
input: { id: resolvedId, status: targetStatus }
|
|
1596
|
+
});
|
|
1597
|
+
if (isJsonMode()) {
|
|
1598
|
+
printJson(data.updateTask);
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
printSuccess(`${pastTense}: ${data.updateTask.title}`);
|
|
1602
|
+
} catch (err) {
|
|
1603
|
+
printError(err.message);
|
|
1604
|
+
process.exitCode = 1;
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
addStatusShortcut("start", "in_progress", "Started");
|
|
1609
|
+
addStatusShortcut("review", "review", "Moved to review");
|
|
1610
|
+
addStatusShortcut("done", "done", "Marked done");
|
|
1611
|
+
tasksCommand.command("delete").description("Delete a task (soft delete)").argument("<id>", "Task ID").option("--force", "Skip confirmation").action(async (id, opts) => {
|
|
1612
|
+
try {
|
|
1613
|
+
validateUuid(id, "Task ID");
|
|
1614
|
+
const resolvedId = await resolveTaskId(id);
|
|
1615
|
+
if (!opts.force) {
|
|
1616
|
+
const ok = await confirm({ message: `Delete task ${truncateId(resolvedId)}?`, default: false });
|
|
1617
|
+
if (!ok) {
|
|
1618
|
+
console.log("Cancelled.");
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
await graphql(deleteTask, { id: resolvedId });
|
|
1623
|
+
if (isJsonMode()) {
|
|
1624
|
+
printJson({ deleted: resolvedId });
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
printSuccess(`Deleted task: ${truncateId(resolvedId)}`);
|
|
1628
|
+
} catch (err) {
|
|
1629
|
+
printError(err.message);
|
|
1630
|
+
process.exitCode = 1;
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
tasksCommand.command("show").description("Show detailed task info").argument("<id>", "Task ID").action(async (id) => {
|
|
1634
|
+
try {
|
|
1635
|
+
validateUuid(id, "Task ID");
|
|
1636
|
+
const resolvedId = await resolveTaskId(id);
|
|
1637
|
+
const data = await graphql(listTasks, {});
|
|
1638
|
+
const archivedData = await graphql(listTasks, { archived: true });
|
|
1639
|
+
const allTasks = [...data.listTasks, ...archivedData.listTasks];
|
|
1640
|
+
const task = allTasks.find((t) => t.id === resolvedId);
|
|
1641
|
+
if (!task) {
|
|
1642
|
+
printError(`Task not found: ${id}`);
|
|
1643
|
+
process.exitCode = 1;
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const [notesData, sharesData] = await Promise.all([
|
|
1647
|
+
graphql(
|
|
1648
|
+
listNotes,
|
|
1649
|
+
{ taskId: task.id }
|
|
1650
|
+
).catch(() => ({ listNotes: [] })),
|
|
1651
|
+
graphql(
|
|
1652
|
+
listTaskShares,
|
|
1653
|
+
{ taskId: task.id }
|
|
1654
|
+
).catch(() => ({ listTaskShares: [] }))
|
|
1655
|
+
]);
|
|
1656
|
+
if (isJsonMode()) {
|
|
1657
|
+
printJson({ ...task, notes: notesData.listNotes, shares: sharesData.listTaskShares });
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
console.log(`ID: ${task.id}`);
|
|
1661
|
+
console.log(`Title: ${task.title}`);
|
|
1662
|
+
console.log(`Status: ${formatStatus(task.status)}`);
|
|
1663
|
+
console.log(`Priority: ${formatPriority(task.priority)}`);
|
|
1664
|
+
console.log(`Description: ${task.description || "\u2014"}`);
|
|
1665
|
+
console.log(`Category: ${task.categoryId || "\u2014"}`);
|
|
1666
|
+
console.log(`Tags: ${task.tags.length ? task.tags.join(", ") : "\u2014"}`);
|
|
1667
|
+
console.log(`Due Date: ${formatDate(task.dueDate)}${task.dueTime ? ` at ${task.dueTime}` : task.dueDate ? " (all day)" : ""}`);
|
|
1668
|
+
console.log(`Color: ${task.color || "\u2014"}`);
|
|
1669
|
+
console.log(`Archived: ${task.archived ? "Yes" : "No"}`);
|
|
1670
|
+
console.log(`Created: ${formatDate(task.createdAt)}`);
|
|
1671
|
+
console.log(`Updated: ${formatDate(task.updatedAt)}`);
|
|
1672
|
+
if (notesData.listNotes.length > 0) {
|
|
1673
|
+
console.log(`
|
|
1674
|
+
Notes (${notesData.listNotes.length}):`);
|
|
1675
|
+
for (const note of notesData.listNotes) {
|
|
1676
|
+
console.log(` [${formatDate(note.createdAt)}] ${note.authorName}: ${note.content}`);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
if (sharesData.listTaskShares.length > 0) {
|
|
1680
|
+
console.log(`
|
|
1681
|
+
Shares (${sharesData.listTaskShares.length}):`);
|
|
1682
|
+
printTable(
|
|
1683
|
+
["Email", "Permission", "Status", "Shared At"],
|
|
1684
|
+
sharesData.listTaskShares.map((s) => [maskEmail(s.sharedWithEmail), s.permission, s.status, formatDate(s.sharedAt)])
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
printError(err.message);
|
|
1689
|
+
process.exitCode = 1;
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1693
|
+
// src/commands/categories.ts
|
|
1694
|
+
import { Command as Command4 } from "commander";
|
|
1695
|
+
import { randomUUID } from "crypto";
|
|
1696
|
+
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
1697
|
+
var categoriesCommand = new Command4("categories").description("Manage categories").action(async () => {
|
|
1698
|
+
try {
|
|
1699
|
+
const data = await graphql(listCategories);
|
|
1700
|
+
if (isJsonMode()) {
|
|
1701
|
+
printJson(data.listCategories);
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (data.listCategories.length === 0) {
|
|
1705
|
+
console.log("No categories.");
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
printTable(
|
|
1709
|
+
["ID", "Name", "Color"],
|
|
1710
|
+
data.listCategories.map((c) => [truncateId(c.id), c.name, c.color || "\u2014"])
|
|
1711
|
+
);
|
|
1712
|
+
} catch (err) {
|
|
1713
|
+
printError(err.message);
|
|
1714
|
+
process.exitCode = 1;
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
categoriesCommand.command("create").description("Create a category").argument("<name>", "Category name").option("--color <color>", "Category color (hex)").action(async (name, opts) => {
|
|
1718
|
+
try {
|
|
1719
|
+
validateStringLength(name, "Name", 100);
|
|
1720
|
+
if (opts.color) validateHexColor(opts.color);
|
|
1721
|
+
const input2 = { id: randomUUID(), name, position: Date.now() };
|
|
1722
|
+
if (opts.color) input2.color = opts.color;
|
|
1723
|
+
const data = await graphql(createCategory, { input: input2 });
|
|
1724
|
+
if (isJsonMode()) {
|
|
1725
|
+
printJson(data.createCategory);
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
printSuccess(`Created category: ${data.createCategory.name}`);
|
|
1729
|
+
} catch (err) {
|
|
1730
|
+
printError(err.message);
|
|
1731
|
+
process.exitCode = 1;
|
|
1732
|
+
}
|
|
1733
|
+
});
|
|
1734
|
+
categoriesCommand.command("update").description("Update a category").argument("<id>", "Category ID").option("--name <name>", "New name").option("--color <color>", "New color").action(async (id, opts) => {
|
|
1735
|
+
try {
|
|
1736
|
+
validateUuid(id, "Category ID");
|
|
1737
|
+
if (opts.name) validateStringLength(opts.name, "Name", 100);
|
|
1738
|
+
if (opts.color) validateHexColor(opts.color);
|
|
1739
|
+
const input2 = { id };
|
|
1740
|
+
if (opts.name) input2.name = opts.name;
|
|
1741
|
+
if (opts.color) input2.color = opts.color;
|
|
1742
|
+
const data = await graphql(updateCategory, { input: input2 });
|
|
1743
|
+
if (isJsonMode()) {
|
|
1744
|
+
printJson(data.updateCategory);
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
printSuccess(`Updated category: ${data.updateCategory.name}`);
|
|
1748
|
+
} catch (err) {
|
|
1749
|
+
printError(err.message);
|
|
1750
|
+
process.exitCode = 1;
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
categoriesCommand.command("delete").description("Delete a category").argument("<id>", "Category ID").option("--force", "Skip confirmation").action(async (id, opts) => {
|
|
1754
|
+
try {
|
|
1755
|
+
validateUuid(id, "Category ID");
|
|
1756
|
+
if (!opts.force) {
|
|
1757
|
+
const ok = await confirm2({ message: `Delete category ${truncateId(id)}?`, default: false });
|
|
1758
|
+
if (!ok) {
|
|
1759
|
+
console.log("Cancelled.");
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
await graphql(deleteCategory, { id });
|
|
1764
|
+
if (isJsonMode()) {
|
|
1765
|
+
printJson({ deleted: id });
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
printSuccess("Deleted category");
|
|
1769
|
+
} catch (err) {
|
|
1770
|
+
printError(err.message);
|
|
1771
|
+
process.exitCode = 1;
|
|
1772
|
+
}
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
// src/commands/tags.ts
|
|
1776
|
+
import { Command as Command5 } from "commander";
|
|
1777
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1778
|
+
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
1779
|
+
var tagsCommand = new Command5("tags").description("Manage tags").action(async () => {
|
|
1780
|
+
try {
|
|
1781
|
+
const data = await graphql(listTags);
|
|
1782
|
+
if (isJsonMode()) {
|
|
1783
|
+
printJson(data.listTags);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
if (data.listTags.length === 0) {
|
|
1787
|
+
console.log("No tags.");
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
printTable(["ID", "Name"], data.listTags.map((t) => [truncateId(t.id), t.name]));
|
|
1791
|
+
} catch (err) {
|
|
1792
|
+
printError(err.message);
|
|
1793
|
+
process.exitCode = 1;
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
tagsCommand.command("create").description("Create a tag").argument("<name>", "Tag name").action(async (name) => {
|
|
1797
|
+
try {
|
|
1798
|
+
validateStringLength(name, "Tag name", 100);
|
|
1799
|
+
const data = await graphql(createTag, { input: { id: randomUUID2(), name } });
|
|
1800
|
+
if (isJsonMode()) {
|
|
1801
|
+
printJson(data.createTag);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
printSuccess(`Created tag: ${data.createTag.name}`);
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
printError(err.message);
|
|
1807
|
+
process.exitCode = 1;
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
tagsCommand.command("update").description("Update a tag").argument("<id>", "Tag ID").option("--name <name>", "New name").action(async (id, opts) => {
|
|
1811
|
+
try {
|
|
1812
|
+
validateUuid(id, "Tag ID");
|
|
1813
|
+
if (opts.name) validateStringLength(opts.name, "Tag name", 100);
|
|
1814
|
+
const input2 = { id };
|
|
1815
|
+
if (opts.name) input2.name = opts.name;
|
|
1816
|
+
const data = await graphql(updateTag, { input: input2 });
|
|
1817
|
+
if (isJsonMode()) {
|
|
1818
|
+
printJson(data.updateTag);
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
printSuccess(`Updated tag: ${data.updateTag.name}`);
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
printError(err.message);
|
|
1824
|
+
process.exitCode = 1;
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
tagsCommand.command("delete").description("Delete a tag").argument("<id>", "Tag ID").option("--force", "Skip confirmation").action(async (id, opts) => {
|
|
1828
|
+
try {
|
|
1829
|
+
validateUuid(id, "Tag ID");
|
|
1830
|
+
if (!opts.force) {
|
|
1831
|
+
const ok = await confirm3({ message: `Delete tag ${truncateId(id)}?`, default: false });
|
|
1832
|
+
if (!ok) {
|
|
1833
|
+
console.log("Cancelled.");
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
await graphql(deleteTagMutation, { id });
|
|
1838
|
+
if (isJsonMode()) {
|
|
1839
|
+
printJson({ deleted: id });
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
printSuccess("Deleted tag");
|
|
1843
|
+
} catch (err) {
|
|
1844
|
+
printError(err.message);
|
|
1845
|
+
process.exitCode = 1;
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
|
|
1849
|
+
// src/commands/notes.ts
|
|
1850
|
+
import { Command as Command6 } from "commander";
|
|
1851
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1852
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
1853
|
+
var notesCommand = new Command6("notes").description("Manage task notes").argument("<task-id>", "Task ID").action(async (taskId) => {
|
|
1854
|
+
try {
|
|
1855
|
+
validateUuid(taskId, "Task ID");
|
|
1856
|
+
const data = await graphql(listNotes, { taskId });
|
|
1857
|
+
if (isJsonMode()) {
|
|
1858
|
+
printJson(data.listNotes);
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (data.listNotes.length === 0) {
|
|
1862
|
+
console.log("No notes.");
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
printTable(
|
|
1866
|
+
["ID", "Author", "Content", "Created"],
|
|
1867
|
+
data.listNotes.map((n) => [truncateId(n.id), n.authorName, n.content.length > 50 ? n.content.substring(0, 47) + "..." : n.content, formatDate(n.createdAt)])
|
|
1868
|
+
);
|
|
1869
|
+
} catch (err) {
|
|
1870
|
+
printError(err.message);
|
|
1871
|
+
process.exitCode = 1;
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
notesCommand.command("add").description("Add a note to a task").argument("<task-id>", "Task ID").argument("<content>", "Note content").action(async (taskId, content) => {
|
|
1875
|
+
try {
|
|
1876
|
+
validateUuid(taskId, "Task ID");
|
|
1877
|
+
validateStringLength(content, "Note content", 5e3);
|
|
1878
|
+
const data = await graphql(createNote, {
|
|
1879
|
+
input: { id: randomUUID3(), taskId, content }
|
|
1880
|
+
});
|
|
1881
|
+
if (isJsonMode()) {
|
|
1882
|
+
printJson(data.createNote);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
printSuccess("Added note to task");
|
|
1886
|
+
} catch (err) {
|
|
1887
|
+
printError(err.message);
|
|
1888
|
+
process.exitCode = 1;
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
notesCommand.command("delete").description("Delete a note").argument("<task-id>", "Task ID").argument("<note-id>", "Note ID").requiredOption("--created-at <date>", "Note creation date (ISO 8601)").option("--force", "Skip confirmation").action(async (taskId, noteId, opts) => {
|
|
1892
|
+
try {
|
|
1893
|
+
validateUuid(taskId, "Task ID");
|
|
1894
|
+
validateUuid(noteId, "Note ID");
|
|
1895
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(opts.createdAt)) {
|
|
1896
|
+
throw new Error("Invalid --created-at: must be an ISO 8601 datetime (e.g. 2024-01-15T10:30:00Z).");
|
|
1897
|
+
}
|
|
1898
|
+
if (!opts.force) {
|
|
1899
|
+
const ok = await confirm4({ message: "Delete this note?", default: false });
|
|
1900
|
+
if (!ok) {
|
|
1901
|
+
console.log("Cancelled.");
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
await graphql(deleteNote, { taskId, noteId, createdAt: opts.createdAt });
|
|
1906
|
+
if (isJsonMode()) {
|
|
1907
|
+
printJson({ deleted: noteId });
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
printSuccess("Deleted note");
|
|
1911
|
+
} catch (err) {
|
|
1912
|
+
printError(err.message);
|
|
1913
|
+
process.exitCode = 1;
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
|
|
1917
|
+
// src/commands/contacts.ts
|
|
1918
|
+
import { Command as Command7 } from "commander";
|
|
1919
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1920
|
+
import { confirm as confirm5 } from "@inquirer/prompts";
|
|
1921
|
+
var contactsCommand = new Command7("contacts").description("Manage contacts").action(async () => {
|
|
1922
|
+
try {
|
|
1923
|
+
const data = await graphql(listContacts);
|
|
1924
|
+
if (isJsonMode()) {
|
|
1925
|
+
printJson(data.listContacts);
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
if (data.listContacts.length === 0) {
|
|
1929
|
+
console.log("No contacts.");
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
printTable(
|
|
1933
|
+
["ID", "Name", "Email"],
|
|
1934
|
+
data.listContacts.map((c) => [truncateId(c.id), c.name, maskEmail(c.email)])
|
|
1935
|
+
);
|
|
1936
|
+
} catch (err) {
|
|
1937
|
+
printError(err.message);
|
|
1938
|
+
process.exitCode = 1;
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
contactsCommand.command("create").description("Create a contact").argument("<name>", "Contact name").requiredOption("--email <email>", "Contact email").action(async (name, opts) => {
|
|
1942
|
+
try {
|
|
1943
|
+
validateEmail(opts.email);
|
|
1944
|
+
validateStringLength(name, "Name", 200);
|
|
1945
|
+
const data = await graphql(createContact, {
|
|
1946
|
+
input: { id: randomUUID4(), name, email: opts.email }
|
|
1947
|
+
});
|
|
1948
|
+
if (isJsonMode()) {
|
|
1949
|
+
printJson(data.createContact);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
printSuccess(`Created contact: ${data.createContact.name}`);
|
|
1953
|
+
} catch (err) {
|
|
1954
|
+
printError(err.message);
|
|
1955
|
+
process.exitCode = 1;
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
contactsCommand.command("update").description("Update a contact").argument("<id>", "Contact ID").option("--name <name>", "New name").option("--email <email>", "New email").action(async (id, opts) => {
|
|
1959
|
+
try {
|
|
1960
|
+
validateUuid(id, "Contact ID");
|
|
1961
|
+
const input2 = { id };
|
|
1962
|
+
if (opts.name) {
|
|
1963
|
+
validateStringLength(opts.name, "Name", 200);
|
|
1964
|
+
input2.name = opts.name;
|
|
1965
|
+
}
|
|
1966
|
+
if (opts.email) {
|
|
1967
|
+
validateEmail(opts.email);
|
|
1968
|
+
input2.email = opts.email;
|
|
1969
|
+
}
|
|
1970
|
+
const data = await graphql(updateContact, { input: input2 });
|
|
1971
|
+
if (isJsonMode()) {
|
|
1972
|
+
printJson(data.updateContact);
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
printSuccess(`Updated contact: ${data.updateContact.name}`);
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
printError(err.message);
|
|
1978
|
+
process.exitCode = 1;
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
contactsCommand.command("delete").description("Delete a contact").argument("<id>", "Contact ID").option("--force", "Skip confirmation").action(async (id, opts) => {
|
|
1982
|
+
try {
|
|
1983
|
+
validateUuid(id, "Contact ID");
|
|
1984
|
+
if (!opts.force) {
|
|
1985
|
+
const ok = await confirm5({ message: `Delete contact ${truncateId(id)}?`, default: false });
|
|
1986
|
+
if (!ok) {
|
|
1987
|
+
console.log("Cancelled.");
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
await graphql(deleteContactMutation, { id });
|
|
1992
|
+
if (isJsonMode()) {
|
|
1993
|
+
printJson({ deleted: id });
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
printSuccess("Deleted contact");
|
|
1997
|
+
} catch (err) {
|
|
1998
|
+
printError(err.message);
|
|
1999
|
+
process.exitCode = 1;
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
// src/commands/prefs.ts
|
|
2004
|
+
import { Command as Command8 } from "commander";
|
|
2005
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2006
|
+
import chalk2 from "chalk";
|
|
2007
|
+
var prefsCommand = new Command8("prefs").description("Manage preferences").action(async () => {
|
|
2008
|
+
try {
|
|
2009
|
+
const data = await graphql(getPreferences);
|
|
2010
|
+
const p = data.getPreferences;
|
|
2011
|
+
if (isJsonMode()) {
|
|
2012
|
+
printJson(p);
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
printTable(
|
|
2016
|
+
["Setting", "Value"],
|
|
2017
|
+
[
|
|
2018
|
+
["Theme", p.theme],
|
|
2019
|
+
["Default View", p.defaultView],
|
|
2020
|
+
["Archive Delay (hours)", String(p.archiveDelay)],
|
|
2021
|
+
["Default Priority", p.defaultPriority || "\u2014"],
|
|
2022
|
+
["Default Category ID", p.defaultCategoryId ? truncateId(p.defaultCategoryId) : "\u2014"],
|
|
2023
|
+
["Default Color", p.defaultColor || "\u2014"]
|
|
2024
|
+
]
|
|
2025
|
+
);
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
printError(err.message);
|
|
2028
|
+
process.exitCode = 1;
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
prefsCommand.command("set").description("Update a preference").option("--theme <theme>", "Theme (light, dark, system)").option("--default-view <view>", "Default view (list, kanban, priority, category)").option("--archive-delay <hours>", "Archive delay in hours").option("--default-priority <priority>", 'Default task priority (critical, high, medium, low) or "none" to clear').option("--default-category <id>", 'Default category ID or "none" to clear').option("--default-color <color>", 'Default task color (hex, e.g. #FF5733) or "none" to clear').action(async (opts) => {
|
|
2032
|
+
try {
|
|
2033
|
+
if (opts.theme) validateTheme(opts.theme);
|
|
2034
|
+
if (opts.defaultView) validateView(opts.defaultView);
|
|
2035
|
+
if (opts.defaultPriority && opts.defaultPriority !== "none") validatePriority(opts.defaultPriority);
|
|
2036
|
+
if (opts.defaultColor && opts.defaultColor !== "none") validateHexColor(opts.defaultColor);
|
|
2037
|
+
const input2 = {};
|
|
2038
|
+
if (opts.theme) input2.theme = opts.theme;
|
|
2039
|
+
if (opts.defaultView) input2.defaultView = opts.defaultView;
|
|
2040
|
+
if (opts.archiveDelay) {
|
|
2041
|
+
const delay = parseInt(opts.archiveDelay, 10);
|
|
2042
|
+
if (isNaN(delay) || delay < 0 || delay > 8760) {
|
|
2043
|
+
printError("Archive delay must be between 0 and 8760 hours (1 year).");
|
|
2044
|
+
process.exitCode = 1;
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
input2.archiveDelay = delay;
|
|
2048
|
+
}
|
|
2049
|
+
if (opts.defaultPriority) input2.defaultPriority = opts.defaultPriority === "none" ? "" : opts.defaultPriority;
|
|
2050
|
+
if (opts.defaultCategory) input2.defaultCategoryId = opts.defaultCategory === "none" ? "" : opts.defaultCategory;
|
|
2051
|
+
if (opts.defaultColor) input2.defaultColor = opts.defaultColor === "none" ? "" : opts.defaultColor;
|
|
2052
|
+
if (Object.keys(input2).length === 0) {
|
|
2053
|
+
printError("Provide at least one option: --theme, --default-view, --archive-delay, --default-priority, --default-category, --default-color");
|
|
2054
|
+
process.exitCode = 1;
|
|
2055
|
+
return;
|
|
2056
|
+
}
|
|
2057
|
+
const data = await graphql(updatePreferences, { input: input2 });
|
|
2058
|
+
if (isJsonMode()) {
|
|
2059
|
+
printJson(data.updatePreferences);
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
for (const [key, value] of Object.entries(input2)) {
|
|
2063
|
+
printSuccess(`Updated preference: ${key} = ${value}`);
|
|
2064
|
+
}
|
|
2065
|
+
} catch (err) {
|
|
2066
|
+
printError(err.message);
|
|
2067
|
+
process.exitCode = 1;
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
var VALID_CONDITION_TYPES = ["tag", "priority", "category"];
|
|
2071
|
+
function isValidColorRule(item) {
|
|
2072
|
+
if (typeof item !== "object" || item === null) return false;
|
|
2073
|
+
const obj = item;
|
|
2074
|
+
return typeof obj.id === "string" && typeof obj.conditionType === "string" && VALID_CONDITION_TYPES.includes(obj.conditionType) && typeof obj.conditionValue === "string" && typeof obj.color === "string" && /^#[0-9A-Fa-f]{6}$/.test(obj.color);
|
|
2075
|
+
}
|
|
2076
|
+
async function loadColorRules() {
|
|
2077
|
+
const data = await graphql(getPreferences);
|
|
2078
|
+
const raw = data.getPreferences.colorRules;
|
|
2079
|
+
if (!raw) return [];
|
|
2080
|
+
const parsed = JSON.parse(raw);
|
|
2081
|
+
if (!Array.isArray(parsed)) return [];
|
|
2082
|
+
return parsed.filter(isValidColorRule);
|
|
2083
|
+
}
|
|
2084
|
+
async function saveColorRules(rules) {
|
|
2085
|
+
await graphql(updatePreferences, {
|
|
2086
|
+
input: { colorRules: JSON.stringify(rules) }
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
var colorRulesCommand = prefsCommand.command("color-rules").description("Manage automatic color rules").action(async () => {
|
|
2090
|
+
try {
|
|
2091
|
+
const rules = await loadColorRules();
|
|
2092
|
+
if (isJsonMode()) {
|
|
2093
|
+
printJson(rules);
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
if (rules.length === 0) {
|
|
2097
|
+
console.log("No color rules configured.");
|
|
2098
|
+
console.log(chalk2.dim("Add one: tasklumina prefs color-rules add --type priority --value critical --color #ef4444"));
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
printTable(
|
|
2102
|
+
["ID", "Type", "Value", "Color"],
|
|
2103
|
+
rules.map((r) => [
|
|
2104
|
+
truncateId(r.id),
|
|
2105
|
+
r.conditionType,
|
|
2106
|
+
r.conditionValue,
|
|
2107
|
+
`${chalk2.hex(r.color)("\u2588\u2588")} ${r.color}`
|
|
2108
|
+
])
|
|
2109
|
+
);
|
|
2110
|
+
} catch (err) {
|
|
2111
|
+
printError(err.message);
|
|
2112
|
+
process.exitCode = 1;
|
|
2113
|
+
}
|
|
2114
|
+
});
|
|
2115
|
+
colorRulesCommand.command("add").description("Add a color rule").requiredOption("--type <type>", "Condition type (tag, priority, category)").requiredOption("--value <value>", 'Condition value (e.g. "critical", "Work")').requiredOption("--color <hex>", "Color hex code (e.g. #ef4444)").action(async (opts) => {
|
|
2116
|
+
try {
|
|
2117
|
+
if (!VALID_CONDITION_TYPES.includes(opts.type)) {
|
|
2118
|
+
printError(`Invalid type: "${opts.type}". Must be one of: ${VALID_CONDITION_TYPES.join(", ")}`);
|
|
2119
|
+
process.exitCode = 1;
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
validateHexColor(opts.color);
|
|
2123
|
+
validateStringLength(opts.value, "Condition value", 200);
|
|
2124
|
+
const rules = await loadColorRules();
|
|
2125
|
+
const existing = rules.find(
|
|
2126
|
+
(r) => r.conditionType === opts.type && r.conditionValue === opts.value
|
|
2127
|
+
);
|
|
2128
|
+
if (existing) {
|
|
2129
|
+
printError(`A rule for ${opts.type} "${opts.value}" already exists (${truncateId(existing.id)}). Remove it first.`);
|
|
2130
|
+
process.exitCode = 1;
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const rule = {
|
|
2134
|
+
id: randomUUID5(),
|
|
2135
|
+
conditionType: opts.type,
|
|
2136
|
+
conditionValue: opts.value,
|
|
2137
|
+
color: opts.color
|
|
2138
|
+
};
|
|
2139
|
+
rules.push(rule);
|
|
2140
|
+
await saveColorRules(rules);
|
|
2141
|
+
if (isJsonMode()) {
|
|
2142
|
+
printJson(rule);
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
printSuccess(`Added color rule: ${opts.type} "${opts.value}" \u2192 ${chalk2.hex(opts.color)("\u2588\u2588")} ${opts.color}`);
|
|
2146
|
+
} catch (err) {
|
|
2147
|
+
printError(err.message);
|
|
2148
|
+
process.exitCode = 1;
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
colorRulesCommand.command("remove").description("Remove a color rule").argument("<id>", "Rule ID or prefix").action(async (id) => {
|
|
2152
|
+
try {
|
|
2153
|
+
const rules = await loadColorRules();
|
|
2154
|
+
const match = rules.filter((r) => r.id === id || r.id.startsWith(id));
|
|
2155
|
+
if (match.length === 0) {
|
|
2156
|
+
printError(`No color rule found matching "${id}".`);
|
|
2157
|
+
process.exitCode = 1;
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
if (match.length > 1) {
|
|
2161
|
+
printError(`Multiple rules match "${id}". Be more specific.`);
|
|
2162
|
+
process.exitCode = 1;
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
const removed = match[0];
|
|
2166
|
+
const updated = rules.filter((r) => r.id !== removed.id);
|
|
2167
|
+
await saveColorRules(updated);
|
|
2168
|
+
if (isJsonMode()) {
|
|
2169
|
+
printJson({ removed: removed.id });
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
printSuccess(`Removed color rule: ${removed.conditionType} "${removed.conditionValue}"`);
|
|
2173
|
+
} catch (err) {
|
|
2174
|
+
printError(err.message);
|
|
2175
|
+
process.exitCode = 1;
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
// src/commands/lists.ts
|
|
2180
|
+
import { Command as Command9 } from "commander";
|
|
2181
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2182
|
+
import { confirm as confirm6 } from "@inquirer/prompts";
|
|
2183
|
+
var listsCommand = new Command9("lists").description("Manage lists").action(async () => {
|
|
2184
|
+
try {
|
|
2185
|
+
const data = await graphql(listLists);
|
|
2186
|
+
if (isJsonMode()) {
|
|
2187
|
+
printJson(data.listLists);
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
if (data.listLists.length === 0) {
|
|
2191
|
+
console.log("No lists.");
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
printTable(
|
|
2195
|
+
["ID", "Name", "Items", "Created"],
|
|
2196
|
+
data.listLists.map((l) => [truncateId(l.id), l.name, String(l.itemCount), formatDate(l.createdAt)])
|
|
2197
|
+
);
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
printError(err.message);
|
|
2200
|
+
process.exitCode = 1;
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
listsCommand.command("show").description("Show a list with items").argument("<id>", "List ID").action(async (id) => {
|
|
2204
|
+
try {
|
|
2205
|
+
validateUuid(id, "List ID");
|
|
2206
|
+
const [listsData, itemsData] = await Promise.all([
|
|
2207
|
+
graphql(listLists),
|
|
2208
|
+
graphql(listListItems, { listId: id })
|
|
2209
|
+
]);
|
|
2210
|
+
const list = listsData.listLists.find((l) => l.id === id || l.id.startsWith(id));
|
|
2211
|
+
if (!list) {
|
|
2212
|
+
printError(`List not found: ${id}`);
|
|
2213
|
+
process.exitCode = 1;
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
if (isJsonMode()) {
|
|
2217
|
+
printJson({ ...list, items: itemsData.listListItems });
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
console.log(`ID: ${list.id}`);
|
|
2221
|
+
console.log(`Name: ${list.name}`);
|
|
2222
|
+
console.log(`Description: ${list.description || "\u2014"}`);
|
|
2223
|
+
console.log(`Icon: ${list.icon || "\u2014"}`);
|
|
2224
|
+
console.log(`Color: ${list.color || "\u2014"}`);
|
|
2225
|
+
console.log(`Items: ${list.itemCount}`);
|
|
2226
|
+
console.log(`Created: ${formatDate(list.createdAt)}`);
|
|
2227
|
+
if (itemsData.listListItems.length > 0) {
|
|
2228
|
+
console.log("\nItems:");
|
|
2229
|
+
printTable(
|
|
2230
|
+
["ID", "Name", "Qty", "Category"],
|
|
2231
|
+
itemsData.listListItems.map((i) => [truncateId(i.id), i.name, i.quantity != null ? String(i.quantity) : "\u2014", i.category || "\u2014"])
|
|
2232
|
+
);
|
|
2233
|
+
}
|
|
2234
|
+
} catch (err) {
|
|
2235
|
+
printError(err.message);
|
|
2236
|
+
process.exitCode = 1;
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
listsCommand.command("create").description("Create a list").argument("<name>", "List name").option("--desc <description>", "Description").option("--icon <icon>", "Icon emoji").option("--color <color>", "Color hex").action(async (name, opts) => {
|
|
2240
|
+
try {
|
|
2241
|
+
validateStringLength(name, "Name", 200);
|
|
2242
|
+
if (opts.desc) validateStringLength(opts.desc, "Description", 2e3);
|
|
2243
|
+
if (opts.color) validateHexColor(opts.color);
|
|
2244
|
+
const input2 = { id: randomUUID6(), name };
|
|
2245
|
+
if (opts.desc) input2.description = opts.desc;
|
|
2246
|
+
if (opts.icon) input2.icon = opts.icon;
|
|
2247
|
+
if (opts.color) input2.color = opts.color;
|
|
2248
|
+
const data = await graphql(createList, { input: input2 });
|
|
2249
|
+
if (isJsonMode()) {
|
|
2250
|
+
printJson(data.createList);
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
printSuccess(`Created list: ${data.createList.name}`);
|
|
2254
|
+
} catch (err) {
|
|
2255
|
+
printError(err.message);
|
|
2256
|
+
process.exitCode = 1;
|
|
2257
|
+
}
|
|
2258
|
+
});
|
|
2259
|
+
listsCommand.command("update").description("Update a list").argument("<id>", "List ID").option("--name <name>", "New name").option("--desc <description>", "New description").option("--icon <icon>", "New icon").option("--color <color>", "New color").action(async (id, opts) => {
|
|
2260
|
+
try {
|
|
2261
|
+
validateUuid(id, "List ID");
|
|
2262
|
+
if (opts.name) validateStringLength(opts.name, "Name", 200);
|
|
2263
|
+
if (opts.desc) validateStringLength(opts.desc, "Description", 2e3);
|
|
2264
|
+
if (opts.color) validateHexColor(opts.color);
|
|
2265
|
+
const input2 = { id };
|
|
2266
|
+
if (opts.name) input2.name = opts.name;
|
|
2267
|
+
if (opts.desc) input2.description = opts.desc;
|
|
2268
|
+
if (opts.icon) input2.icon = opts.icon;
|
|
2269
|
+
if (opts.color) input2.color = opts.color;
|
|
2270
|
+
const data = await graphql(updateList, { input: input2 });
|
|
2271
|
+
if (isJsonMode()) {
|
|
2272
|
+
printJson(data.updateList);
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
printSuccess(`Updated list: ${data.updateList.name}`);
|
|
2276
|
+
} catch (err) {
|
|
2277
|
+
printError(err.message);
|
|
2278
|
+
process.exitCode = 1;
|
|
2279
|
+
}
|
|
2280
|
+
});
|
|
2281
|
+
listsCommand.command("delete").description("Delete a list").argument("<id>", "List ID").option("--force", "Skip confirmation").action(async (id, opts) => {
|
|
2282
|
+
try {
|
|
2283
|
+
validateUuid(id, "List ID");
|
|
2284
|
+
if (!opts.force) {
|
|
2285
|
+
const ok = await confirm6({ message: `Delete list ${truncateId(id)}?`, default: false });
|
|
2286
|
+
if (!ok) {
|
|
2287
|
+
console.log("Cancelled.");
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
await graphql(deleteListMutation, { id });
|
|
2292
|
+
if (isJsonMode()) {
|
|
2293
|
+
printJson({ deleted: id });
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
printSuccess("Deleted list");
|
|
2297
|
+
} catch (err) {
|
|
2298
|
+
printError(err.message);
|
|
2299
|
+
process.exitCode = 1;
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
listsCommand.command("add-item").description("Add an item to a list").argument("<list-id>", "List ID").argument("<name>", "Item name").option("--qty <quantity>", "Quantity").option("--category <category>", "Category").option("--link <url>", "Link URL").action(async (listId, name, opts) => {
|
|
2303
|
+
try {
|
|
2304
|
+
validateUuid(listId, "List ID");
|
|
2305
|
+
validateStringLength(name, "Name", 500);
|
|
2306
|
+
if (opts.qty) validateQuantity(opts.qty);
|
|
2307
|
+
if (opts.link) validateUrl(opts.link);
|
|
2308
|
+
const input2 = { id: randomUUID6(), listId, name, tags: [], customFields: "{}", position: Date.now() };
|
|
2309
|
+
if (opts.qty) input2.quantity = validateQuantity(opts.qty);
|
|
2310
|
+
if (opts.category) input2.category = opts.category;
|
|
2311
|
+
if (opts.link) input2.link = opts.link;
|
|
2312
|
+
const data = await graphql(createListItem, { input: input2 });
|
|
2313
|
+
if (isJsonMode()) {
|
|
2314
|
+
printJson(data.createListItem);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
printSuccess(`Added item: ${data.createListItem.name}`);
|
|
2318
|
+
} catch (err) {
|
|
2319
|
+
printError(err.message);
|
|
2320
|
+
process.exitCode = 1;
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
listsCommand.command("bulk-add").description("Add multiple items to a list").argument("<list-id>", "List ID").argument("<items...>", "Item names").action(async (listId, items) => {
|
|
2324
|
+
try {
|
|
2325
|
+
validateUuid(listId, "List ID");
|
|
2326
|
+
for (const name of items) {
|
|
2327
|
+
validateStringLength(name, "Item name", 500);
|
|
2328
|
+
}
|
|
2329
|
+
const itemInputs = items.map((name, i) => ({
|
|
2330
|
+
id: randomUUID6(),
|
|
2331
|
+
listId,
|
|
2332
|
+
name,
|
|
2333
|
+
tags: [],
|
|
2334
|
+
customFields: "{}",
|
|
2335
|
+
position: Date.now() + i
|
|
2336
|
+
}));
|
|
2337
|
+
const data = await graphql(bulkCreateListItems, {
|
|
2338
|
+
input: { listId, items: itemInputs }
|
|
2339
|
+
});
|
|
2340
|
+
if (isJsonMode()) {
|
|
2341
|
+
printJson(data.bulkCreateListItems);
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
printSuccess(`Added ${data.bulkCreateListItems.length} items to list`);
|
|
2345
|
+
} catch (err) {
|
|
2346
|
+
printError(err.message);
|
|
2347
|
+
process.exitCode = 1;
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
2350
|
+
listsCommand.command("update-item").description("Update a list item").argument("<item-id>", "Item ID").requiredOption("--list <list-id>", "List ID").option("--name <name>", "New name").option("--qty <quantity>", "New quantity").option("--category <category>", "New category").option("--link <url>", "New link").action(async (itemId, opts) => {
|
|
2351
|
+
try {
|
|
2352
|
+
validateUuid(itemId, "Item ID");
|
|
2353
|
+
validateUuid(opts.list, "List ID");
|
|
2354
|
+
if (opts.name) validateStringLength(opts.name, "Name", 500);
|
|
2355
|
+
if (opts.qty) validateQuantity(opts.qty);
|
|
2356
|
+
if (opts.link) validateUrl(opts.link);
|
|
2357
|
+
const input2 = { id: itemId, listId: opts.list };
|
|
2358
|
+
if (opts.name) input2.name = opts.name;
|
|
2359
|
+
if (opts.qty) input2.quantity = validateQuantity(opts.qty);
|
|
2360
|
+
if (opts.category) input2.category = opts.category;
|
|
2361
|
+
if (opts.link) input2.link = opts.link;
|
|
2362
|
+
const data = await graphql(updateListItem, { input: input2 });
|
|
2363
|
+
if (isJsonMode()) {
|
|
2364
|
+
printJson(data.updateListItem);
|
|
2365
|
+
return;
|
|
2366
|
+
}
|
|
2367
|
+
printSuccess("Updated item");
|
|
2368
|
+
} catch (err) {
|
|
2369
|
+
printError(err.message);
|
|
2370
|
+
process.exitCode = 1;
|
|
2371
|
+
}
|
|
2372
|
+
});
|
|
2373
|
+
listsCommand.command("remove-item").description("Remove a list item").argument("<item-id>", "Item ID").requiredOption("--list <list-id>", "List ID").option("--force", "Skip confirmation").action(async (itemId, opts) => {
|
|
2374
|
+
try {
|
|
2375
|
+
validateUuid(itemId, "Item ID");
|
|
2376
|
+
validateUuid(opts.list, "List ID");
|
|
2377
|
+
if (!opts.force) {
|
|
2378
|
+
const ok = await confirm6({ message: "Remove this item?", default: false });
|
|
2379
|
+
if (!ok) {
|
|
2380
|
+
console.log("Cancelled.");
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
await graphql(deleteListItemMutation, { id: itemId, listId: opts.list });
|
|
2385
|
+
if (isJsonMode()) {
|
|
2386
|
+
printJson({ deleted: itemId });
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
printSuccess("Removed item");
|
|
2390
|
+
} catch (err) {
|
|
2391
|
+
printError(err.message);
|
|
2392
|
+
process.exitCode = 1;
|
|
2393
|
+
}
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
// src/commands/shares.ts
|
|
2397
|
+
import { Command as Command10 } from "commander";
|
|
2398
|
+
async function resolveEmail(opts) {
|
|
2399
|
+
if (opts.email) {
|
|
2400
|
+
validateEmail(opts.email);
|
|
2401
|
+
return opts.email;
|
|
2402
|
+
}
|
|
2403
|
+
if (opts.contact) {
|
|
2404
|
+
const data = await graphql(listContacts);
|
|
2405
|
+
const name = opts.contact.toLowerCase();
|
|
2406
|
+
const match = data.listContacts.filter((c) => c.name.toLowerCase().includes(name));
|
|
2407
|
+
if (match.length === 0) {
|
|
2408
|
+
throw new Error(`No contact found matching "${opts.contact}". Run \`tasklumina contacts\` to see your contacts.`);
|
|
2409
|
+
}
|
|
2410
|
+
if (match.length > 1) {
|
|
2411
|
+
const names = match.map((c) => ` ${c.name} (${c.email})`).join("\n");
|
|
2412
|
+
throw new Error(`Multiple contacts match "${opts.contact}":
|
|
2413
|
+
${names}
|
|
2414
|
+
Be more specific or use --email instead.`);
|
|
2415
|
+
}
|
|
2416
|
+
return match[0].email;
|
|
2417
|
+
}
|
|
2418
|
+
throw new Error("Provide --email or --contact");
|
|
2419
|
+
}
|
|
2420
|
+
var sharesCommand = new Command10("shares").description("Manage task and list sharing");
|
|
2421
|
+
sharesCommand.command("send").description("Share a task with someone").argument("<task-id>", "Task ID").option("--email <email>", "Recipient email").option("--contact <name>", "Recipient contact name").option("--permission <perm>", "Permission (view or edit)", "view").action(async (taskId, opts) => {
|
|
2422
|
+
try {
|
|
2423
|
+
validateUuid(taskId, "Task ID");
|
|
2424
|
+
const email = await resolveEmail(opts);
|
|
2425
|
+
validatePermission(opts.permission);
|
|
2426
|
+
const data = await graphql(shareTask, {
|
|
2427
|
+
input: { taskId, sharedWithEmail: email, permission: opts.permission }
|
|
2428
|
+
});
|
|
2429
|
+
if (isJsonMode()) {
|
|
2430
|
+
printJson(data.shareTask);
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
printSuccess(`Shared task with ${maskEmail(email)} (${opts.permission})`);
|
|
2434
|
+
} catch (err) {
|
|
2435
|
+
printError(err.message);
|
|
2436
|
+
process.exitCode = 1;
|
|
2437
|
+
}
|
|
2438
|
+
});
|
|
2439
|
+
sharesCommand.command("inbox").description("List tasks shared with me").action(async () => {
|
|
2440
|
+
try {
|
|
2441
|
+
const data = await graphql(listSharedWithMe);
|
|
2442
|
+
if (isJsonMode()) {
|
|
2443
|
+
printJson(data.listSharedWithMe);
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
if (data.listSharedWithMe.length === 0) {
|
|
2447
|
+
console.log("No shared tasks.");
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
printTable(
|
|
2451
|
+
["Task", "Owner", "Permission", "Status"],
|
|
2452
|
+
data.listSharedWithMe.map((s) => [
|
|
2453
|
+
s.task?.title || truncateId(s.taskId),
|
|
2454
|
+
maskEmail(s.ownerEmail),
|
|
2455
|
+
s.permission,
|
|
2456
|
+
s.status
|
|
2457
|
+
])
|
|
2458
|
+
);
|
|
2459
|
+
} catch (err) {
|
|
2460
|
+
printError(err.message);
|
|
2461
|
+
process.exitCode = 1;
|
|
2462
|
+
}
|
|
2463
|
+
});
|
|
2464
|
+
sharesCommand.command("accept").description("Accept a shared task").argument("<task-id>", "Task ID").requiredOption("--owner <owner-id>", "Owner user ID").action(async (taskId, opts) => {
|
|
2465
|
+
try {
|
|
2466
|
+
validateUuid(taskId, "Task ID");
|
|
2467
|
+
validateUuid(opts.owner, "Owner ID");
|
|
2468
|
+
const data = await graphql(acceptShare, { taskId, ownerId: opts.owner });
|
|
2469
|
+
if (isJsonMode()) {
|
|
2470
|
+
printJson(data.acceptShare);
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
printSuccess("Accepted share");
|
|
2474
|
+
} catch (err) {
|
|
2475
|
+
printError(err.message);
|
|
2476
|
+
process.exitCode = 1;
|
|
2477
|
+
}
|
|
2478
|
+
});
|
|
2479
|
+
sharesCommand.command("decline").description("Decline a shared task").argument("<task-id>", "Task ID").requiredOption("--owner <owner-id>", "Owner user ID").action(async (taskId, opts) => {
|
|
2480
|
+
try {
|
|
2481
|
+
validateUuid(taskId, "Task ID");
|
|
2482
|
+
validateUuid(opts.owner, "Owner ID");
|
|
2483
|
+
const data = await graphql(declineShare, { taskId, ownerId: opts.owner });
|
|
2484
|
+
if (isJsonMode()) {
|
|
2485
|
+
printJson(data.declineShare);
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
printSuccess("Declined share");
|
|
2489
|
+
} catch (err) {
|
|
2490
|
+
printError(err.message);
|
|
2491
|
+
process.exitCode = 1;
|
|
2492
|
+
}
|
|
2493
|
+
});
|
|
2494
|
+
sharesCommand.command("revoke").description("Revoke a task share").argument("<task-id>", "Task ID").requiredOption("--user <user-id>", "Shared-with user ID").action(async (taskId, opts) => {
|
|
2495
|
+
try {
|
|
2496
|
+
validateUuid(taskId, "Task ID");
|
|
2497
|
+
validateUuid(opts.user, "User ID");
|
|
2498
|
+
await graphql(revokeShare, { taskId, sharedWithId: opts.user });
|
|
2499
|
+
if (isJsonMode()) {
|
|
2500
|
+
printJson({ revoked: true, taskId });
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
printSuccess("Revoked share");
|
|
2504
|
+
} catch (err) {
|
|
2505
|
+
printError(err.message);
|
|
2506
|
+
process.exitCode = 1;
|
|
2507
|
+
}
|
|
2508
|
+
});
|
|
2509
|
+
sharesCommand.command("ls").description("List shares for a task").argument("<task-id>", "Task ID").action(async (taskId) => {
|
|
2510
|
+
try {
|
|
2511
|
+
validateUuid(taskId, "Task ID");
|
|
2512
|
+
const data = await graphql(listTaskShares, { taskId });
|
|
2513
|
+
if (isJsonMode()) {
|
|
2514
|
+
printJson(data.listTaskShares);
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
if (data.listTaskShares.length === 0) {
|
|
2518
|
+
console.log("Not shared.");
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
printTable(
|
|
2522
|
+
["Email", "Permission", "Status", "Shared At"],
|
|
2523
|
+
data.listTaskShares.map((s) => [maskEmail(s.sharedWithEmail), s.permission, s.status, formatDate(s.sharedAt)])
|
|
2524
|
+
);
|
|
2525
|
+
} catch (err) {
|
|
2526
|
+
printError(err.message);
|
|
2527
|
+
process.exitCode = 1;
|
|
2528
|
+
}
|
|
2529
|
+
});
|
|
2530
|
+
sharesCommand.command("outbox").description("List all shares you have sent (tasks and lists)").action(async () => {
|
|
2531
|
+
try {
|
|
2532
|
+
const [taskData, listData] = await Promise.all([
|
|
2533
|
+
graphql(listMyOutgoingTaskShares),
|
|
2534
|
+
graphql(listMyOutgoingListShares)
|
|
2535
|
+
]);
|
|
2536
|
+
const tasks = taskData.listMyOutgoingTaskShares;
|
|
2537
|
+
const lists = listData.listMyOutgoingListShares;
|
|
2538
|
+
if (isJsonMode()) {
|
|
2539
|
+
printJson({ tasks, lists });
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
if (tasks.length === 0 && lists.length === 0) {
|
|
2543
|
+
console.log("No outgoing shares.");
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
if (tasks.length > 0) {
|
|
2547
|
+
console.log("\nTask shares:");
|
|
2548
|
+
printTable(
|
|
2549
|
+
["Task", "To", "Permission", "Status", "Shared At"],
|
|
2550
|
+
tasks.map((s) => [
|
|
2551
|
+
s.task?.title || truncateId(s.taskId),
|
|
2552
|
+
maskEmail(s.sharedWithEmail),
|
|
2553
|
+
s.permission,
|
|
2554
|
+
s.status,
|
|
2555
|
+
formatDate(s.sharedAt)
|
|
2556
|
+
])
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
if (lists.length > 0) {
|
|
2560
|
+
console.log("\nList shares:");
|
|
2561
|
+
printTable(
|
|
2562
|
+
["List", "To", "Permission", "Status", "Shared At"],
|
|
2563
|
+
lists.map((s) => [
|
|
2564
|
+
s.list?.name || truncateId(s.listId),
|
|
2565
|
+
maskEmail(s.sharedWithEmail),
|
|
2566
|
+
s.permission,
|
|
2567
|
+
s.status,
|
|
2568
|
+
formatDate(s.sharedAt)
|
|
2569
|
+
])
|
|
2570
|
+
);
|
|
2571
|
+
}
|
|
2572
|
+
} catch (err) {
|
|
2573
|
+
printError(err.message);
|
|
2574
|
+
process.exitCode = 1;
|
|
2575
|
+
}
|
|
2576
|
+
});
|
|
2577
|
+
sharesCommand.command("update-permission").description("Change permission on a task share").argument("<task-id>", "Task ID").requiredOption("--user <user-id>", "Shared-with user ID").requiredOption("--permission <perm>", "New permission (view or edit)").action(async (taskId, opts) => {
|
|
2578
|
+
try {
|
|
2579
|
+
validateUuid(taskId, "Task ID");
|
|
2580
|
+
validateUuid(opts.user, "User ID");
|
|
2581
|
+
validatePermission(opts.permission);
|
|
2582
|
+
const data = await graphql(updateSharePermission, {
|
|
2583
|
+
input: { taskId, sharedWithId: opts.user, permission: opts.permission }
|
|
2584
|
+
});
|
|
2585
|
+
if (isJsonMode()) {
|
|
2586
|
+
printJson(data.updateSharePermission);
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
printSuccess(`Updated permission to ${opts.permission}`);
|
|
2590
|
+
} catch (err) {
|
|
2591
|
+
printError(err.message);
|
|
2592
|
+
process.exitCode = 1;
|
|
2593
|
+
}
|
|
2594
|
+
});
|
|
2595
|
+
sharesCommand.command("list-send").description("Share a list with someone").argument("<list-id>", "List ID").option("--email <email>", "Recipient email").option("--contact <name>", "Recipient contact name").option("--permission <perm>", "Permission (view or edit)", "view").action(async (listId, opts) => {
|
|
2596
|
+
try {
|
|
2597
|
+
validateUuid(listId, "List ID");
|
|
2598
|
+
const email = await resolveEmail(opts);
|
|
2599
|
+
validatePermission(opts.permission);
|
|
2600
|
+
const data = await graphql(shareList, {
|
|
2601
|
+
input: { listId, sharedWithEmail: email, permission: opts.permission }
|
|
2602
|
+
});
|
|
2603
|
+
if (isJsonMode()) {
|
|
2604
|
+
printJson(data.shareList);
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
printSuccess(`Shared list with ${maskEmail(email)} (${opts.permission})`);
|
|
2608
|
+
} catch (err) {
|
|
2609
|
+
printError(err.message);
|
|
2610
|
+
process.exitCode = 1;
|
|
2611
|
+
}
|
|
2612
|
+
});
|
|
2613
|
+
sharesCommand.command("list-inbox").description("List lists shared with me").action(async () => {
|
|
2614
|
+
try {
|
|
2615
|
+
const data = await graphql(listListsSharedWithMe);
|
|
2616
|
+
if (isJsonMode()) {
|
|
2617
|
+
printJson(data.listListsSharedWithMe);
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (data.listListsSharedWithMe.length === 0) {
|
|
2621
|
+
console.log("No shared lists.");
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
printTable(
|
|
2625
|
+
["List", "Owner", "Permission", "Status"],
|
|
2626
|
+
data.listListsSharedWithMe.map((s) => [
|
|
2627
|
+
s.list?.name || truncateId(s.listId),
|
|
2628
|
+
s.ownerEmail ? maskEmail(s.ownerEmail) : "\u2014",
|
|
2629
|
+
s.permission,
|
|
2630
|
+
s.status
|
|
2631
|
+
])
|
|
2632
|
+
);
|
|
2633
|
+
} catch (err) {
|
|
2634
|
+
printError(err.message);
|
|
2635
|
+
process.exitCode = 1;
|
|
2636
|
+
}
|
|
2637
|
+
});
|
|
2638
|
+
sharesCommand.command("list-accept").description("Accept a shared list").argument("<list-id>", "List ID").requiredOption("--owner <owner-id>", "Owner user ID").action(async (listId, opts) => {
|
|
2639
|
+
try {
|
|
2640
|
+
validateUuid(listId, "List ID");
|
|
2641
|
+
validateUuid(opts.owner, "Owner ID");
|
|
2642
|
+
await graphql(acceptListShare, { listId, ownerId: opts.owner });
|
|
2643
|
+
if (isJsonMode()) {
|
|
2644
|
+
printJson({ accepted: true, listId });
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
printSuccess("Accepted list share");
|
|
2648
|
+
} catch (err) {
|
|
2649
|
+
printError(err.message);
|
|
2650
|
+
process.exitCode = 1;
|
|
2651
|
+
}
|
|
2652
|
+
});
|
|
2653
|
+
sharesCommand.command("list-decline").description("Decline a shared list").argument("<list-id>", "List ID").requiredOption("--owner <owner-id>", "Owner user ID").action(async (listId, opts) => {
|
|
2654
|
+
try {
|
|
2655
|
+
validateUuid(listId, "List ID");
|
|
2656
|
+
validateUuid(opts.owner, "Owner ID");
|
|
2657
|
+
await graphql(declineListShare, { listId, ownerId: opts.owner });
|
|
2658
|
+
if (isJsonMode()) {
|
|
2659
|
+
printJson({ declined: true, listId });
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2662
|
+
printSuccess("Declined list share");
|
|
2663
|
+
} catch (err) {
|
|
2664
|
+
printError(err.message);
|
|
2665
|
+
process.exitCode = 1;
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2668
|
+
sharesCommand.command("list-revoke").description("Revoke a list share").argument("<list-id>", "List ID").requiredOption("--user <user-id>", "Shared-with user ID").action(async (listId, opts) => {
|
|
2669
|
+
try {
|
|
2670
|
+
validateUuid(listId, "List ID");
|
|
2671
|
+
validateUuid(opts.user, "User ID");
|
|
2672
|
+
await graphql(revokeListShare, { listId, sharedWithId: opts.user });
|
|
2673
|
+
if (isJsonMode()) {
|
|
2674
|
+
printJson({ revoked: true, listId });
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
printSuccess("Revoked list share");
|
|
2678
|
+
} catch (err) {
|
|
2679
|
+
printError(err.message);
|
|
2680
|
+
process.exitCode = 1;
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
sharesCommand.command("list-ls").description("List shares for a list").argument("<list-id>", "List ID").action(async (listId) => {
|
|
2684
|
+
try {
|
|
2685
|
+
validateUuid(listId, "List ID");
|
|
2686
|
+
const data = await graphql(listListShares, { listId });
|
|
2687
|
+
if (isJsonMode()) {
|
|
2688
|
+
printJson(data.listListShares);
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
if (data.listListShares.length === 0) {
|
|
2692
|
+
console.log("Not shared.");
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
printTable(
|
|
2696
|
+
["Email", "Permission", "Status", "Shared At"],
|
|
2697
|
+
data.listListShares.map((s) => [maskEmail(s.sharedWithEmail), s.permission, s.status, formatDate(s.sharedAt)])
|
|
2698
|
+
);
|
|
2699
|
+
} catch (err) {
|
|
2700
|
+
printError(err.message);
|
|
2701
|
+
process.exitCode = 1;
|
|
2702
|
+
}
|
|
2703
|
+
});
|
|
2704
|
+
sharesCommand.command("list-update-permission").description("Change permission on a list share").argument("<list-id>", "List ID").requiredOption("--user <user-id>", "Shared-with user ID").requiredOption("--permission <perm>", "New permission (view or edit)").action(async (listId, opts) => {
|
|
2705
|
+
try {
|
|
2706
|
+
validateUuid(listId, "List ID");
|
|
2707
|
+
validateUuid(opts.user, "User ID");
|
|
2708
|
+
validatePermission(opts.permission);
|
|
2709
|
+
const data = await graphql(updateListSharePermission, {
|
|
2710
|
+
input: { listId, sharedWithId: opts.user, permission: opts.permission }
|
|
2711
|
+
});
|
|
2712
|
+
if (isJsonMode()) {
|
|
2713
|
+
printJson(data.updateListSharePermission);
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
printSuccess(`Updated list share permission to ${opts.permission}`);
|
|
2717
|
+
} catch (err) {
|
|
2718
|
+
printError(err.message);
|
|
2719
|
+
process.exitCode = 1;
|
|
2720
|
+
}
|
|
2721
|
+
});
|
|
2722
|
+
|
|
2723
|
+
// src/commands/status.ts
|
|
2724
|
+
import { Command as Command11 } from "commander";
|
|
2725
|
+
import chalk3 from "chalk";
|
|
2726
|
+
var statusCommand = new Command11("status").description("Show task dashboard with counts by status").option("--all", "Include archived tasks").action(async (opts) => {
|
|
2727
|
+
try {
|
|
2728
|
+
const data = await graphql(listTasks, {});
|
|
2729
|
+
const tasks = opts.all ? data.listTasks : data.listTasks.filter((t) => !t.archived);
|
|
2730
|
+
if (isJsonMode()) {
|
|
2731
|
+
const counts = {};
|
|
2732
|
+
for (const t of tasks) {
|
|
2733
|
+
counts[t.status] = (counts[t.status] || 0) + 1;
|
|
2734
|
+
}
|
|
2735
|
+
printJson({ total: tasks.length, counts });
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2738
|
+
if (tasks.length === 0) {
|
|
2739
|
+
console.log("No tasks found.");
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
const buckets = {
|
|
2743
|
+
todo: [],
|
|
2744
|
+
in_progress: [],
|
|
2745
|
+
review: [],
|
|
2746
|
+
done: []
|
|
2747
|
+
};
|
|
2748
|
+
for (const t of tasks) {
|
|
2749
|
+
if (buckets[t.status]) buckets[t.status].push(t);
|
|
2750
|
+
}
|
|
2751
|
+
const total = tasks.length;
|
|
2752
|
+
const parts = Object.entries(buckets).filter(([, list]) => list.length > 0).map(([status, list]) => `${formatStatus(status)} ${chalk3.bold(String(list.length))}`);
|
|
2753
|
+
console.log(`
|
|
2754
|
+
${chalk3.bold(String(total))} tasks: ${parts.join(" ")}
|
|
2755
|
+
`);
|
|
2756
|
+
for (const status of ["in_progress", "review", "todo"]) {
|
|
2757
|
+
const list = buckets[status];
|
|
2758
|
+
if (list.length === 0) continue;
|
|
2759
|
+
console.log(` ${formatStatus(status)}`);
|
|
2760
|
+
for (const t of list.slice(0, 10)) {
|
|
2761
|
+
const due = t.dueDate ? chalk3.dim(` due ${formatDate(t.dueDate)}`) : "";
|
|
2762
|
+
const pri = t.priority === "critical" || t.priority === "high" ? ` ${formatPriority(t.priority)}` : "";
|
|
2763
|
+
console.log(` ${chalk3.dim(truncateId(t.id))} ${t.title}${pri}${due}`);
|
|
2764
|
+
}
|
|
2765
|
+
if (list.length > 10) {
|
|
2766
|
+
console.log(chalk3.dim(` ... and ${list.length - 10} more`));
|
|
2767
|
+
}
|
|
2768
|
+
console.log();
|
|
2769
|
+
}
|
|
2770
|
+
const now = /* @__PURE__ */ new Date();
|
|
2771
|
+
const overdue = tasks.filter(
|
|
2772
|
+
(t) => t.status !== "done" && t.dueDate && new Date(t.dueDate) < now
|
|
2773
|
+
);
|
|
2774
|
+
if (overdue.length > 0) {
|
|
2775
|
+
console.log(chalk3.red.bold(` ! ${overdue.length} overdue task${overdue.length > 1 ? "s" : ""}`));
|
|
2776
|
+
for (const t of overdue.slice(0, 5)) {
|
|
2777
|
+
console.log(chalk3.red(` ${truncateId(t.id)} ${t.title} (due ${formatDate(t.dueDate)})`));
|
|
2778
|
+
}
|
|
2779
|
+
console.log();
|
|
2780
|
+
}
|
|
2781
|
+
} catch (err) {
|
|
2782
|
+
printError(err.message);
|
|
2783
|
+
process.exitCode = 1;
|
|
2784
|
+
}
|
|
2785
|
+
});
|
|
2786
|
+
|
|
2787
|
+
// src/commands/apikeys.ts
|
|
2788
|
+
import { Command as Command12 } from "commander";
|
|
2789
|
+
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
2790
|
+
function printKeyList(keys) {
|
|
2791
|
+
if (keys.length === 0) {
|
|
2792
|
+
console.log("No API keys.");
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
printTable(
|
|
2796
|
+
["Name", "Prefix", "Status", "Created", "Last Used"],
|
|
2797
|
+
keys.map((k) => [k.name, k.keyPrefix, k.status, formatDate(k.createdAt), k.lastUsedAt ? formatDate(k.lastUsedAt) : "never"])
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
var apikeysCommand = new Command12("apikeys").description("List and manage API keys").action(async () => {
|
|
2801
|
+
try {
|
|
2802
|
+
const data = await graphql(listApiKeys);
|
|
2803
|
+
if (isJsonMode()) {
|
|
2804
|
+
printJson(data.listApiKeys);
|
|
2805
|
+
return;
|
|
2806
|
+
}
|
|
2807
|
+
printKeyList(data.listApiKeys);
|
|
2808
|
+
} catch (err) {
|
|
2809
|
+
printError(err.message);
|
|
2810
|
+
process.exitCode = 1;
|
|
2811
|
+
}
|
|
2812
|
+
});
|
|
2813
|
+
apikeysCommand.command("list").description("List all API keys").action(async () => {
|
|
2814
|
+
try {
|
|
2815
|
+
const data = await graphql(listApiKeys);
|
|
2816
|
+
if (isJsonMode()) {
|
|
2817
|
+
printJson(data.listApiKeys);
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
printKeyList(data.listApiKeys);
|
|
2821
|
+
} catch (err) {
|
|
2822
|
+
printError(err.message);
|
|
2823
|
+
process.exitCode = 1;
|
|
2824
|
+
}
|
|
2825
|
+
});
|
|
2826
|
+
apikeysCommand.command("revoke").description("Revoke an API key").argument("<id>", "API key ID").option("--force", "Skip confirmation").action(async (id, opts) => {
|
|
2827
|
+
try {
|
|
2828
|
+
if (!opts.force) {
|
|
2829
|
+
const ok = await confirm7({ message: `Revoke API key ${id}?`, default: false });
|
|
2830
|
+
if (!ok) {
|
|
2831
|
+
console.log("Cancelled.");
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
const data = await graphql(revokeApiKey, { id });
|
|
2836
|
+
if (isJsonMode()) {
|
|
2837
|
+
printJson(data.revokeApiKey);
|
|
2838
|
+
return;
|
|
2839
|
+
}
|
|
2840
|
+
printSuccess(`Revoked API key: ${data.revokeApiKey.name}`);
|
|
2841
|
+
} catch (err) {
|
|
2842
|
+
printError(err.message);
|
|
2843
|
+
process.exitCode = 1;
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
|
|
2847
|
+
// src/index.ts
|
|
2848
|
+
var program = new Command13();
|
|
2849
|
+
program.name("tasklumina").version("1.0.0").description("Task Lumina CLI \u2014 manage tasks from the terminal").option("--json", "Output as JSON").option("--no-color", "Disable colored output").option("--verbose", "Show detailed request info (endpoint masked)").option("--redact", "Mask emails and sensitive data in output");
|
|
2850
|
+
program.hook("postAction", async () => {
|
|
2851
|
+
await maybeAutoRotate();
|
|
2852
|
+
});
|
|
2853
|
+
program.addCommand(configureCommand);
|
|
2854
|
+
program.addCommand(whoamiCommand);
|
|
2855
|
+
program.addCommand(statusCommand);
|
|
2856
|
+
program.addCommand(tasksCommand);
|
|
2857
|
+
program.addCommand(categoriesCommand);
|
|
2858
|
+
program.addCommand(tagsCommand);
|
|
2859
|
+
program.addCommand(notesCommand);
|
|
2860
|
+
program.addCommand(contactsCommand);
|
|
2861
|
+
program.addCommand(prefsCommand);
|
|
2862
|
+
program.addCommand(listsCommand);
|
|
2863
|
+
program.addCommand(sharesCommand);
|
|
2864
|
+
program.addCommand(apikeysCommand);
|
|
2865
|
+
program.parse();
|