@donebear/cli 0.1.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/cli.mjs ADDED
@@ -0,0 +1,3676 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { Command } from "commander";
4
+ import { homedir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
7
+ import { createClient } from "@supabase/supabase-js";
8
+ import { existsSync } from "node:fs";
9
+ import { config } from "dotenv";
10
+ import { execSync, spawn } from "node:child_process";
11
+ import { randomBytes, randomUUID } from "node:crypto";
12
+ import { createServer } from "node:http";
13
+ import { styleText } from "node:util";
14
+ import { createInterface } from "node:readline";
15
+
16
+ //#region src/constants.ts
17
+ const CLI_NAME = "donebear";
18
+ const DONEBEAR_TOKEN_ENV = "DONEBEAR_TOKEN";
19
+ const DONEBEAR_DEBUG_ENV = "DONEBEAR_DEBUG";
20
+ const DONEBEAR_CONFIG_DIR_ENV = "DONEBEAR_CONFIG_DIR";
21
+ const DONEBEAR_SUPABASE_URL_ENV = "DONEBEAR_SUPABASE_URL";
22
+ const DONEBEAR_SUPABASE_KEY_ENV = "DONEBEAR_SUPABASE_PUBLISHABLE_KEY";
23
+ const DONEBEAR_API_URL_ENV = "DONEBEAR_API_URL";
24
+ const FALLBACK_SUPABASE_URL_ENV_KEYS = ["NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_URL"];
25
+ const FALLBACK_SUPABASE_KEY_ENV_KEYS = ["NEXT_PUBLIC_SUPABASE_PUBLISHABLE_OR_ANON_KEY", "SUPABASE_ANON_KEY"];
26
+ const FALLBACK_API_URL_ENV_KEYS = ["NEXT_PUBLIC_RESERVE_MANAGE_API"];
27
+ const DEFAULT_API_URL = "http://127.0.0.1:3001";
28
+ const DEFAULT_OAUTH_PROVIDER = "google";
29
+ const DEFAULT_OAUTH_CALLBACK_PORT = 8787;
30
+ const DEFAULT_OAUTH_CALLBACK_PATH = "/auth/callback";
31
+ const DEFAULT_OAUTH_TIMEOUT_SECONDS = 180;
32
+ function getDefaultConfigBaseDir() {
33
+ if (process.platform === "win32") return process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
34
+ return process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
35
+ }
36
+ const configBaseDir = getDefaultConfigBaseDir();
37
+ const CONFIG_DIR = process.env[DONEBEAR_CONFIG_DIR_ENV] ?? join(configBaseDir, CLI_NAME);
38
+ const AUTH_FILE_PATH = join(CONFIG_DIR, "auth.json");
39
+ const CONTEXT_FILE_PATH = join(CONFIG_DIR, "context.json");
40
+
41
+ //#endregion
42
+ //#region src/errors.ts
43
+ const EXIT_CODES = {
44
+ SUCCESS: 0,
45
+ ERROR: 1,
46
+ CANCELLED: 2,
47
+ AUTH_REQUIRED: 4
48
+ };
49
+ var CliError = class extends Error {
50
+ exitCode;
51
+ constructor(message, exitCode = EXIT_CODES.ERROR) {
52
+ super(message);
53
+ this.name = "CliError";
54
+ this.exitCode = exitCode;
55
+ }
56
+ };
57
+ function toCliError(error) {
58
+ if (error instanceof CliError) return error;
59
+ if (error instanceof Error) return new CliError(error.message, EXIT_CODES.ERROR);
60
+ return new CliError("Unknown error", EXIT_CODES.ERROR);
61
+ }
62
+
63
+ //#endregion
64
+ //#region src/storage.ts
65
+ function isRecord$1(value) {
66
+ return typeof value === "object" && value !== null;
67
+ }
68
+ function isOAuthProvider(value) {
69
+ return value === "google" || value === "github";
70
+ }
71
+ function parseStoredAuthSession(value) {
72
+ if (!isRecord$1(value)) return null;
73
+ if (typeof value.accessToken !== "string") return null;
74
+ if (value.refreshToken !== null && typeof value.refreshToken !== "string") return null;
75
+ if (value.tokenType !== null && typeof value.tokenType !== "string") return null;
76
+ if (value.expiresAt !== null && typeof value.expiresAt !== "string") return null;
77
+ if (!isOAuthProvider(value.provider)) return null;
78
+ const user = value.user;
79
+ if (user !== null && (!isRecord$1(user) || typeof user.id !== "string" || user.email !== null && typeof user.email !== "string")) return null;
80
+ if (typeof value.createdAt !== "string") return null;
81
+ const parsedUser = user === null ? null : {
82
+ id: user.id,
83
+ email: user.email ?? null
84
+ };
85
+ return {
86
+ accessToken: value.accessToken,
87
+ refreshToken: value.refreshToken,
88
+ tokenType: value.tokenType,
89
+ expiresAt: value.expiresAt,
90
+ provider: value.provider,
91
+ user: parsedUser,
92
+ createdAt: value.createdAt
93
+ };
94
+ }
95
+ function parseAuthFile(payload) {
96
+ const parsed = JSON.parse(payload);
97
+ if (!isRecord$1(parsed) || parsed.version !== 1) throw new CliError(`Invalid auth cache format in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
98
+ const session = parseStoredAuthSession(parsed.session);
99
+ if (!session) throw new CliError(`Invalid auth session in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
100
+ return {
101
+ version: 1,
102
+ session
103
+ };
104
+ }
105
+ async function readStoredAuthSession() {
106
+ try {
107
+ return parseAuthFile(await readFile(AUTH_FILE_PATH, "utf8")).session;
108
+ } catch (error) {
109
+ if (isRecord$1(error) && error.code === "ENOENT") return null;
110
+ if (error instanceof CliError) throw error;
111
+ throw new CliError(`Failed to read credentials from ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
112
+ }
113
+ }
114
+ async function writeStoredAuthSession(session) {
115
+ const payload = {
116
+ version: 1,
117
+ session
118
+ };
119
+ await mkdir(CONFIG_DIR, {
120
+ recursive: true,
121
+ mode: 448
122
+ });
123
+ await writeFile(AUTH_FILE_PATH, `${JSON.stringify(payload, null, 2)}\n`, {
124
+ encoding: "utf8",
125
+ mode: 384
126
+ });
127
+ }
128
+ async function clearStoredAuthSession() {
129
+ await rm(AUTH_FILE_PATH, { force: true });
130
+ }
131
+
132
+ //#endregion
133
+ //#region src/env.ts
134
+ const DEFAULT_ENV_FILES = [
135
+ ".env.local",
136
+ ".env",
137
+ "apps/manage-frontend/.env.local",
138
+ "apps/manage-api/.env"
139
+ ];
140
+ let loaded = false;
141
+ function loadCliEnvironmentFiles() {
142
+ if (loaded) return;
143
+ for (const relativePath of DEFAULT_ENV_FILES) {
144
+ const absolutePath = resolve(process.cwd(), relativePath);
145
+ if (!existsSync(absolutePath)) continue;
146
+ config({
147
+ path: absolutePath,
148
+ override: false,
149
+ quiet: true
150
+ });
151
+ }
152
+ loaded = true;
153
+ }
154
+ function readEnvValue(key) {
155
+ const value = process.env[key];
156
+ if (typeof value !== "string") return;
157
+ const trimmed = value.trim();
158
+ return trimmed.length > 0 ? trimmed : void 0;
159
+ }
160
+
161
+ //#endregion
162
+ //#region src/supabase.ts
163
+ var MemoryStorage = class {
164
+ values = /* @__PURE__ */ new Map();
165
+ getItem(key) {
166
+ return this.values.get(key) ?? null;
167
+ }
168
+ setItem(key, value) {
169
+ this.values.set(key, value);
170
+ }
171
+ removeItem(key) {
172
+ this.values.delete(key);
173
+ }
174
+ };
175
+ function firstDefined$1(keys) {
176
+ for (const key of keys) {
177
+ const value = readEnvValue(key);
178
+ if (value) return value;
179
+ }
180
+ }
181
+ function resolveSupabaseConfig() {
182
+ const url = readEnvValue(DONEBEAR_SUPABASE_URL_ENV) ?? firstDefined$1(FALLBACK_SUPABASE_URL_ENV_KEYS);
183
+ const publishableKey = readEnvValue(DONEBEAR_SUPABASE_KEY_ENV) ?? firstDefined$1(FALLBACK_SUPABASE_KEY_ENV_KEYS);
184
+ if (!(url && publishableKey)) throw new CliError([
185
+ "Supabase configuration is missing.",
186
+ `Set ${DONEBEAR_SUPABASE_URL_ENV} and ${DONEBEAR_SUPABASE_KEY_ENV}.`,
187
+ `Fallback keys are also supported: ${FALLBACK_SUPABASE_URL_ENV_KEYS.join(", ")} and ${FALLBACK_SUPABASE_KEY_ENV_KEYS.join(", ")}.`
188
+ ].join(" "), EXIT_CODES.ERROR);
189
+ return {
190
+ url,
191
+ publishableKey
192
+ };
193
+ }
194
+ function createSupabaseClient(config, options) {
195
+ return createClient(config.url, config.publishableKey, { auth: {
196
+ flowType: "pkce",
197
+ autoRefreshToken: false,
198
+ detectSessionInUrl: false,
199
+ persistSession: options?.persistSession ?? false,
200
+ storage: options?.storage
201
+ } });
202
+ }
203
+ function createPkceSupabaseClient(config) {
204
+ return createSupabaseClient(config, {
205
+ storage: new MemoryStorage(),
206
+ persistSession: true
207
+ });
208
+ }
209
+ function toStoredAuthSession(session, provider) {
210
+ const expiresAt = typeof session.expires_at === "number" ? (/* @__PURE__ */ new Date(session.expires_at * 1e3)).toISOString() : null;
211
+ return {
212
+ accessToken: session.access_token,
213
+ refreshToken: session.refresh_token ?? null,
214
+ tokenType: session.token_type ?? null,
215
+ expiresAt,
216
+ provider,
217
+ user: session.user ? {
218
+ id: session.user.id,
219
+ email: session.user.email ?? null
220
+ } : null,
221
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
222
+ };
223
+ }
224
+ async function refreshSupabaseSession(config, refreshToken) {
225
+ const { data, error } = await createSupabaseClient(config).auth.refreshSession({ refresh_token: refreshToken });
226
+ if (error) throw new CliError(`Failed to refresh session: ${error.message}`, EXIT_CODES.AUTH_REQUIRED);
227
+ if (!data.session) throw new CliError("No session returned during refresh", EXIT_CODES.AUTH_REQUIRED);
228
+ return data.session;
229
+ }
230
+ async function getSupabaseUser(config, accessToken) {
231
+ const { data, error } = await createSupabaseClient(config).auth.getUser(accessToken);
232
+ if (error) throw new CliError(`Failed to load user: ${error.message}`, EXIT_CODES.ERROR);
233
+ if (!data.user) throw new CliError("No user returned for current token", EXIT_CODES.ERROR);
234
+ return data.user;
235
+ }
236
+
237
+ //#endregion
238
+ //#region src/auth-session.ts
239
+ function parseJwtBase64Url(input) {
240
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
241
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
242
+ return Buffer.from(padded, "base64").toString("utf8");
243
+ }
244
+ function parseJwtClaims(token) {
245
+ const payloadPart = token.split(".").at(1);
246
+ if (!payloadPart) return null;
247
+ try {
248
+ const payload = parseJwtBase64Url(payloadPart);
249
+ const parsed = JSON.parse(payload);
250
+ if (typeof parsed !== "object" || parsed === null) return null;
251
+ const claims = parsed;
252
+ return {
253
+ exp: typeof claims.exp === "number" ? claims.exp : void 0,
254
+ email: typeof claims.email === "string" ? claims.email : void 0,
255
+ sub: typeof claims.sub === "string" ? claims.sub : void 0
256
+ };
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+ function expiryFromClaims(claims) {
262
+ if (!claims || typeof claims.exp !== "number") return null;
263
+ return (/* @__PURE__ */ new Date(claims.exp * 1e3)).toISOString();
264
+ }
265
+ function userFromClaims(claims) {
266
+ if (!(claims?.sub || claims?.email)) return null;
267
+ return {
268
+ id: claims.sub ?? "unknown",
269
+ email: claims.email ?? null
270
+ };
271
+ }
272
+ function nowUnixSeconds() {
273
+ return Math.floor(Date.now() / 1e3);
274
+ }
275
+ function isExpired(session) {
276
+ if (!session.expiresAt) return false;
277
+ const expiresAtMs = Date.parse(session.expiresAt);
278
+ if (Number.isNaN(expiresAtMs)) return false;
279
+ return expiresAtMs <= Date.now();
280
+ }
281
+ function shouldRefresh(session) {
282
+ if (!session.expiresAt) return false;
283
+ const expiresAtMs = Date.parse(session.expiresAt);
284
+ if (Number.isNaN(expiresAtMs)) return false;
285
+ return expiresAtMs - Date.now() <= 6e4;
286
+ }
287
+ function tokenFromRaw(token, source) {
288
+ const claims = parseJwtClaims(token);
289
+ return {
290
+ token,
291
+ source,
292
+ expiresAt: expiryFromClaims(claims),
293
+ user: userFromClaims(claims)
294
+ };
295
+ }
296
+ async function resolveAuthToken(context, config) {
297
+ if (context.tokenOverride) return tokenFromRaw(context.tokenOverride, "flag");
298
+ const envToken = process.env[DONEBEAR_TOKEN_ENV]?.trim();
299
+ if (envToken) return tokenFromRaw(envToken, "environment");
300
+ const session = await readStoredAuthSession();
301
+ if (!session) return null;
302
+ if (!(shouldRefresh(session) || isExpired(session))) return {
303
+ token: session.accessToken,
304
+ source: "stored",
305
+ expiresAt: session.expiresAt,
306
+ user: session.user
307
+ };
308
+ if (!(session.refreshToken && config)) {
309
+ if (isExpired(session)) {
310
+ await clearStoredAuthSession();
311
+ return null;
312
+ }
313
+ return {
314
+ token: session.accessToken,
315
+ source: "stored",
316
+ expiresAt: session.expiresAt,
317
+ user: session.user
318
+ };
319
+ }
320
+ const updatedSession = toStoredAuthSession(await refreshSupabaseSession(config, session.refreshToken), session.provider);
321
+ await writeStoredAuthSession(updatedSession);
322
+ return {
323
+ token: updatedSession.accessToken,
324
+ source: "stored",
325
+ expiresAt: updatedSession.expiresAt,
326
+ user: updatedSession.user
327
+ };
328
+ }
329
+ function resolveTokenStatus(token) {
330
+ const claims = parseJwtClaims(token.token);
331
+ if (!claims?.exp) return {
332
+ expiresInSeconds: null,
333
+ expired: false
334
+ };
335
+ const expiresInSeconds = claims.exp - nowUnixSeconds();
336
+ return {
337
+ expiresInSeconds,
338
+ expired: expiresInSeconds <= 0
339
+ };
340
+ }
341
+
342
+ //#endregion
343
+ //#region src/browser.ts
344
+ function getOpenCommand(url) {
345
+ if (process.platform === "darwin") return {
346
+ command: "open",
347
+ args: [url]
348
+ };
349
+ if (process.platform === "win32") return {
350
+ command: "cmd",
351
+ args: [
352
+ "/c",
353
+ "start",
354
+ "",
355
+ url
356
+ ]
357
+ };
358
+ return {
359
+ command: "xdg-open",
360
+ args: [url]
361
+ };
362
+ }
363
+ async function openUrl(url) {
364
+ const openCommand = getOpenCommand(url);
365
+ return await new Promise((resolve) => {
366
+ try {
367
+ const child = spawn(openCommand.command, openCommand.args, {
368
+ detached: true,
369
+ stdio: "ignore"
370
+ });
371
+ child.once("error", () => {
372
+ resolve(false);
373
+ });
374
+ child.once("spawn", () => {
375
+ child.unref();
376
+ resolve(true);
377
+ });
378
+ } catch {
379
+ resolve(false);
380
+ }
381
+ });
382
+ }
383
+
384
+ //#endregion
385
+ //#region src/command-parsers.ts
386
+ function parsePositiveInteger(value, label) {
387
+ const parsed = Number.parseInt(value, 10);
388
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${label} must be a positive integer`);
389
+ return parsed;
390
+ }
391
+
392
+ //#endregion
393
+ //#region src/oauth.ts
394
+ function createOAuthState() {
395
+ return randomBytes(24).toString("hex");
396
+ }
397
+
398
+ //#endregion
399
+ //#region src/oauth-callback.ts
400
+ function successHtml() {
401
+ return `<!doctype html><html><head><meta charset="utf-8"/><title>Done Bear CLI</title></head><body><h1>Authentication complete</h1><p>You can return to your terminal.</p></body></html>`;
402
+ }
403
+ function errorHtml(message) {
404
+ return `<!doctype html><html><head><meta charset="utf-8"/><title>Done Bear CLI</title></head><body><h1>Authentication failed</h1><p>${message}</p></body></html>`;
405
+ }
406
+ async function waitForOAuthCode(options) {
407
+ return await new Promise((resolve, reject) => {
408
+ const host = options.redirectUrl.hostname;
409
+ const port = Number(options.redirectUrl.port);
410
+ const pathname = options.redirectUrl.pathname;
411
+ if (!Number.isInteger(port) || port <= 0) {
412
+ reject(new CliError("OAuth redirect URL requires an explicit port", EXIT_CODES.ERROR));
413
+ return;
414
+ }
415
+ let settled = false;
416
+ const finish = (result) => {
417
+ if (settled) return;
418
+ settled = true;
419
+ clearTimeout(timeout);
420
+ server.close(() => {
421
+ if (result.error) {
422
+ reject(result.error);
423
+ return;
424
+ }
425
+ resolve(result.code ?? "");
426
+ });
427
+ };
428
+ const server = createServer((request, response) => {
429
+ if (!request.url) {
430
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
431
+ response.end(errorHtml("Missing request URL"));
432
+ finish({ error: new CliError("OAuth callback is missing a request URL", EXIT_CODES.ERROR) });
433
+ return;
434
+ }
435
+ const url = new URL(request.url, options.redirectUrl.origin);
436
+ if (url.pathname !== pathname) {
437
+ response.writeHead(404, { "content-type": "text/html; charset=utf-8" });
438
+ response.end(errorHtml("Invalid callback path"));
439
+ return;
440
+ }
441
+ const providerError = url.searchParams.get("error");
442
+ if (providerError) {
443
+ const description = url.searchParams.get("error_description") ?? providerError;
444
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
445
+ response.end(errorHtml(description));
446
+ finish({ error: new CliError(`OAuth provider returned an error: ${description}`, EXIT_CODES.ERROR) });
447
+ return;
448
+ }
449
+ if (url.searchParams.get("state") !== options.expectedState) {
450
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
451
+ response.end(errorHtml("State verification failed"));
452
+ finish({ error: new CliError("OAuth state verification failed", EXIT_CODES.ERROR) });
453
+ return;
454
+ }
455
+ const code = url.searchParams.get("code");
456
+ if (!code) {
457
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
458
+ response.end(errorHtml("Authorization code was missing"));
459
+ finish({ error: new CliError("OAuth callback is missing an authorization code", EXIT_CODES.ERROR) });
460
+ return;
461
+ }
462
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
463
+ response.end(successHtml());
464
+ finish({ code });
465
+ });
466
+ server.on("error", (error) => {
467
+ finish({ error: new CliError(`Failed to start callback server on ${host}:${port}: ${error.message}`, EXIT_CODES.ERROR) });
468
+ });
469
+ const timeout = setTimeout(() => {
470
+ finish({ error: new CliError("Timed out waiting for OAuth callback", EXIT_CODES.CANCELLED) });
471
+ }, options.timeoutMs);
472
+ server.listen(port, host);
473
+ });
474
+ }
475
+
476
+ //#endregion
477
+ //#region src/output.ts
478
+ function writeLine(stream, message) {
479
+ stream.write(`${message}\n`);
480
+ }
481
+ function colorize(context, format, message) {
482
+ if (!context.color) return message;
483
+ return styleText(format, message);
484
+ }
485
+ function writeJson(value) {
486
+ writeLine(process.stdout, JSON.stringify(value, null, 2));
487
+ }
488
+ function writeInfo(context, message) {
489
+ writeLine(process.stdout, colorize(context, "cyan", message));
490
+ }
491
+ function writeSuccess(context, message) {
492
+ writeLine(process.stdout, colorize(context, "green", message));
493
+ }
494
+ function writeWarning(context, message) {
495
+ writeLine(process.stderr, colorize(context, "yellow", message));
496
+ }
497
+ function writeError(context, message) {
498
+ writeLine(process.stderr, colorize(context, "red", message));
499
+ }
500
+ function writeDebug(context, message) {
501
+ if (!context.debug) return;
502
+ writeLine(process.stderr, colorize(context, "gray", `[debug] ${message}`));
503
+ }
504
+ function writeTotal(context, count) {
505
+ writeLine(process.stdout, colorize(context, "cyan", String(count)));
506
+ }
507
+ function escapeCsvField(value) {
508
+ if (value.includes(",") || value.includes("\"") || value.includes("\n")) return `"${value.replace(/"/g, "\"\"")}"`;
509
+ return value;
510
+ }
511
+ function writeCsv(headers, rows) {
512
+ writeLine(process.stdout, headers.map(escapeCsvField).join(","));
513
+ for (const row of rows) writeLine(process.stdout, row.map(escapeCsvField).join(","));
514
+ }
515
+ function writeTsv(headers, rows) {
516
+ writeLine(process.stdout, headers.join(" "));
517
+ for (const row of rows) writeLine(process.stdout, row.join(" "));
518
+ }
519
+ /**
520
+ * Handles --total, --format csv, --format tsv output (including --copy for tabular data).
521
+ * Returns true if the format was handled; caller renders text mode if false.
522
+ */
523
+ function writeFormattedRows(context, totalCount, headers, rows) {
524
+ if (context.total) {
525
+ writeTotal(context, totalCount);
526
+ return true;
527
+ }
528
+ if (context.format === "csv") {
529
+ writeCsv(headers, rows);
530
+ if (context.copy) copyToClipboard([headers.join(","), ...rows.map((r) => r.join(","))].join("\n"));
531
+ return true;
532
+ }
533
+ if (context.format === "tsv") {
534
+ writeTsv(headers, rows);
535
+ if (context.copy) copyToClipboard([headers.join(" "), ...rows.map((r) => r.join(" "))].join("\n"));
536
+ return true;
537
+ }
538
+ return false;
539
+ }
540
+ function copyToClipboard(text) {
541
+ try {
542
+ if (process.platform === "darwin") execSync("pbcopy", { input: text });
543
+ else if (process.platform === "win32") execSync("clip", { input: text });
544
+ else try {
545
+ execSync("xclip -selection clipboard", { input: text });
546
+ } catch {
547
+ execSync("xsel --clipboard --input", { input: text });
548
+ }
549
+ return true;
550
+ } catch {
551
+ return false;
552
+ }
553
+ }
554
+
555
+ //#endregion
556
+ //#region src/runtime.ts
557
+ function parseOutputFormat(value) {
558
+ switch (value?.trim().toLowerCase()) {
559
+ case "json": return "json";
560
+ case "csv": return "csv";
561
+ case "tsv": return "tsv";
562
+ default: return "text";
563
+ }
564
+ }
565
+ function contextFromCommand(command) {
566
+ const options = command.optsWithGlobals();
567
+ const rawFormat = parseOutputFormat(options.format);
568
+ const format = options.json === true ? "json" : rawFormat;
569
+ return {
570
+ json: format === "json",
571
+ color: options.color !== false && process.env.NO_COLOR !== "1",
572
+ debug: options.debug === true || process.env[DONEBEAR_DEBUG_ENV] === "1",
573
+ tokenOverride: options.token?.trim() || null,
574
+ apiUrlOverride: options.apiUrl?.trim() || null,
575
+ format,
576
+ copy: options.copy === true,
577
+ total: options.total === true
578
+ };
579
+ }
580
+ function contextFromArgv(argv) {
581
+ const hasJsonFlag = argv.includes("--json");
582
+ const rawFormat = argv.some((a) => a === "--format=json" || argv[argv.indexOf("--format") + 1] === "json" && a === "--format") ? "json" : "text";
583
+ const format = hasJsonFlag ? "json" : rawFormat;
584
+ return {
585
+ json: format === "json",
586
+ color: !argv.includes("--no-color") && process.env.NO_COLOR !== "1",
587
+ debug: argv.includes("--debug") || process.env[DONEBEAR_DEBUG_ENV] === "1",
588
+ tokenOverride: null,
589
+ apiUrlOverride: null,
590
+ format,
591
+ copy: argv.includes("--copy"),
592
+ total: argv.includes("--total")
593
+ };
594
+ }
595
+ async function runWithErrorHandling(context, action) {
596
+ try {
597
+ await action();
598
+ } catch (error) {
599
+ const cliError = toCliError(error);
600
+ if (context.json) writeJson({
601
+ ok: false,
602
+ error: {
603
+ message: cliError.message,
604
+ exitCode: cliError.exitCode
605
+ }
606
+ });
607
+ else writeError(context, cliError.message);
608
+ if (context.debug && error instanceof Error && typeof error.stack === "string") writeDebug(context, error.stack);
609
+ process.exitCode = cliError.exitCode;
610
+ }
611
+ }
612
+
613
+ //#endregion
614
+ //#region src/commands/auth.ts
615
+ const SUPPORTED_PROVIDERS = ["google", "github"];
616
+ function parseProvider(value) {
617
+ const normalized = value.trim().toLowerCase();
618
+ if (SUPPORTED_PROVIDERS.some((provider) => provider === normalized)) return normalized;
619
+ throw new Error(`Unsupported provider "${value}". Supported providers: ${SUPPORTED_PROVIDERS.join(", ")}.`);
620
+ }
621
+ async function runLogin(context, options) {
622
+ const provider = parseProvider(options.provider);
623
+ const config = resolveSupabaseConfig();
624
+ const redirectUrl = new URL(`http://127.0.0.1:${options.port}${DEFAULT_OAUTH_CALLBACK_PATH}`);
625
+ const state = createOAuthState();
626
+ const client = createPkceSupabaseClient(config);
627
+ const { data: oauthStart, error: oauthStartError } = await client.auth.signInWithOAuth({
628
+ provider,
629
+ options: {
630
+ redirectTo: redirectUrl.toString(),
631
+ queryParams: { state },
632
+ skipBrowserRedirect: true
633
+ }
634
+ });
635
+ if (oauthStartError) throw new Error(`Failed to start OAuth flow: ${oauthStartError.message}`);
636
+ if (!oauthStart.url) throw new Error("Supabase did not return an OAuth URL");
637
+ const callbackPromise = waitForOAuthCode({
638
+ redirectUrl,
639
+ expectedState: state,
640
+ timeoutMs: options.timeout * 1e3
641
+ });
642
+ if (!options.open) {
643
+ writeInfo(context, "Open this URL to continue authentication:");
644
+ writeInfo(context, oauthStart.url);
645
+ } else if (await openUrl(oauthStart.url)) writeInfo(context, "Opened browser for authentication.");
646
+ else {
647
+ writeWarning(context, "Failed to open the browser automatically. Open this URL manually:");
648
+ writeInfo(context, oauthStart.url);
649
+ }
650
+ const authorizationCode = await callbackPromise;
651
+ const { data: sessionData, error: exchangeError } = await client.auth.exchangeCodeForSession(authorizationCode);
652
+ if (exchangeError) throw new Error(`OAuth callback exchange failed: ${exchangeError.message}`);
653
+ if (!sessionData.session) throw new Error("OAuth callback did not return a session");
654
+ const storedSession = toStoredAuthSession(sessionData.session, provider);
655
+ await writeStoredAuthSession(storedSession);
656
+ if (context.json) {
657
+ writeJson({
658
+ ok: true,
659
+ provider,
660
+ user: storedSession.user,
661
+ expiresAt: storedSession.expiresAt,
662
+ source: "stored"
663
+ });
664
+ return;
665
+ }
666
+ writeSuccess(context, "Authentication succeeded.");
667
+ if (storedSession.user?.email) writeInfo(context, `Signed in as ${storedSession.user.email}`);
668
+ }
669
+ async function runStatus(context) {
670
+ const token = await resolveAuthToken(context, getOptionalSupabaseConfig$1());
671
+ if (!token) {
672
+ if (context.json) writeJson({
673
+ authenticated: false,
674
+ source: null
675
+ });
676
+ else {
677
+ writeInfo(context, "Not authenticated.");
678
+ writeInfo(context, "Run `donebear auth login` to authenticate.");
679
+ }
680
+ process.exitCode = EXIT_CODES.AUTH_REQUIRED;
681
+ return;
682
+ }
683
+ const tokenStatus = resolveTokenStatus(token);
684
+ if (context.json) {
685
+ writeJson({
686
+ authenticated: true,
687
+ source: token.source,
688
+ user: token.user,
689
+ expiresAt: token.expiresAt,
690
+ expiresInSeconds: tokenStatus.expiresInSeconds,
691
+ expired: tokenStatus.expired
692
+ });
693
+ return;
694
+ }
695
+ writeSuccess(context, "Authenticated.");
696
+ writeInfo(context, `Source: ${token.source}`);
697
+ if (token.user?.email) writeInfo(context, `User: ${token.user.email}`);
698
+ if (token.expiresAt) writeInfo(context, `Expires at: ${token.expiresAt}`);
699
+ }
700
+ async function runLogout(context) {
701
+ const existingSession = await readStoredAuthSession();
702
+ await clearStoredAuthSession();
703
+ if (context.json) {
704
+ writeJson({
705
+ ok: true,
706
+ removedStoredCredentials: existingSession !== null
707
+ });
708
+ return;
709
+ }
710
+ if (existingSession) writeSuccess(context, "Stored credentials removed.");
711
+ else writeInfo(context, "No stored credentials found.");
712
+ }
713
+ function getOptionalSupabaseConfig$1() {
714
+ try {
715
+ return resolveSupabaseConfig();
716
+ } catch {
717
+ return null;
718
+ }
719
+ }
720
+ function registerAuthCommands(program) {
721
+ const auth = program.command("auth").description("Authenticate donebear CLI").addHelpText("after", `
722
+ Examples:
723
+ donebear auth
724
+ donebear auth login --provider github
725
+ donebear auth status --json
726
+ donebear auth logout
727
+ `).action(async (_options, command) => {
728
+ const context = contextFromCommand(command);
729
+ await runWithErrorHandling(context, async () => {
730
+ await runStatus(context);
731
+ });
732
+ });
733
+ auth.command("login").description("Authenticate with OAuth in the browser").option("-p, --provider <provider>", `OAuth provider (${SUPPORTED_PROVIDERS.join("|")})`, DEFAULT_OAUTH_PROVIDER).option("--port <port>", "Loopback callback port", (value) => parsePositiveInteger(value, "Port"), DEFAULT_OAUTH_CALLBACK_PORT).option("--timeout <seconds>", "OAuth timeout in seconds", (value) => parsePositiveInteger(value, "Timeout"), DEFAULT_OAUTH_TIMEOUT_SECONDS).option("--no-open", "Print URL instead of opening the browser").action(async (options, command) => {
734
+ const context = contextFromCommand(command);
735
+ await runWithErrorHandling(context, async () => {
736
+ await runLogin(context, options);
737
+ });
738
+ });
739
+ auth.command("status").description("Check authentication status").action(async (_options, command) => {
740
+ const context = contextFromCommand(command);
741
+ await runWithErrorHandling(context, async () => {
742
+ await runStatus(context);
743
+ });
744
+ });
745
+ auth.command("logout").alias("clear").description("Remove local cached credentials").action(async (_options, command) => {
746
+ const context = contextFromCommand(command);
747
+ await runWithErrorHandling(context, async () => {
748
+ await runLogout(context);
749
+ });
750
+ });
751
+ }
752
+
753
+ //#endregion
754
+ //#region src/context-store.ts
755
+ function isRecord(value) {
756
+ return typeof value === "object" && value !== null;
757
+ }
758
+ function parseLocalContext(value) {
759
+ if (!isRecord(value)) return null;
760
+ if (value.workspaceId !== null && typeof value.workspaceId !== "string") return null;
761
+ if (typeof value.clientId !== "string" || value.clientId.length === 0) return null;
762
+ return {
763
+ workspaceId: value.workspaceId,
764
+ clientId: value.clientId
765
+ };
766
+ }
767
+ function parseContextFile(payload) {
768
+ const parsed = JSON.parse(payload);
769
+ if (!isRecord(parsed) || parsed.version !== 1) throw new CliError(`Invalid CLI context format in ${CONTEXT_FILE_PATH}`, EXIT_CODES.ERROR);
770
+ const context = parseLocalContext(parsed.context);
771
+ if (!context) throw new CliError(`Invalid CLI context in ${CONTEXT_FILE_PATH}`, EXIT_CODES.ERROR);
772
+ return {
773
+ version: 1,
774
+ context
775
+ };
776
+ }
777
+ async function writeContext(context) {
778
+ const payload = {
779
+ version: 1,
780
+ context
781
+ };
782
+ await mkdir(CONFIG_DIR, {
783
+ recursive: true,
784
+ mode: 448
785
+ });
786
+ await writeFile(CONTEXT_FILE_PATH, `${JSON.stringify(payload, null, 2)}\n`, {
787
+ encoding: "utf8",
788
+ mode: 384
789
+ });
790
+ }
791
+ async function readLocalContext() {
792
+ try {
793
+ return parseContextFile(await readFile(CONTEXT_FILE_PATH, "utf8")).context;
794
+ } catch (error) {
795
+ if (isRecord(error) && error.code === "ENOENT") return null;
796
+ if (error instanceof CliError) throw error;
797
+ throw new CliError(`Failed to read context from ${CONTEXT_FILE_PATH}`, EXIT_CODES.ERROR);
798
+ }
799
+ }
800
+ async function ensureLocalContext() {
801
+ const existing = await readLocalContext();
802
+ if (existing) return existing;
803
+ const created = {
804
+ workspaceId: null,
805
+ clientId: randomUUID()
806
+ };
807
+ await writeContext(created);
808
+ return created;
809
+ }
810
+ async function setCurrentWorkspace(workspaceId) {
811
+ const nextContext = {
812
+ ...await ensureLocalContext(),
813
+ workspaceId
814
+ };
815
+ await writeContext(nextContext);
816
+ return nextContext;
817
+ }
818
+
819
+ //#endregion
820
+ //#region src/manage-api.ts
821
+ const TASKS_QUERY = `
822
+ query DonebearTasks($first: Int!, $after: String, $workspaceId: ID!) {
823
+ tasks(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
824
+ nodes {
825
+ id
826
+ title
827
+ description
828
+ createdAt
829
+ updatedAt
830
+ completedAt
831
+ archivedAt
832
+ start
833
+ startDate
834
+ startBucket
835
+ todayIndexReferenceDate
836
+ deadlineAt
837
+ creatorId
838
+ workspaceId
839
+ projectId
840
+ teamId
841
+ assigneeId
842
+ headingId
843
+ }
844
+ pageInfo {
845
+ hasNextPage
846
+ endCursor
847
+ }
848
+ }
849
+ }
850
+ `;
851
+ const PROJECTS_QUERY = `
852
+ query DonebearProjects($first: Int!, $after: String, $workspaceId: ID!) {
853
+ projects(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
854
+ nodes {
855
+ id
856
+ key
857
+ name
858
+ description
859
+ status
860
+ sortOrder
861
+ targetDate
862
+ completedAt
863
+ archivedAt
864
+ createdAt
865
+ updatedAt
866
+ workspaceId
867
+ creatorId
868
+ }
869
+ pageInfo {
870
+ hasNextPage
871
+ endCursor
872
+ }
873
+ }
874
+ }
875
+ `;
876
+ const LABELS_QUERY = `
877
+ query DonebearLabels($first: Int!, $after: String, $workspaceId: ID!) {
878
+ labels(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
879
+ nodes {
880
+ id
881
+ title
882
+ workspaceId
883
+ createdAt
884
+ updatedAt
885
+ }
886
+ pageInfo {
887
+ hasNextPage
888
+ endCursor
889
+ }
890
+ }
891
+ }
892
+ `;
893
+ const TEAMS_QUERY = `
894
+ query DonebearTeams($first: Int!, $after: String, $workspaceId: ID!) {
895
+ teams(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
896
+ nodes {
897
+ id
898
+ key
899
+ name
900
+ description
901
+ workspaceId
902
+ createdAt
903
+ updatedAt
904
+ archivedAt
905
+ }
906
+ pageInfo {
907
+ hasNextPage
908
+ endCursor
909
+ }
910
+ }
911
+ }
912
+ `;
913
+ function normalizeApiUrl(url) {
914
+ return url.endsWith("/") ? url.slice(0, -1) : url;
915
+ }
916
+ function firstDefined(keys) {
917
+ for (const key of keys) {
918
+ const value = readEnvValue(key);
919
+ if (value) return value;
920
+ }
921
+ }
922
+ function extractErrorMessage(value) {
923
+ if (typeof value === "string") return value;
924
+ if (typeof value !== "object" || value === null) return "Request failed";
925
+ const record = value;
926
+ if (typeof record.message === "string") return record.message;
927
+ if (typeof record.error === "string") return record.error;
928
+ return "Request failed";
929
+ }
930
+ async function request(_context, endpoint, options) {
931
+ const headers = new Headers({
932
+ Authorization: `Bearer ${options.token}`,
933
+ Accept: "application/json"
934
+ });
935
+ if (options.body !== void 0) headers.set("Content-Type", "application/json");
936
+ const response = await fetch(endpoint, {
937
+ method: options.method ?? "GET",
938
+ headers,
939
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
940
+ });
941
+ if (response.ok) return response;
942
+ const raw = await response.text();
943
+ let parsed = null;
944
+ try {
945
+ parsed = raw.length > 0 ? JSON.parse(raw) : null;
946
+ } catch {
947
+ parsed = null;
948
+ }
949
+ let message = `HTTP ${response.status}`;
950
+ if (parsed !== null) message = extractErrorMessage(parsed);
951
+ else if (raw.length > 0) message = raw;
952
+ const exitCode = response.status === 401 || response.status === 403 ? EXIT_CODES.AUTH_REQUIRED : EXIT_CODES.ERROR;
953
+ throw new CliError(message, exitCode);
954
+ }
955
+ async function requestJson(context, endpoint, options) {
956
+ return await (await request(context, endpoint, options)).json();
957
+ }
958
+ async function requestNdjson(context, endpoint, options) {
959
+ const lines = (await (await request(context, endpoint, {
960
+ method: "POST",
961
+ token: options.token,
962
+ body: options.body
963
+ })).text()).split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
964
+ const parsed = [];
965
+ for (const line of lines) parsed.push(JSON.parse(line));
966
+ return parsed;
967
+ }
968
+ async function requestGraphql(context, endpoint, options) {
969
+ const response = await requestJson(context, endpoint, {
970
+ method: "POST",
971
+ token: options.token,
972
+ body: {
973
+ query: options.query,
974
+ variables: options.variables
975
+ }
976
+ });
977
+ if (Array.isArray(response.errors) && response.errors.length > 0) {
978
+ const first = response.errors[0];
979
+ throw new CliError(typeof first?.message === "string" ? first.message : "GraphQL request failed", EXIT_CODES.ERROR);
980
+ }
981
+ if (!response.data) throw new CliError("GraphQL request returned no data", EXIT_CODES.ERROR);
982
+ return response.data;
983
+ }
984
+ function toTaskRecord(value) {
985
+ if (typeof value !== "object" || value === null) return null;
986
+ const row = value;
987
+ if (row.__class !== "Task") return null;
988
+ if (typeof row.id !== "string" || typeof row.title !== "string") return null;
989
+ if (typeof row.creatorId !== "string" || typeof row.workspaceId !== "string" || typeof row.start !== "string" || typeof row.startBucket !== "string") return null;
990
+ return {
991
+ id: row.id,
992
+ title: row.title,
993
+ description: typeof row.description === "string" ? row.description : null,
994
+ createdAt: toNullableNumber(row.createdAt),
995
+ updatedAt: toNullableNumber(row.updatedAt),
996
+ completedAt: toNullableNumber(row.completedAt),
997
+ archivedAt: toNullableNumber(row.archivedAt),
998
+ start: row.start,
999
+ startDate: toNullableNumber(row.startDate),
1000
+ startBucket: row.startBucket,
1001
+ todayIndexReferenceDate: toNullableNumber(row.todayIndexReferenceDate),
1002
+ deadlineAt: toNullableNumber(row.deadlineAt),
1003
+ creatorId: row.creatorId,
1004
+ workspaceId: row.workspaceId,
1005
+ projectId: typeof row.projectId === "string" ? row.projectId : null,
1006
+ teamId: typeof row.teamId === "string" ? row.teamId : null,
1007
+ assigneeId: typeof row.assigneeId === "string" ? row.assigneeId : null,
1008
+ headingId: typeof row.headingId === "string" ? row.headingId : null
1009
+ };
1010
+ }
1011
+ function toNullableNumber(input) {
1012
+ if (input === null || typeof input === "undefined") return null;
1013
+ return typeof input === "number" ? input : null;
1014
+ }
1015
+ function parseIsoDateToEpoch(value) {
1016
+ if (typeof value !== "string") return null;
1017
+ const parsed = Date.parse(value);
1018
+ return Number.isNaN(parsed) ? null : parsed;
1019
+ }
1020
+ function toTaskRecordFromGraphqlNode(node) {
1021
+ return {
1022
+ id: node.id,
1023
+ title: node.title,
1024
+ description: node.description,
1025
+ createdAt: parseIsoDateToEpoch(node.createdAt),
1026
+ updatedAt: parseIsoDateToEpoch(node.updatedAt),
1027
+ completedAt: parseIsoDateToEpoch(node.completedAt),
1028
+ archivedAt: parseIsoDateToEpoch(node.archivedAt),
1029
+ start: node.start,
1030
+ startDate: parseIsoDateToEpoch(node.startDate),
1031
+ startBucket: node.startBucket,
1032
+ todayIndexReferenceDate: parseIsoDateToEpoch(node.todayIndexReferenceDate),
1033
+ deadlineAt: parseIsoDateToEpoch(node.deadlineAt),
1034
+ creatorId: node.creatorId,
1035
+ workspaceId: node.workspaceId,
1036
+ projectId: node.projectId,
1037
+ teamId: node.teamId,
1038
+ assigneeId: node.assigneeId,
1039
+ headingId: node.headingId
1040
+ };
1041
+ }
1042
+ function toProjectRecordFromGraphqlNode(node) {
1043
+ return {
1044
+ id: node.id,
1045
+ key: node.key,
1046
+ name: node.name,
1047
+ description: node.description,
1048
+ status: node.status,
1049
+ sortOrder: node.sortOrder,
1050
+ targetDate: parseIsoDateToEpoch(node.targetDate),
1051
+ completedAt: parseIsoDateToEpoch(node.completedAt),
1052
+ archivedAt: parseIsoDateToEpoch(node.archivedAt),
1053
+ createdAt: parseIsoDateToEpoch(node.createdAt),
1054
+ updatedAt: parseIsoDateToEpoch(node.updatedAt),
1055
+ workspaceId: node.workspaceId,
1056
+ creatorId: node.creatorId
1057
+ };
1058
+ }
1059
+ function toLabelRecordFromGraphqlNode(node) {
1060
+ return {
1061
+ id: node.id,
1062
+ title: node.title,
1063
+ workspaceId: node.workspaceId,
1064
+ createdAt: parseIsoDateToEpoch(node.createdAt),
1065
+ updatedAt: parseIsoDateToEpoch(node.updatedAt)
1066
+ };
1067
+ }
1068
+ function toTeamRecordFromGraphqlNode(node) {
1069
+ return {
1070
+ id: node.id,
1071
+ key: node.key,
1072
+ name: node.name,
1073
+ description: node.description,
1074
+ workspaceId: node.workspaceId,
1075
+ createdAt: parseIsoDateToEpoch(node.createdAt),
1076
+ updatedAt: parseIsoDateToEpoch(node.updatedAt),
1077
+ archivedAt: parseIsoDateToEpoch(node.archivedAt)
1078
+ };
1079
+ }
1080
+ function toChecklistItemRecord(value) {
1081
+ if (typeof value !== "object" || value === null) return null;
1082
+ const row = value;
1083
+ if (row.__class !== "TaskChecklistItem") return null;
1084
+ if (typeof row.id !== "string" || typeof row.title !== "string") return null;
1085
+ if (typeof row.taskId !== "string" || typeof row.workspaceId !== "string") return null;
1086
+ return {
1087
+ id: row.id,
1088
+ title: row.title,
1089
+ taskId: row.taskId,
1090
+ workspaceId: row.workspaceId,
1091
+ sortOrder: typeof row.sortOrder === "number" ? row.sortOrder : 0,
1092
+ completedAt: toNullableNumber(row.completedAt),
1093
+ createdAt: toNullableNumber(row.createdAt),
1094
+ updatedAt: toNullableNumber(row.updatedAt)
1095
+ };
1096
+ }
1097
+ function resolveApiBaseUrl(context) {
1098
+ const fromContext = context.apiUrlOverride?.trim();
1099
+ if (fromContext) return normalizeApiUrl(fromContext);
1100
+ return normalizeApiUrl(readEnvValue(DONEBEAR_API_URL_ENV) ?? firstDefined(FALLBACK_API_URL_ENV_KEYS) ?? DEFAULT_API_URL);
1101
+ }
1102
+ async function listWorkspaces(context, options) {
1103
+ return (await requestJson(context, `${options.baseUrl}/api/workspaces`, { token: options.token })).workspaces;
1104
+ }
1105
+ async function createWorkspace(context, options) {
1106
+ return (await requestJson(context, `${options.baseUrl}/api/workspaces`, {
1107
+ method: "POST",
1108
+ token: options.token,
1109
+ body: options.body
1110
+ })).workspace;
1111
+ }
1112
+ function joinWorkspace(context, options) {
1113
+ return requestJson(context, `${options.baseUrl}/api/workspaces/join`, {
1114
+ method: "POST",
1115
+ token: options.token,
1116
+ body: { code: options.code }
1117
+ });
1118
+ }
1119
+ async function loadTasksByWorkspace(context, options) {
1120
+ const endpoint = `${options.baseUrl}/graphql`;
1121
+ const results = [];
1122
+ let after = null;
1123
+ let pageCount = 0;
1124
+ while (true) {
1125
+ pageCount += 1;
1126
+ if (pageCount > 1e3) throw new CliError("GraphQL pagination exceeded safe page limit", EXIT_CODES.ERROR);
1127
+ const data = await requestGraphql(context, endpoint, {
1128
+ token: options.token,
1129
+ query: TASKS_QUERY,
1130
+ variables: {
1131
+ first: 100,
1132
+ after,
1133
+ workspaceId: options.workspaceId
1134
+ }
1135
+ });
1136
+ for (const node of data.tasks.nodes) results.push(toTaskRecordFromGraphqlNode(node));
1137
+ const nextCursor = data.tasks.pageInfo.hasNextPage ? data.tasks.pageInfo.endCursor : null;
1138
+ if (!nextCursor) break;
1139
+ after = nextCursor;
1140
+ }
1141
+ return results;
1142
+ }
1143
+ async function loadTaskById(context, options) {
1144
+ const url = `${options.baseUrl}/sync/batch`;
1145
+ const body = { requests: [{
1146
+ modelName: "Task",
1147
+ indexedKey: "id",
1148
+ keyValue: options.taskId
1149
+ }] };
1150
+ const rows = await requestNdjson(context, url, {
1151
+ token: options.token,
1152
+ body
1153
+ });
1154
+ for (const row of rows) {
1155
+ const task = toTaskRecord(row);
1156
+ if (task) return task;
1157
+ }
1158
+ return null;
1159
+ }
1160
+ async function mutateTask(context, options) {
1161
+ const response = await requestJson(context, `${options.baseUrl}/sync/mutate`, {
1162
+ method: "POST",
1163
+ token: options.token,
1164
+ body: options.request
1165
+ });
1166
+ if (!response.success) {
1167
+ const error = response.results.find((result) => !result.success);
1168
+ if (error?.error) throw new CliError(error.error, EXIT_CODES.ERROR);
1169
+ throw new CliError("Sync mutation failed", EXIT_CODES.ERROR);
1170
+ }
1171
+ }
1172
+ async function loadProjectsByWorkspace(context, options) {
1173
+ const endpoint = `${options.baseUrl}/graphql`;
1174
+ const results = [];
1175
+ let after = null;
1176
+ let pageCount = 0;
1177
+ while (true) {
1178
+ pageCount += 1;
1179
+ if (pageCount > 1e3) throw new CliError("GraphQL pagination exceeded safe page limit", EXIT_CODES.ERROR);
1180
+ const data = await requestGraphql(context, endpoint, {
1181
+ token: options.token,
1182
+ query: PROJECTS_QUERY,
1183
+ variables: {
1184
+ first: 100,
1185
+ after,
1186
+ workspaceId: options.workspaceId
1187
+ }
1188
+ });
1189
+ for (const node of data.projects.nodes) results.push(toProjectRecordFromGraphqlNode(node));
1190
+ const nextCursor = data.projects.pageInfo.hasNextPage ? data.projects.pageInfo.endCursor : null;
1191
+ if (!nextCursor) break;
1192
+ after = nextCursor;
1193
+ }
1194
+ return results;
1195
+ }
1196
+ async function loadLabelsByWorkspace(context, options) {
1197
+ const endpoint = `${options.baseUrl}/graphql`;
1198
+ const results = [];
1199
+ let after = null;
1200
+ let pageCount = 0;
1201
+ while (true) {
1202
+ pageCount += 1;
1203
+ if (pageCount > 1e3) throw new CliError("GraphQL pagination exceeded safe page limit", EXIT_CODES.ERROR);
1204
+ const data = await requestGraphql(context, endpoint, {
1205
+ token: options.token,
1206
+ query: LABELS_QUERY,
1207
+ variables: {
1208
+ first: 100,
1209
+ after,
1210
+ workspaceId: options.workspaceId
1211
+ }
1212
+ });
1213
+ for (const node of data.labels.nodes) results.push(toLabelRecordFromGraphqlNode(node));
1214
+ const nextCursor = data.labels.pageInfo.hasNextPage ? data.labels.pageInfo.endCursor : null;
1215
+ if (!nextCursor) break;
1216
+ after = nextCursor;
1217
+ }
1218
+ return results;
1219
+ }
1220
+ async function loadTeamsByWorkspace(context, options) {
1221
+ const endpoint = `${options.baseUrl}/graphql`;
1222
+ const results = [];
1223
+ let after = null;
1224
+ let pageCount = 0;
1225
+ while (true) {
1226
+ pageCount += 1;
1227
+ if (pageCount > 1e3) throw new CliError("GraphQL pagination exceeded safe page limit", EXIT_CODES.ERROR);
1228
+ const data = await requestGraphql(context, endpoint, {
1229
+ token: options.token,
1230
+ query: TEAMS_QUERY,
1231
+ variables: {
1232
+ first: 100,
1233
+ after,
1234
+ workspaceId: options.workspaceId
1235
+ }
1236
+ });
1237
+ for (const node of data.teams.nodes) results.push(toTeamRecordFromGraphqlNode(node));
1238
+ const nextCursor = data.teams.pageInfo.hasNextPage ? data.teams.pageInfo.endCursor : null;
1239
+ if (!nextCursor) break;
1240
+ after = nextCursor;
1241
+ }
1242
+ return results;
1243
+ }
1244
+ async function loadChecklistItemsByTask(context, options) {
1245
+ const url = `${options.baseUrl}/sync/batch`;
1246
+ const body = { requests: [{
1247
+ modelName: "TaskChecklistItem",
1248
+ indexedKey: "taskId",
1249
+ keyValue: options.taskId
1250
+ }] };
1251
+ const rows = await requestNdjson(context, url, {
1252
+ token: options.token,
1253
+ body
1254
+ });
1255
+ const results = [];
1256
+ for (const row of rows) {
1257
+ const item = toChecklistItemRecord(row);
1258
+ if (item) results.push(item);
1259
+ }
1260
+ return results;
1261
+ }
1262
+ async function mutateModel(context, options) {
1263
+ const response = await requestJson(context, `${options.baseUrl}/sync/mutate`, {
1264
+ method: "POST",
1265
+ token: options.token,
1266
+ body: options.request
1267
+ });
1268
+ if (!response.success) {
1269
+ const error = response.results.find((result) => !result.success);
1270
+ if (error?.error) throw new CliError(error.error, EXIT_CODES.ERROR);
1271
+ throw new CliError("Sync mutation failed", EXIT_CODES.ERROR);
1272
+ }
1273
+ }
1274
+ async function listWorkspaceMembers(context, options) {
1275
+ const data = await requestJson(context, `${options.baseUrl}/api/workspaces/${options.workspaceId}/members`, { token: options.token });
1276
+ return Array.isArray(data) ? data : data.members ?? [];
1277
+ }
1278
+ async function listWorkspaceInvitations(context, options) {
1279
+ const data = await requestJson(context, `${options.baseUrl}/api/workspaces/${options.workspaceId}/invitations`, { token: options.token });
1280
+ return Array.isArray(data) ? data : data.invitations ?? [];
1281
+ }
1282
+ async function createWorkspaceInvitation(context, options) {
1283
+ const url = `${options.baseUrl}/api/workspaces/${options.workspaceId}/invitations`;
1284
+ const body = {};
1285
+ if (options.email !== void 0) body.email = options.email;
1286
+ if (options.role !== void 0) body.role = options.role;
1287
+ const data = await requestJson(context, url, {
1288
+ method: "POST",
1289
+ token: options.token,
1290
+ body
1291
+ });
1292
+ if ("id" in data && typeof data.id === "string") return data;
1293
+ const nested = data.invitation;
1294
+ if (nested) return nested;
1295
+ throw new CliError("Invalid response from invitation endpoint", EXIT_CODES.ERROR);
1296
+ }
1297
+ async function listWorkspaceHistory(context, options) {
1298
+ const params = new URLSearchParams({ limit: String(options.limit) });
1299
+ if (options.model) params.set("model", options.model);
1300
+ return (await requestJson(context, `${options.baseUrl}/api/workspaces/${options.workspaceId}/history?${params.toString()}`, { token: options.token })).history;
1301
+ }
1302
+
1303
+ //#endregion
1304
+ //#region src/command-context.ts
1305
+ function getOptionalSupabaseConfig() {
1306
+ try {
1307
+ return resolveSupabaseConfig();
1308
+ } catch {
1309
+ return null;
1310
+ }
1311
+ }
1312
+ async function requireAuthToken(context) {
1313
+ const resolvedToken = await resolveAuthToken(context, getOptionalSupabaseConfig());
1314
+ if (!resolvedToken) throw new CliError("Not authenticated. Run `donebear auth login`.", EXIT_CODES.AUTH_REQUIRED);
1315
+ return {
1316
+ token: resolvedToken.token,
1317
+ resolvedToken
1318
+ };
1319
+ }
1320
+ async function resolveCommandContext(context) {
1321
+ const { token, resolvedToken } = await requireAuthToken(context);
1322
+ if (!token) throw new CliError("Missing access token", EXIT_CODES.AUTH_REQUIRED);
1323
+ return {
1324
+ token,
1325
+ resolvedToken,
1326
+ apiBaseUrl: resolveApiBaseUrl(context),
1327
+ localContext: await ensureLocalContext()
1328
+ };
1329
+ }
1330
+ function requireUserId(resolvedToken) {
1331
+ const userId = resolvedToken.user?.id ?? null;
1332
+ if (!(typeof userId === "string" && userId.length > 0)) throw new CliError("Could not determine user id from auth token", EXIT_CODES.AUTH_REQUIRED);
1333
+ return userId;
1334
+ }
1335
+
1336
+ //#endregion
1337
+ //#region src/task-defaults.ts
1338
+ const DAY_MS = 1440 * 60 * 1e3;
1339
+ function startOfDayEpoch(now) {
1340
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
1341
+ }
1342
+ function buildTaskStartFields(view, now = /* @__PURE__ */ new Date()) {
1343
+ const today = startOfDayEpoch(now);
1344
+ const tomorrow = today + DAY_MS;
1345
+ switch (view) {
1346
+ case "inbox": return {
1347
+ start: "not_started",
1348
+ startBucket: "today",
1349
+ startDate: null,
1350
+ todayIndexReferenceDate: null
1351
+ };
1352
+ case "anytime": return {
1353
+ start: "started",
1354
+ startBucket: "today",
1355
+ startDate: null,
1356
+ todayIndexReferenceDate: null
1357
+ };
1358
+ case "today": return {
1359
+ start: "started",
1360
+ startBucket: "today",
1361
+ startDate: today,
1362
+ todayIndexReferenceDate: today
1363
+ };
1364
+ case "upcoming": return {
1365
+ start: "started",
1366
+ startBucket: "upcoming",
1367
+ startDate: tomorrow,
1368
+ todayIndexReferenceDate: null
1369
+ };
1370
+ case "someday": return {
1371
+ start: "someday",
1372
+ startBucket: "today",
1373
+ startDate: null,
1374
+ todayIndexReferenceDate: null
1375
+ };
1376
+ default: return {
1377
+ start: "not_started",
1378
+ startBucket: "today",
1379
+ startDate: null,
1380
+ todayIndexReferenceDate: null
1381
+ };
1382
+ }
1383
+ }
1384
+ function getTaskState(task) {
1385
+ if (task.archivedAt !== null) return "archived";
1386
+ if (task.completedAt !== null) return "done";
1387
+ return "open";
1388
+ }
1389
+ function formatEpochDate(epochMs) {
1390
+ if (epochMs === null) return null;
1391
+ const date = new Date(epochMs);
1392
+ if (Number.isNaN(date.getTime())) return null;
1393
+ return date.toISOString();
1394
+ }
1395
+ function formatShortDate(epochMs) {
1396
+ const iso = formatEpochDate(epochMs);
1397
+ if (!iso) return null;
1398
+ return iso.slice(0, 10);
1399
+ }
1400
+
1401
+ //#endregion
1402
+ //#region src/command-helpers.ts
1403
+ function normalizeWorkspaceRef(value) {
1404
+ const normalized = value?.trim() ?? "";
1405
+ return normalized.length > 0 ? normalized : null;
1406
+ }
1407
+ function parseTaskState(value) {
1408
+ switch (value.trim().toLowerCase()) {
1409
+ case "open":
1410
+ case "todo":
1411
+ case "pending": return "open";
1412
+ case "done":
1413
+ case "complete":
1414
+ case "completed": return "done";
1415
+ case "archived":
1416
+ case "archive": return "archived";
1417
+ case "all": return "all";
1418
+ default: throw new Error(`Invalid state "${value}". Expected one of: open, done, archived, all.`);
1419
+ }
1420
+ }
1421
+ function toTaskMarker(task) {
1422
+ const state = getTaskState(task);
1423
+ if (state === "done") return "[x]";
1424
+ if (state === "archived") return "[-]";
1425
+ return "[ ]";
1426
+ }
1427
+
1428
+ //#endregion
1429
+ //#region src/workspace-context.ts
1430
+ const WORKSPACE_URL_KEY_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1431
+ function normalize(value) {
1432
+ return value.trim().toLowerCase();
1433
+ }
1434
+ function byReference(workspaces, reference) {
1435
+ const normalized = normalize(reference);
1436
+ for (const workspace of workspaces) {
1437
+ if (workspace.id === reference) return workspace;
1438
+ if (workspace.urlKey && normalize(workspace.urlKey) === normalized) return workspace;
1439
+ if (normalize(workspace.name) === normalized) return workspace;
1440
+ }
1441
+ return null;
1442
+ }
1443
+ function toWorkspaceUrlKey(name) {
1444
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/-{2,}/g, "-").slice(0, 50);
1445
+ }
1446
+ function isValidWorkspaceUrlKey(value) {
1447
+ return WORKSPACE_URL_KEY_REGEX.test(value) && value.length >= 3;
1448
+ }
1449
+ async function resolveWorkspace(context, options) {
1450
+ const workspaces = await listWorkspaces(context, {
1451
+ token: options.token,
1452
+ baseUrl: options.apiBaseUrl
1453
+ });
1454
+ if (workspaces.length === 0) throw new Error("No workspaces available for this account");
1455
+ if (options.workspaceRef) {
1456
+ const selected = byReference(workspaces, options.workspaceRef);
1457
+ if (!selected) throw new Error(`Workspace "${options.workspaceRef}" not found. Run \`donebear workspace list\`.`);
1458
+ return {
1459
+ workspace: selected,
1460
+ autoSelected: false
1461
+ };
1462
+ }
1463
+ if (options.storedWorkspaceId) {
1464
+ const selected = byReference(workspaces, options.storedWorkspaceId);
1465
+ if (selected) return {
1466
+ workspace: selected,
1467
+ autoSelected: false
1468
+ };
1469
+ }
1470
+ if (workspaces.length === 1) {
1471
+ const workspace = workspaces[0];
1472
+ if (!workspace) throw new Error("No workspaces available for this account");
1473
+ await setCurrentWorkspace(workspace.id);
1474
+ return {
1475
+ workspace,
1476
+ autoSelected: true
1477
+ };
1478
+ }
1479
+ throw new Error("No default workspace selected. Run `donebear workspace use <id-or-slug>`.");
1480
+ }
1481
+
1482
+ //#endregion
1483
+ //#region src/commands/history.ts
1484
+ const DEFAULT_HISTORY_LIMIT = 50;
1485
+ const ACTION_LABELS = {
1486
+ I: "CREATE",
1487
+ U: "UPDATE",
1488
+ A: "ARCHIVE",
1489
+ D: "DELETE",
1490
+ V: "UNARCHIVE"
1491
+ };
1492
+ function formatActionCode(action) {
1493
+ return ACTION_LABELS[action] ?? action;
1494
+ }
1495
+ function formatHistoryDate(isoString) {
1496
+ const date = new Date(isoString);
1497
+ if (Number.isNaN(date.getTime())) return isoString;
1498
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
1499
+ }
1500
+ function extractPayloadTitle(entry) {
1501
+ if (!entry.payload) return "";
1502
+ const title = entry.payload.title;
1503
+ if (typeof title === "string") return ` (title: "${title}")`;
1504
+ return "";
1505
+ }
1506
+ async function runHistory(context, workspaceOverride, options) {
1507
+ const commandContext = await resolveCommandContext(context);
1508
+ const { workspace } = await resolveWorkspace(context, {
1509
+ token: commandContext.token,
1510
+ apiBaseUrl: commandContext.apiBaseUrl,
1511
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
1512
+ storedWorkspaceId: commandContext.localContext.workspaceId
1513
+ });
1514
+ try {
1515
+ const history = await listWorkspaceHistory(context, {
1516
+ baseUrl: commandContext.apiBaseUrl,
1517
+ token: commandContext.token,
1518
+ workspaceId: workspace.id,
1519
+ limit: options.limit,
1520
+ model: options.model
1521
+ });
1522
+ if (context.json) {
1523
+ writeJson({
1524
+ workspace,
1525
+ history
1526
+ });
1527
+ return;
1528
+ }
1529
+ const historyHeaders = [
1530
+ "date",
1531
+ "action",
1532
+ "model",
1533
+ "modelId",
1534
+ "title"
1535
+ ];
1536
+ const historyRows = history.map((entry) => [
1537
+ formatHistoryDate(entry.createdAt),
1538
+ formatActionCode(entry.action),
1539
+ entry.modelName,
1540
+ entry.modelId.slice(0, 8),
1541
+ typeof entry.payload?.title === "string" ? entry.payload.title : ""
1542
+ ]);
1543
+ if (writeFormattedRows(context, history.length, historyHeaders, historyRows)) return;
1544
+ if (history.length === 0) {
1545
+ writeInfo(context, "No history entries found.");
1546
+ return;
1547
+ }
1548
+ writeInfo(context, `History for ${workspace.name}:`);
1549
+ for (const entry of history) {
1550
+ const date = formatHistoryDate(entry.createdAt);
1551
+ const action = formatActionCode(entry.action);
1552
+ const modelId = entry.modelId.slice(0, 8);
1553
+ const title = extractPayloadTitle(entry);
1554
+ writeInfo(context, `${date} ${action} ${entry.modelName} ${modelId}...${title}`);
1555
+ }
1556
+ } catch {
1557
+ writeWarning(context, "History is not available for this server version.");
1558
+ writeWarning(context, "Upgrade your manage-api to enable audit logs.");
1559
+ }
1560
+ }
1561
+ function registerHistoryCommand(program, workspaceOverride) {
1562
+ program.command("history").description("Show recent workspace audit log").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--model <model>", "Filter by model name: Task|Project|Label|Team").option("-n, --limit <count>", "Maximum number of entries", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_HISTORY_LIMIT).addHelpText("after", `
1563
+ Examples:
1564
+ donebear history
1565
+ donebear history --model Task --limit 20
1566
+ donebear history --format csv
1567
+ `).action(async (options, command) => {
1568
+ const context = contextFromCommand(command);
1569
+ await runWithErrorHandling(context, async () => {
1570
+ await runHistory(context, workspaceOverride, options);
1571
+ });
1572
+ });
1573
+ }
1574
+
1575
+ //#endregion
1576
+ //#region src/commands/interactive.ts
1577
+ const HISTORY_FILE = join(CONFIG_DIR, "history.json");
1578
+ const MAX_HISTORY = 200;
1579
+ function isHistoryFile(value) {
1580
+ if (typeof value !== "object" || value === null) return false;
1581
+ const record = value;
1582
+ return record.version === 1 && Array.isArray(record.entries) && record.entries.every((e) => typeof e === "string");
1583
+ }
1584
+ async function loadHistory(historyFile) {
1585
+ try {
1586
+ const raw = await readFile(historyFile, "utf8");
1587
+ const parsed = JSON.parse(raw);
1588
+ if (isHistoryFile(parsed)) return parsed.entries;
1589
+ return [];
1590
+ } catch {
1591
+ return [];
1592
+ }
1593
+ }
1594
+ function tokenize(input) {
1595
+ const tokens = [];
1596
+ let current = "";
1597
+ let inQuote = false;
1598
+ let quoteChar = "";
1599
+ for (const char of input) if (inQuote) if (char === quoteChar) inQuote = false;
1600
+ else current += char;
1601
+ else if (char === "\"" || char === "'") {
1602
+ inQuote = true;
1603
+ quoteChar = char;
1604
+ } else if (char === " ") {
1605
+ if (current.length > 0) {
1606
+ tokens.push(current);
1607
+ current = "";
1608
+ }
1609
+ } else current += char;
1610
+ if (current.length > 0) tokens.push(current);
1611
+ return tokens;
1612
+ }
1613
+ function buildCompleter(program) {
1614
+ return (line) => {
1615
+ const allCommands = program.commands.flatMap((cmd) => [cmd.name(), ...cmd.aliases()]);
1616
+ const hits = allCommands.filter((c) => c.startsWith(line));
1617
+ return [hits.length > 0 ? hits : allCommands, line];
1618
+ };
1619
+ }
1620
+ async function runInteractiveMode(context, program) {
1621
+ const history = await loadHistory(HISTORY_FILE);
1622
+ const rl = createInterface({
1623
+ input: process.stdin,
1624
+ output: process.stdout,
1625
+ prompt: "donebear> ",
1626
+ historySize: MAX_HISTORY,
1627
+ completer: buildCompleter(program)
1628
+ });
1629
+ for (const entry of [...history].reverse()) rl.history.push(entry);
1630
+ writeSuccess(context, "Done Bear interactive mode. Type \"exit\" or Ctrl+D to quit.");
1631
+ writeInfo(context, "Commands: task, workspace, project, label, team, today, search, auth, whoami");
1632
+ rl.prompt();
1633
+ const sessionEntries = [];
1634
+ for await (const line of rl) {
1635
+ const trimmed = line.trim();
1636
+ if (!trimmed) {
1637
+ rl.prompt();
1638
+ continue;
1639
+ }
1640
+ if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") break;
1641
+ const tokens = tokenize(trimmed);
1642
+ try {
1643
+ await program.parseAsync([
1644
+ "node",
1645
+ "donebear",
1646
+ ...tokens
1647
+ ]);
1648
+ } catch {}
1649
+ sessionEntries.push(trimmed);
1650
+ rl.prompt();
1651
+ }
1652
+ rl.close();
1653
+ if (sessionEntries.length > 0) try {
1654
+ const payload = {
1655
+ version: 1,
1656
+ entries: [...await loadHistory(HISTORY_FILE), ...sessionEntries].slice(-MAX_HISTORY)
1657
+ };
1658
+ await mkdir(CONFIG_DIR, {
1659
+ recursive: true,
1660
+ mode: 448
1661
+ });
1662
+ await writeFile(HISTORY_FILE, `${JSON.stringify(payload, null, 2)}\n`, {
1663
+ encoding: "utf8",
1664
+ mode: 384
1665
+ });
1666
+ } catch {}
1667
+ }
1668
+
1669
+ //#endregion
1670
+ //#region src/commands/label.ts
1671
+ function sortLabels(labels, sort) {
1672
+ const sorted = [...labels];
1673
+ if (sort === "alpha") sorted.sort((a, b) => a.title.localeCompare(b.title));
1674
+ else sorted.sort((a, b) => a.title.localeCompare(b.title));
1675
+ return sorted;
1676
+ }
1677
+ async function resolveLabel(context, options) {
1678
+ const { workspace } = await resolveWorkspace(context, {
1679
+ token: options.commandContext.token,
1680
+ apiBaseUrl: options.commandContext.apiBaseUrl,
1681
+ workspaceRef: options.workspaceRef,
1682
+ storedWorkspaceId: options.commandContext.localContext.workspaceId
1683
+ });
1684
+ const labels = await loadLabelsByWorkspace(context, {
1685
+ baseUrl: options.commandContext.apiBaseUrl,
1686
+ token: options.commandContext.token,
1687
+ workspaceId: workspace.id
1688
+ });
1689
+ const normalized = options.labelRef.trim().toLowerCase();
1690
+ const found = labels.find((l) => l.id === options.labelRef || l.title.toLowerCase() === normalized || l.id.startsWith(options.labelRef));
1691
+ if (!found) throw new Error(`Label "${options.labelRef}" not found.`);
1692
+ return found;
1693
+ }
1694
+ async function runLabelList(context, options) {
1695
+ const commandContext = await resolveCommandContext(context);
1696
+ const { workspace, autoSelected } = await resolveWorkspace(context, {
1697
+ token: commandContext.token,
1698
+ apiBaseUrl: commandContext.apiBaseUrl,
1699
+ workspaceRef: normalizeWorkspaceRef(options.workspace),
1700
+ storedWorkspaceId: commandContext.localContext.workspaceId
1701
+ });
1702
+ const sorted = sortLabels(await loadLabelsByWorkspace(context, {
1703
+ baseUrl: commandContext.apiBaseUrl,
1704
+ token: commandContext.token,
1705
+ workspaceId: workspace.id
1706
+ }), options.sort);
1707
+ if (context.json) {
1708
+ writeJson({
1709
+ workspace,
1710
+ autoSelected,
1711
+ total: sorted.length,
1712
+ labels: sorted
1713
+ });
1714
+ return;
1715
+ }
1716
+ const headers = [
1717
+ "id",
1718
+ "title",
1719
+ "createdAt",
1720
+ "updatedAt"
1721
+ ];
1722
+ const rows = sorted.map((l) => [
1723
+ l.id,
1724
+ l.title,
1725
+ formatEpochDate(l.createdAt) ?? "",
1726
+ formatEpochDate(l.updatedAt) ?? ""
1727
+ ]);
1728
+ if (writeFormattedRows(context, sorted.length, headers, rows)) return;
1729
+ if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
1730
+ if (sorted.length === 0) {
1731
+ writeInfo(context, "No labels found.");
1732
+ return;
1733
+ }
1734
+ writeInfo(context, `${workspace.name} (${sorted.length} label(s))`);
1735
+ for (const label of sorted) writeInfo(context, `${label.id.slice(0, 8)} ${label.title}`);
1736
+ if (context.copy) copyToClipboard(sorted.map((l) => `${l.id.slice(0, 8)} ${l.title}`).join("\n"));
1737
+ }
1738
+ async function runLabelShow(context, labelRef, options) {
1739
+ const label = await resolveLabel(context, {
1740
+ labelRef,
1741
+ commandContext: await resolveCommandContext(context),
1742
+ workspaceRef: normalizeWorkspaceRef(options.workspace)
1743
+ });
1744
+ if (context.json) {
1745
+ writeJson({
1746
+ label,
1747
+ createdAtIso: formatEpochDate(label.createdAt),
1748
+ updatedAtIso: formatEpochDate(label.updatedAt)
1749
+ });
1750
+ return;
1751
+ }
1752
+ writeSuccess(context, label.title);
1753
+ writeInfo(context, `id: ${label.id}`);
1754
+ writeInfo(context, `workspaceId: ${label.workspaceId}`);
1755
+ writeInfo(context, `createdAt: ${formatEpochDate(label.createdAt) ?? "(none)"}`);
1756
+ writeInfo(context, `updatedAt: ${formatEpochDate(label.updatedAt) ?? "(none)"}`);
1757
+ }
1758
+ function registerLabelCommands(program, workspaceOverride) {
1759
+ const label = program.command("label").alias("labels").description("Manage labels").addHelpText("after", `
1760
+ Examples:
1761
+ donebear label
1762
+ donebear label list --sort alpha
1763
+ donebear label show bug
1764
+ `).action(async (_options, command) => {
1765
+ const context = contextFromCommand(command);
1766
+ await runWithErrorHandling(context, async () => {
1767
+ await runLabelList(context, { sort: "alpha" });
1768
+ });
1769
+ });
1770
+ label.command("list").alias("ls").description("List labels").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--sort <order>", "Sort order: alpha|count", "alpha").action(async (options, command) => {
1771
+ const context = contextFromCommand(command);
1772
+ await runWithErrorHandling(context, async () => {
1773
+ await runLabelList(context, {
1774
+ ...options,
1775
+ workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0
1776
+ });
1777
+ });
1778
+ });
1779
+ label.command("show").description("Show label details").argument("<id-or-title>", "Label id, title, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (labelRef, options, command) => {
1780
+ const context = contextFromCommand(command);
1781
+ await runWithErrorHandling(context, async () => {
1782
+ await runLabelShow(context, labelRef, { workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0 });
1783
+ });
1784
+ });
1785
+ }
1786
+
1787
+ //#endregion
1788
+ //#region src/commands/project.ts
1789
+ const ISO_YMD_REGEX$1 = /^(\d{4})-(\d{2})-(\d{2})$/;
1790
+ function parseProjectStatus(value) {
1791
+ switch (value.trim().toLowerCase()) {
1792
+ case "active": return "active";
1793
+ case "done":
1794
+ case "complete":
1795
+ case "completed": return "done";
1796
+ case "archived":
1797
+ case "archive": return "archived";
1798
+ case "all": return "all";
1799
+ default: throw new Error(`Invalid status "${value}". Expected one of: active, done, archived, all.`);
1800
+ }
1801
+ }
1802
+ function getProjectStatus(project) {
1803
+ if (project.archivedAt !== null) return "archived";
1804
+ if (project.completedAt !== null) return "done";
1805
+ return "active";
1806
+ }
1807
+ function parseTargetDate(value) {
1808
+ if (!value) return null;
1809
+ const trimmed = value.trim();
1810
+ if (ISO_YMD_REGEX$1.exec(trimmed)) {
1811
+ const year = Number.parseInt(trimmed.slice(0, 4), 10);
1812
+ const month = Number.parseInt(trimmed.slice(5, 7), 10);
1813
+ const day = Number.parseInt(trimmed.slice(8, 10), 10);
1814
+ const date = new Date(year, month - 1, day);
1815
+ if (Number.isNaN(date.getTime())) throw new Error(`Invalid target date: ${value}`);
1816
+ return date.getTime();
1817
+ }
1818
+ const parsed = Date.parse(trimmed);
1819
+ if (Number.isNaN(parsed)) throw new Error(`Invalid target date: ${value}`);
1820
+ return parsed;
1821
+ }
1822
+ const WHITESPACE_REGEX = /\s+/;
1823
+ function autoGenerateKey(name) {
1824
+ return name.trim().split(WHITESPACE_REGEX).map((word) => word[0]?.toUpperCase() ?? "").join("").replace(/[^A-Z0-9]/g, "").slice(0, 5) || "PROJ";
1825
+ }
1826
+ function filterProjects(projects, status) {
1827
+ if (status === "all") return projects;
1828
+ return projects.filter((p) => getProjectStatus(p) === status);
1829
+ }
1830
+ function sortProjectsByUpdatedDesc(projects) {
1831
+ return [...projects].sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
1832
+ }
1833
+ async function resolveProjectReference(context, options) {
1834
+ const { workspace } = await resolveWorkspace(context, {
1835
+ token: options.commandContext.token,
1836
+ apiBaseUrl: options.commandContext.apiBaseUrl,
1837
+ workspaceRef: options.workspaceRef,
1838
+ storedWorkspaceId: options.commandContext.localContext.workspaceId
1839
+ });
1840
+ const projects = await loadProjectsByWorkspace(context, {
1841
+ baseUrl: options.commandContext.apiBaseUrl,
1842
+ token: options.commandContext.token,
1843
+ workspaceId: workspace.id
1844
+ });
1845
+ const normalized = options.projectRef.trim().toLowerCase();
1846
+ const found = projects.find((p) => p.id === options.projectRef || p.key.toLowerCase() === normalized || p.id.startsWith(options.projectRef));
1847
+ if (!found) throw new Error(`Project "${options.projectRef}" not found.`);
1848
+ return {
1849
+ project: found,
1850
+ workspace
1851
+ };
1852
+ }
1853
+ async function runProjectList(context, options) {
1854
+ const commandContext = await resolveCommandContext(context);
1855
+ const requestedStatus = parseProjectStatus(options.status);
1856
+ const { workspace, autoSelected } = await resolveWorkspace(context, {
1857
+ token: commandContext.token,
1858
+ apiBaseUrl: commandContext.apiBaseUrl,
1859
+ workspaceRef: normalizeWorkspaceRef(options.workspace),
1860
+ storedWorkspaceId: commandContext.localContext.workspaceId
1861
+ });
1862
+ const filtered = filterProjects(sortProjectsByUpdatedDesc(await loadProjectsByWorkspace(context, {
1863
+ baseUrl: commandContext.apiBaseUrl,
1864
+ token: commandContext.token,
1865
+ workspaceId: workspace.id
1866
+ })), requestedStatus);
1867
+ if (context.json) {
1868
+ writeJson({
1869
+ workspace,
1870
+ autoSelected,
1871
+ status: requestedStatus,
1872
+ total: filtered.length,
1873
+ projects: filtered
1874
+ });
1875
+ return;
1876
+ }
1877
+ const headers = [
1878
+ "id",
1879
+ "key",
1880
+ "name",
1881
+ "status",
1882
+ "targetDate",
1883
+ "updatedAt"
1884
+ ];
1885
+ const rows = filtered.map((p) => [
1886
+ p.id,
1887
+ p.key,
1888
+ p.name,
1889
+ getProjectStatus(p),
1890
+ formatShortDate(p.targetDate) ?? "",
1891
+ formatShortDate(p.updatedAt) ?? ""
1892
+ ]);
1893
+ if (writeFormattedRows(context, filtered.length, headers, rows)) return;
1894
+ if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
1895
+ if (filtered.length === 0) {
1896
+ writeInfo(context, "No projects found.");
1897
+ return;
1898
+ }
1899
+ writeInfo(context, `${workspace.name} (${filtered.length} project(s))`);
1900
+ for (const project of filtered) {
1901
+ const status = getProjectStatus(project);
1902
+ const target = formatShortDate(project.targetDate);
1903
+ const suffix = target ? ` due ${target}` : "";
1904
+ writeInfo(context, `${project.id.slice(0, 8)} [${project.key}] ${project.name} (${status})${suffix}`);
1905
+ }
1906
+ if (context.copy) copyToClipboard(filtered.map((p) => `${p.id.slice(0, 8)} [${p.key}] ${p.name} (${getProjectStatus(p)})`).join("\n"));
1907
+ }
1908
+ async function runProjectShow(context, projectRef, options) {
1909
+ const { project, workspace } = await resolveProjectReference(context, {
1910
+ projectRef,
1911
+ commandContext: await resolveCommandContext(context),
1912
+ workspaceRef: normalizeWorkspaceRef(options.workspace)
1913
+ });
1914
+ if (context.json) {
1915
+ writeJson({
1916
+ project,
1917
+ status: getProjectStatus(project),
1918
+ workspace,
1919
+ createdAtIso: formatEpochDate(project.createdAt),
1920
+ updatedAtIso: formatEpochDate(project.updatedAt),
1921
+ completedAtIso: formatEpochDate(project.completedAt),
1922
+ archivedAtIso: formatEpochDate(project.archivedAt),
1923
+ targetDateIso: formatEpochDate(project.targetDate)
1924
+ });
1925
+ return;
1926
+ }
1927
+ writeSuccess(context, project.name);
1928
+ writeInfo(context, `id: ${project.id}`);
1929
+ writeInfo(context, `key: ${project.key}`);
1930
+ writeInfo(context, `status: ${getProjectStatus(project)}`);
1931
+ writeInfo(context, `workspaceId: ${project.workspaceId}`);
1932
+ writeInfo(context, `targetDate: ${formatEpochDate(project.targetDate) ?? "(none)"}`);
1933
+ writeInfo(context, `createdAt: ${formatEpochDate(project.createdAt) ?? "(none)"}`);
1934
+ writeInfo(context, `updatedAt: ${formatEpochDate(project.updatedAt) ?? "(none)"}`);
1935
+ if (project.description) writeInfo(context, `description: ${project.description}`);
1936
+ }
1937
+ async function runProjectAdd(context, name, options) {
1938
+ const commandContext = await resolveCommandContext(context);
1939
+ const userId = requireUserId(commandContext.resolvedToken);
1940
+ const normalizedName = name.trim();
1941
+ if (normalizedName.length === 0) throw new Error("Project name cannot be empty.");
1942
+ const { workspace, autoSelected } = await resolveWorkspace(context, {
1943
+ token: commandContext.token,
1944
+ apiBaseUrl: commandContext.apiBaseUrl,
1945
+ workspaceRef: normalizeWorkspaceRef(options.workspace),
1946
+ storedWorkspaceId: commandContext.localContext.workspaceId
1947
+ });
1948
+ const projectId = randomUUID();
1949
+ const now = Date.now();
1950
+ const key = options.key?.trim().toUpperCase() || autoGenerateKey(normalizedName);
1951
+ await mutateModel(context, {
1952
+ baseUrl: commandContext.apiBaseUrl,
1953
+ token: commandContext.token,
1954
+ request: {
1955
+ batchId: randomUUID(),
1956
+ transactions: [{
1957
+ clientTxId: randomUUID(),
1958
+ clientId: commandContext.localContext.clientId,
1959
+ modelName: "Project",
1960
+ modelId: projectId,
1961
+ action: "INSERT",
1962
+ payload: {
1963
+ name: normalizedName,
1964
+ key,
1965
+ description: options.description?.trim() || null,
1966
+ status: "active",
1967
+ sortOrder: 0,
1968
+ targetDate: parseTargetDate(options.targetDate),
1969
+ completedAt: null,
1970
+ archivedAt: null,
1971
+ creatorId: userId,
1972
+ workspaceId: workspace.id,
1973
+ createdAt: now,
1974
+ updatedAt: now
1975
+ }
1976
+ }]
1977
+ }
1978
+ });
1979
+ if (context.json) {
1980
+ writeJson({
1981
+ created: true,
1982
+ id: projectId,
1983
+ key,
1984
+ workspaceId: workspace.id,
1985
+ workspaceName: workspace.name,
1986
+ autoSelected
1987
+ });
1988
+ return;
1989
+ }
1990
+ if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
1991
+ writeSuccess(context, `Added project: ${normalizedName}`);
1992
+ writeInfo(context, `id: ${projectId}`);
1993
+ writeInfo(context, `key: ${key}`);
1994
+ }
1995
+ function buildProjectUpdates(options) {
1996
+ if (options.clearDescription && typeof options.description === "string") throw new Error("Use either --description or --clear-description, not both.");
1997
+ if (options.clearTargetDate && typeof options.targetDate === "string") throw new Error("Use either --target-date or --clear-target-date, not both.");
1998
+ const updates = {};
1999
+ if (typeof options.name === "string") {
2000
+ const name = options.name.trim();
2001
+ if (name.length === 0) throw new Error("Project name cannot be empty.");
2002
+ updates.name = name;
2003
+ }
2004
+ if (options.clearDescription) updates.description = null;
2005
+ else if (typeof options.description === "string") {
2006
+ const desc = options.description.trim();
2007
+ updates.description = desc.length > 0 ? desc : null;
2008
+ }
2009
+ if (options.clearTargetDate) updates.targetDate = null;
2010
+ else if (typeof options.targetDate === "string") updates.targetDate = parseTargetDate(options.targetDate);
2011
+ if (typeof options.status === "string") applyStatusToUpdates(updates, parseProjectStatus(options.status));
2012
+ if (Object.keys(updates).length === 0) throw new Error("No updates provided. Use --name, --description, --clear-description, --status, --target-date, or --clear-target-date.");
2013
+ updates.updatedAt = Date.now();
2014
+ return updates;
2015
+ }
2016
+ function applyStatusToUpdates(updates, status) {
2017
+ if (status === "done") {
2018
+ updates.completedAt = Date.now();
2019
+ updates.archivedAt = null;
2020
+ } else if (status === "active") {
2021
+ updates.completedAt = null;
2022
+ updates.archivedAt = null;
2023
+ } else if (status === "archived") updates.archivedAt = Date.now();
2024
+ }
2025
+ async function runProjectEdit(context, projectRef, options) {
2026
+ const updates = buildProjectUpdates(options);
2027
+ const commandContext = await resolveCommandContext(context);
2028
+ const { project } = await resolveProjectReference(context, {
2029
+ projectRef,
2030
+ commandContext,
2031
+ workspaceRef: normalizeWorkspaceRef(options.workspace)
2032
+ });
2033
+ await mutateModel(context, {
2034
+ baseUrl: commandContext.apiBaseUrl,
2035
+ token: commandContext.token,
2036
+ request: {
2037
+ batchId: randomUUID(),
2038
+ transactions: [{
2039
+ clientTxId: randomUUID(),
2040
+ clientId: commandContext.localContext.clientId,
2041
+ modelName: "Project",
2042
+ modelId: project.id,
2043
+ action: "UPDATE",
2044
+ payload: updates
2045
+ }]
2046
+ }
2047
+ });
2048
+ if (context.json) {
2049
+ writeJson({
2050
+ ok: true,
2051
+ id: project.id,
2052
+ action: "UPDATE"
2053
+ });
2054
+ return;
2055
+ }
2056
+ writeSuccess(context, `Updated project: ${project.id}`);
2057
+ }
2058
+ async function runProjectMutation(context, projectRef, payload) {
2059
+ const commandContext = await resolveCommandContext(context);
2060
+ const { project } = await resolveProjectReference(context, {
2061
+ projectRef,
2062
+ commandContext,
2063
+ workspaceRef: payload.workspaceRef
2064
+ });
2065
+ await mutateModel(context, {
2066
+ baseUrl: commandContext.apiBaseUrl,
2067
+ token: commandContext.token,
2068
+ request: {
2069
+ batchId: randomUUID(),
2070
+ transactions: [{
2071
+ clientTxId: randomUUID(),
2072
+ clientId: commandContext.localContext.clientId,
2073
+ modelName: "Project",
2074
+ modelId: project.id,
2075
+ action: payload.action,
2076
+ payload: payload.data
2077
+ }]
2078
+ }
2079
+ });
2080
+ if (context.json) {
2081
+ writeJson({
2082
+ ok: true,
2083
+ id: project.id,
2084
+ action: payload.action
2085
+ });
2086
+ return;
2087
+ }
2088
+ writeSuccess(context, `${payload.successMessage}: ${project.id}`);
2089
+ }
2090
+ function registerProjectCommands(program, workspaceOverride) {
2091
+ const project = program.command("project").alias("projects").description("Manage projects").addHelpText("after", `
2092
+ Examples:
2093
+ donebear project
2094
+ donebear project add "Q1 Launch" --key LAUNCH
2095
+ donebear project list --status active
2096
+ donebear project edit LAUNCH --target-date 2026-03-31
2097
+ `).action(async (_options, command) => {
2098
+ const context = contextFromCommand(command);
2099
+ await runWithErrorHandling(context, async () => {
2100
+ await runProjectList(context, { status: "active" });
2101
+ });
2102
+ });
2103
+ project.command("list").alias("ls").description("List projects").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --status <status>", "active|done|archived|all", "active").action(async (options, command) => {
2104
+ const context = contextFromCommand(command);
2105
+ await runWithErrorHandling(context, async () => {
2106
+ await runProjectList(context, {
2107
+ ...options,
2108
+ workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0
2109
+ });
2110
+ });
2111
+ });
2112
+ project.command("show").description("Show project details").argument("<id-or-key>", "Project id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (projectRef, options, command) => {
2113
+ const context = contextFromCommand(command);
2114
+ await runWithErrorHandling(context, async () => {
2115
+ await runProjectShow(context, projectRef, { workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0 });
2116
+ });
2117
+ });
2118
+ project.command("add").alias("create").description("Create a project").argument("<name>", "Project name").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--key <KEY>", "Short project key (auto-generated if not provided)").option("-d, --description <text>", "Project description").option("--target-date <date>", "Target date (YYYY-MM-DD)").action(async (name, options, command) => {
2119
+ const context = contextFromCommand(command);
2120
+ await runWithErrorHandling(context, async () => {
2121
+ await runProjectAdd(context, name, {
2122
+ ...options,
2123
+ workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0
2124
+ });
2125
+ });
2126
+ });
2127
+ project.command("edit").description("Update project fields").argument("<id-or-key>", "Project id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--name <name>", "Replace project name").option("-d, --description <text>", "Replace project description").option("--clear-description", "Clear project description").option("--status <status>", "Set status (active|done|archived)").option("--target-date <date>", "Set target date (YYYY-MM-DD)").option("--clear-target-date", "Clear target date").action(async (projectRef, options, command) => {
2128
+ const context = contextFromCommand(command);
2129
+ await runWithErrorHandling(context, async () => {
2130
+ await runProjectEdit(context, projectRef, {
2131
+ ...options,
2132
+ workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0
2133
+ });
2134
+ });
2135
+ });
2136
+ project.command("done").alias("complete").description("Mark a project as completed").argument("<id-or-key>", "Project id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (projectRef, options, command) => {
2137
+ const context = contextFromCommand(command);
2138
+ await runWithErrorHandling(context, async () => {
2139
+ const now = Date.now();
2140
+ await runProjectMutation(context, projectRef, {
2141
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2142
+ action: "UPDATE",
2143
+ data: {
2144
+ completedAt: now,
2145
+ updatedAt: now
2146
+ },
2147
+ successMessage: "Marked done"
2148
+ });
2149
+ });
2150
+ });
2151
+ project.command("archive").description("Archive a project").argument("<id-or-key>", "Project id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (projectRef, options, command) => {
2152
+ const context = contextFromCommand(command);
2153
+ await runWithErrorHandling(context, async () => {
2154
+ await runProjectMutation(context, projectRef, {
2155
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2156
+ action: "ARCHIVE",
2157
+ data: { archivedAt: Date.now() },
2158
+ successMessage: "Archived"
2159
+ });
2160
+ });
2161
+ });
2162
+ project.command("unarchive").alias("restore").description("Unarchive a project").argument("<id-or-key>", "Project id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (projectRef, options, command) => {
2163
+ const context = contextFromCommand(command);
2164
+ await runWithErrorHandling(context, async () => {
2165
+ await runProjectMutation(context, projectRef, {
2166
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2167
+ action: "UNARCHIVE",
2168
+ data: {},
2169
+ successMessage: "Unarchived"
2170
+ });
2171
+ });
2172
+ });
2173
+ }
2174
+
2175
+ //#endregion
2176
+ //#region src/commands/task.ts
2177
+ const ISO_YMD_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/;
2178
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
2179
+ const DEFAULT_TASK_LIST_LIMIT = 20;
2180
+ const VALID_TASK_VIEWS = new Set([
2181
+ "inbox",
2182
+ "anytime",
2183
+ "today",
2184
+ "upcoming",
2185
+ "someday"
2186
+ ]);
2187
+ function parseTaskView(value) {
2188
+ const normalized = value.trim().toLowerCase();
2189
+ if (VALID_TASK_VIEWS.has(normalized)) return normalized;
2190
+ throw new Error(`Invalid view "${value}". Expected one of: inbox, anytime, today, upcoming, someday.`);
2191
+ }
2192
+ function parseDeadline(value) {
2193
+ if (!value) return null;
2194
+ const trimmed = value.trim();
2195
+ if (ISO_YMD_REGEX.exec(trimmed)) {
2196
+ const year = Number.parseInt(trimmed.slice(0, 4), 10);
2197
+ const month = Number.parseInt(trimmed.slice(5, 7), 10);
2198
+ const day = Number.parseInt(trimmed.slice(8, 10), 10);
2199
+ const date = new Date(year, month - 1, day);
2200
+ if (Number.isNaN(date.getTime())) throw new Error(`Invalid deadline date: ${value}`);
2201
+ return date.getTime();
2202
+ }
2203
+ const parsed = Date.parse(trimmed);
2204
+ if (Number.isNaN(parsed)) throw new Error(`Invalid deadline date: ${value}`);
2205
+ return parsed;
2206
+ }
2207
+ function sortTasksByUpdatedDesc(tasks) {
2208
+ return [...tasks].sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
2209
+ }
2210
+ async function resolveTaskReference(context, options) {
2211
+ const taskRef = options.taskRef.trim();
2212
+ if (!taskRef) throw new Error("Task id is required.");
2213
+ const exact = await loadTaskById(context, {
2214
+ baseUrl: options.commandContext.apiBaseUrl,
2215
+ token: options.commandContext.token,
2216
+ taskId: taskRef
2217
+ });
2218
+ if (exact) return exact;
2219
+ if (UUID_REGEX.test(taskRef)) throw new Error(`Task "${taskRef}" not found.`);
2220
+ if (taskRef.length < 4) throw new Error("Task id prefix must be at least 4 characters.");
2221
+ const { workspace, autoSelected } = await resolveWorkspace(context, {
2222
+ token: options.commandContext.token,
2223
+ apiBaseUrl: options.commandContext.apiBaseUrl,
2224
+ workspaceRef: options.workspaceRef,
2225
+ storedWorkspaceId: options.commandContext.localContext.workspaceId
2226
+ });
2227
+ const matches = (await loadTasksByWorkspace(context, {
2228
+ baseUrl: options.commandContext.apiBaseUrl,
2229
+ token: options.commandContext.token,
2230
+ workspaceId: workspace.id
2231
+ })).filter((task) => task.id.startsWith(taskRef));
2232
+ if (matches.length === 0) throw new Error(`Task "${taskRef}" not found.`);
2233
+ if (matches.length === 1) {
2234
+ const [match] = matches;
2235
+ if (!match) throw new Error(`Task "${taskRef}" not found.`);
2236
+ if (autoSelected && !context.json) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
2237
+ return match;
2238
+ }
2239
+ throw new Error(`Task prefix "${taskRef}" is ambiguous (${matches.length} matches). Use the full id.`);
2240
+ }
2241
+ function filterTasks(tasks, options) {
2242
+ const stateFiltered = options.state === "all" ? tasks : tasks.filter((task) => getTaskState(task) === options.state);
2243
+ if (!options.search) return stateFiltered;
2244
+ const query = options.search.trim().toLowerCase();
2245
+ if (query.length === 0) return stateFiltered;
2246
+ return stateFiltered.filter((task) => {
2247
+ const title = task.title.toLowerCase();
2248
+ const notes = task.description?.toLowerCase() ?? "";
2249
+ return title.includes(query) || notes.includes(query);
2250
+ });
2251
+ }
2252
+ async function runTaskList(context, options, workspaceOverride) {
2253
+ const commandContext = await resolveCommandContext(context);
2254
+ const requestedState = parseTaskState(options.state);
2255
+ const { workspace, autoSelected } = await resolveWorkspace(context, {
2256
+ token: commandContext.token,
2257
+ apiBaseUrl: commandContext.apiBaseUrl,
2258
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2259
+ storedWorkspaceId: commandContext.localContext.workspaceId
2260
+ });
2261
+ const filtered = filterTasks(sortTasksByUpdatedDesc(await loadTasksByWorkspace(context, {
2262
+ baseUrl: commandContext.apiBaseUrl,
2263
+ token: commandContext.token,
2264
+ workspaceId: workspace.id
2265
+ })), {
2266
+ state: requestedState,
2267
+ search: options.search?.trim() ?? null
2268
+ });
2269
+ const limited = filtered.slice(0, options.limit);
2270
+ if (context.json) {
2271
+ writeJson({
2272
+ workspace,
2273
+ autoSelected,
2274
+ state: requestedState,
2275
+ total: filtered.length,
2276
+ count: limited.length,
2277
+ tasks: limited
2278
+ });
2279
+ return;
2280
+ }
2281
+ const headers = [
2282
+ "id",
2283
+ "title",
2284
+ "state",
2285
+ "deadline"
2286
+ ];
2287
+ const rows = limited.map((task) => [
2288
+ task.id,
2289
+ task.title,
2290
+ getTaskState(task),
2291
+ formatShortDate(task.deadlineAt) ?? ""
2292
+ ]);
2293
+ if (writeFormattedRows(context, filtered.length, headers, rows)) return;
2294
+ if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
2295
+ if (limited.length === 0) {
2296
+ writeInfo(context, "No tasks found.");
2297
+ return;
2298
+ }
2299
+ const lines = [];
2300
+ lines.push(`${workspace.name} (${limited.length}/${filtered.length})`);
2301
+ for (const task of limited) {
2302
+ const marker = toTaskMarker(task);
2303
+ const deadline = formatShortDate(task.deadlineAt);
2304
+ const suffix = deadline ? ` due ${deadline}` : "";
2305
+ lines.push(`${marker} ${task.id.slice(0, 8)} ${task.title}${suffix}`);
2306
+ }
2307
+ for (const line of lines) writeInfo(context, line);
2308
+ }
2309
+ async function runTaskShow(context, taskRef, options, workspaceOverride) {
2310
+ const task = await resolveTaskReference(context, {
2311
+ taskRef,
2312
+ commandContext: await resolveCommandContext(context),
2313
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
2314
+ });
2315
+ if (context.json) {
2316
+ writeJson({
2317
+ task,
2318
+ state: getTaskState(task),
2319
+ createdAtIso: formatEpochDate(task.createdAt),
2320
+ updatedAtIso: formatEpochDate(task.updatedAt),
2321
+ completedAtIso: formatEpochDate(task.completedAt),
2322
+ archivedAtIso: formatEpochDate(task.archivedAt),
2323
+ deadlineAtIso: formatEpochDate(task.deadlineAt)
2324
+ });
2325
+ return;
2326
+ }
2327
+ writeSuccess(context, task.title);
2328
+ writeInfo(context, `id: ${task.id}`);
2329
+ writeInfo(context, `state: ${getTaskState(task)}`);
2330
+ writeInfo(context, `workspaceId: ${task.workspaceId}`);
2331
+ writeInfo(context, `start: ${task.start}`);
2332
+ writeInfo(context, `startBucket: ${task.startBucket}`);
2333
+ writeInfo(context, `deadline: ${formatEpochDate(task.deadlineAt) ?? "(none)"}`);
2334
+ if (task.projectId) writeInfo(context, `projectId: ${task.projectId}`);
2335
+ if (task.teamId) writeInfo(context, `teamId: ${task.teamId}`);
2336
+ if (task.assigneeId) writeInfo(context, `assigneeId: ${task.assigneeId}`);
2337
+ if (task.description) writeInfo(context, `notes: ${task.description}`);
2338
+ }
2339
+ async function runTaskAdd(context, title, options, workspaceOverride) {
2340
+ const commandContext = await resolveCommandContext(context);
2341
+ const userId = requireUserId(commandContext.resolvedToken);
2342
+ const normalizedTitle = title.trim();
2343
+ if (normalizedTitle.length === 0) throw new Error("Task title cannot be empty.");
2344
+ const { workspace, autoSelected } = await resolveWorkspace(context, {
2345
+ token: commandContext.token,
2346
+ apiBaseUrl: commandContext.apiBaseUrl,
2347
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2348
+ storedWorkspaceId: commandContext.localContext.workspaceId
2349
+ });
2350
+ const [projectId, teamId] = await Promise.all([options.project ? loadProjectsByWorkspace(context, {
2351
+ baseUrl: commandContext.apiBaseUrl,
2352
+ token: commandContext.token,
2353
+ workspaceId: workspace.id
2354
+ }).then((projects) => resolveEntityId(projects, options.project, "Project")) : Promise.resolve(null), options.team ? loadTeamsByWorkspace(context, {
2355
+ baseUrl: commandContext.apiBaseUrl,
2356
+ token: commandContext.token,
2357
+ workspaceId: workspace.id
2358
+ }).then((teams) => resolveEntityId(teams, options.team, "Team")) : Promise.resolve(null)]);
2359
+ const taskId = randomUUID();
2360
+ const now = Date.now();
2361
+ const view = parseTaskView(options.when);
2362
+ const startFields = buildTaskStartFields(view);
2363
+ await mutateTask(context, {
2364
+ baseUrl: commandContext.apiBaseUrl,
2365
+ token: commandContext.token,
2366
+ request: {
2367
+ batchId: randomUUID(),
2368
+ transactions: [{
2369
+ clientTxId: randomUUID(),
2370
+ clientId: commandContext.localContext.clientId,
2371
+ modelName: "Task",
2372
+ modelId: taskId,
2373
+ action: "INSERT",
2374
+ payload: {
2375
+ title: normalizedTitle,
2376
+ description: options.notes?.trim() || null,
2377
+ creatorId: userId,
2378
+ workspaceId: workspace.id,
2379
+ createdAt: now,
2380
+ updatedAt: now,
2381
+ sortOrder: 0,
2382
+ todaySortOrder: 0,
2383
+ completedAt: null,
2384
+ archivedAt: null,
2385
+ deadlineAt: parseDeadline(options.deadline),
2386
+ projectId,
2387
+ teamId,
2388
+ ...startFields
2389
+ }
2390
+ }]
2391
+ }
2392
+ });
2393
+ if (context.json) {
2394
+ writeJson({
2395
+ created: true,
2396
+ id: taskId,
2397
+ workspaceId: workspace.id,
2398
+ workspaceName: workspace.name,
2399
+ autoSelected,
2400
+ view,
2401
+ projectId,
2402
+ teamId
2403
+ });
2404
+ return;
2405
+ }
2406
+ if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
2407
+ writeSuccess(context, `Added task: ${normalizedTitle}`);
2408
+ writeInfo(context, `id: ${taskId}`);
2409
+ }
2410
+ async function runTaskMutation(context, taskRef, payload) {
2411
+ const commandContext = await resolveCommandContext(context);
2412
+ const task = await resolveTaskReference(context, {
2413
+ taskRef,
2414
+ commandContext,
2415
+ workspaceRef: payload.workspaceRef
2416
+ });
2417
+ await mutateTask(context, {
2418
+ baseUrl: commandContext.apiBaseUrl,
2419
+ token: commandContext.token,
2420
+ request: {
2421
+ batchId: randomUUID(),
2422
+ transactions: [{
2423
+ clientTxId: randomUUID(),
2424
+ clientId: commandContext.localContext.clientId,
2425
+ modelName: "Task",
2426
+ modelId: task.id,
2427
+ action: payload.action,
2428
+ payload: payload.data
2429
+ }]
2430
+ }
2431
+ });
2432
+ if (context.json) {
2433
+ writeJson({
2434
+ ok: true,
2435
+ id: task.id,
2436
+ action: payload.action
2437
+ });
2438
+ return;
2439
+ }
2440
+ writeSuccess(context, `${payload.successMessage}: ${task.id}`);
2441
+ }
2442
+ function resolveEntityId(entities, ref, label) {
2443
+ const trimmed = ref.trim();
2444
+ const lower = trimmed.toLowerCase();
2445
+ const found = entities.find((e) => e.id === trimmed || e.key.toLowerCase() === lower || e.id.startsWith(trimmed));
2446
+ if (!found) throw new Error(`${label} "${ref}" not found.`);
2447
+ return found.id;
2448
+ }
2449
+ async function resolveProjectId(context, commandContext, projectRef, workspaceRef) {
2450
+ const { workspace } = await resolveWorkspace(context, {
2451
+ token: commandContext.token,
2452
+ apiBaseUrl: commandContext.apiBaseUrl,
2453
+ workspaceRef,
2454
+ storedWorkspaceId: commandContext.localContext.workspaceId
2455
+ });
2456
+ return resolveEntityId(await loadProjectsByWorkspace(context, {
2457
+ baseUrl: commandContext.apiBaseUrl,
2458
+ token: commandContext.token,
2459
+ workspaceId: workspace.id
2460
+ }), projectRef, "Project");
2461
+ }
2462
+ async function resolveTeamId(context, commandContext, teamRef, workspaceRef) {
2463
+ const { workspace } = await resolveWorkspace(context, {
2464
+ token: commandContext.token,
2465
+ apiBaseUrl: commandContext.apiBaseUrl,
2466
+ workspaceRef,
2467
+ storedWorkspaceId: commandContext.localContext.workspaceId
2468
+ });
2469
+ return resolveEntityId(await loadTeamsByWorkspace(context, {
2470
+ baseUrl: commandContext.apiBaseUrl,
2471
+ token: commandContext.token,
2472
+ workspaceId: workspace.id
2473
+ }), teamRef, "Team");
2474
+ }
2475
+ function validateTaskEditOptions(options) {
2476
+ if (options.clearNotes && typeof options.notes === "string") throw new Error("Use either --notes or --clear-notes, not both.");
2477
+ if (options.clearDeadline && typeof options.deadline === "string") throw new Error("Use either --deadline or --clear-deadline, not both.");
2478
+ if (options.clearProject && typeof options.project === "string") throw new Error("Use either --project or --clear-project, not both.");
2479
+ if (options.clearTeam && typeof options.team === "string") throw new Error("Use either --team or --clear-team, not both.");
2480
+ }
2481
+ function applyScalarEdits(updates, options) {
2482
+ if (typeof options.title === "string") {
2483
+ const title = options.title.trim();
2484
+ if (title.length === 0) throw new Error("Task title cannot be empty.");
2485
+ updates.title = title;
2486
+ }
2487
+ if (options.clearNotes) updates.description = null;
2488
+ else if (typeof options.notes === "string") {
2489
+ const notes = options.notes.trim();
2490
+ updates.description = notes.length > 0 ? notes : null;
2491
+ }
2492
+ if (typeof options.when === "string") Object.assign(updates, buildTaskStartFields(parseTaskView(options.when)));
2493
+ if (options.clearDeadline) updates.deadlineAt = null;
2494
+ else if (typeof options.deadline === "string") updates.deadlineAt = parseDeadline(options.deadline);
2495
+ }
2496
+ async function runTaskEdit(context, taskRef, options, workspaceOverride) {
2497
+ validateTaskEditOptions(options);
2498
+ const updates = {};
2499
+ applyScalarEdits(updates, options);
2500
+ const wsRef = workspaceOverride ?? normalizeWorkspaceRef(options.workspace);
2501
+ const commandContext = await resolveCommandContext(context);
2502
+ if (options.clearProject) updates.projectId = null;
2503
+ else if (typeof options.project === "string") updates.projectId = await resolveProjectId(context, commandContext, options.project, wsRef);
2504
+ if (options.clearTeam) updates.teamId = null;
2505
+ else if (typeof options.team === "string") updates.teamId = await resolveTeamId(context, commandContext, options.team, wsRef);
2506
+ if (Object.keys(updates).length === 0) throw new Error("No updates provided. Use --title, --notes, --clear-notes, --when, --deadline, --clear-deadline, --project, --clear-project, --team, or --clear-team.");
2507
+ updates.updatedAt = Date.now();
2508
+ await runTaskMutation(context, taskRef, {
2509
+ workspaceRef: wsRef,
2510
+ action: "UPDATE",
2511
+ data: updates,
2512
+ successMessage: "Updated"
2513
+ });
2514
+ }
2515
+ async function runTaskRead(context, taskRef, options, workspaceOverride) {
2516
+ const task = await resolveTaskReference(context, {
2517
+ taskRef,
2518
+ commandContext: await resolveCommandContext(context),
2519
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
2520
+ });
2521
+ if (context.json) {
2522
+ writeJson({
2523
+ id: task.id,
2524
+ description: task.description
2525
+ });
2526
+ return;
2527
+ }
2528
+ writeInfo(context, task.description ?? "(empty)");
2529
+ }
2530
+ async function runTaskAppend(context, taskRef, text, options, workspaceOverride) {
2531
+ const task = await resolveTaskReference(context, {
2532
+ taskRef,
2533
+ commandContext: await resolveCommandContext(context),
2534
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
2535
+ });
2536
+ const newDesc = `${task.description ?? ""}\n${text.trim()}`;
2537
+ await runTaskMutation(context, task.id, {
2538
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2539
+ action: "UPDATE",
2540
+ data: {
2541
+ description: newDesc,
2542
+ updatedAt: Date.now()
2543
+ },
2544
+ successMessage: "Appended"
2545
+ });
2546
+ }
2547
+ async function runTaskPrepend(context, taskRef, text, options, workspaceOverride) {
2548
+ const task = await resolveTaskReference(context, {
2549
+ taskRef,
2550
+ commandContext: await resolveCommandContext(context),
2551
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
2552
+ });
2553
+ const existing = task.description ?? "";
2554
+ const newDesc = existing.length > 0 ? `${text.trim()}\n${existing}` : text.trim();
2555
+ await runTaskMutation(context, task.id, {
2556
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2557
+ action: "UPDATE",
2558
+ data: {
2559
+ description: newDesc,
2560
+ updatedAt: Date.now()
2561
+ },
2562
+ successMessage: "Prepended"
2563
+ });
2564
+ }
2565
+ async function runTaskRandom(context, options, workspaceOverride) {
2566
+ const commandContext = await resolveCommandContext(context);
2567
+ const requestedState = options.state ? parseTaskState(options.state) : "open";
2568
+ const { workspace, autoSelected } = await resolveWorkspace(context, {
2569
+ token: commandContext.token,
2570
+ apiBaseUrl: commandContext.apiBaseUrl,
2571
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2572
+ storedWorkspaceId: commandContext.localContext.workspaceId
2573
+ });
2574
+ const filtered = filterTasks(await loadTasksByWorkspace(context, {
2575
+ baseUrl: commandContext.apiBaseUrl,
2576
+ token: commandContext.token,
2577
+ workspaceId: workspace.id
2578
+ }), {
2579
+ state: requestedState,
2580
+ search: null
2581
+ });
2582
+ if (filtered.length === 0) {
2583
+ if (context.json) {
2584
+ writeJson({ task: null });
2585
+ return;
2586
+ }
2587
+ writeInfo(context, "No tasks found.");
2588
+ return;
2589
+ }
2590
+ const task = filtered[Math.floor(Math.random() * filtered.length)];
2591
+ if (!task) throw new Error("Failed to pick a random task.");
2592
+ if (context.json) {
2593
+ writeJson({
2594
+ task,
2595
+ state: getTaskState(task),
2596
+ createdAtIso: formatEpochDate(task.createdAt),
2597
+ updatedAtIso: formatEpochDate(task.updatedAt),
2598
+ completedAtIso: formatEpochDate(task.completedAt),
2599
+ archivedAtIso: formatEpochDate(task.archivedAt),
2600
+ deadlineAtIso: formatEpochDate(task.deadlineAt)
2601
+ });
2602
+ return;
2603
+ }
2604
+ if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
2605
+ writeSuccess(context, task.title);
2606
+ writeInfo(context, `id: ${task.id}`);
2607
+ writeInfo(context, `state: ${getTaskState(task)}`);
2608
+ writeInfo(context, `workspaceId: ${task.workspaceId}`);
2609
+ writeInfo(context, `start: ${task.start}`);
2610
+ writeInfo(context, `startBucket: ${task.startBucket}`);
2611
+ writeInfo(context, `deadline: ${formatEpochDate(task.deadlineAt) ?? "(none)"}`);
2612
+ if (task.description) writeInfo(context, `notes: ${task.description}`);
2613
+ }
2614
+ async function resolveChecklistItemReference(context, commandContext, taskId, itemRef) {
2615
+ const trimmed = itemRef.trim();
2616
+ if (!trimmed) throw new Error("Checklist item id is required.");
2617
+ const items = await loadChecklistItemsByTask(context, {
2618
+ baseUrl: commandContext.apiBaseUrl,
2619
+ token: commandContext.token,
2620
+ taskId
2621
+ });
2622
+ const exact = items.find((item) => item.id === trimmed);
2623
+ if (exact) return exact;
2624
+ const matches = items.filter((item) => item.id.startsWith(trimmed));
2625
+ if (matches.length === 0) throw new Error(`Checklist item "${trimmed}" not found.`);
2626
+ if (matches.length === 1) {
2627
+ const [match] = matches;
2628
+ if (!match) throw new Error(`Checklist item "${trimmed}" not found.`);
2629
+ return match;
2630
+ }
2631
+ throw new Error(`Checklist item prefix "${trimmed}" is ambiguous (${matches.length} matches). Use the full id.`);
2632
+ }
2633
+ async function runChecklistList(context, taskId, workspaceOption) {
2634
+ const commandContext = await resolveCommandContext(context);
2635
+ const task = await resolveTaskReference(context, {
2636
+ taskRef: taskId,
2637
+ commandContext,
2638
+ workspaceRef: workspaceOption?.trim() ?? null
2639
+ });
2640
+ const sorted = [...await loadChecklistItemsByTask(context, {
2641
+ baseUrl: commandContext.apiBaseUrl,
2642
+ token: commandContext.token,
2643
+ taskId: task.id
2644
+ })].sort((a, b) => a.sortOrder - b.sortOrder);
2645
+ if (context.json) {
2646
+ writeJson({
2647
+ taskId: task.id,
2648
+ items: sorted
2649
+ });
2650
+ return;
2651
+ }
2652
+ if (sorted.length === 0) {
2653
+ writeInfo(context, "No checklist items.");
2654
+ return;
2655
+ }
2656
+ writeInfo(context, `Checklist for: ${task.title}`);
2657
+ for (const item of sorted) writeInfo(context, `${item.completedAt !== null ? "[x]" : "[ ]"} ${item.id.slice(0, 8)} ${item.title}`);
2658
+ }
2659
+ function registerTaskCommands(program, workspaceOverride) {
2660
+ const task = program.command("task").alias("tasks").description("Manage tasks").addHelpText("after", `
2661
+ Examples:
2662
+ donebear task
2663
+ donebear task add "Write launch post" --when today
2664
+ donebear task list --state done --limit 50
2665
+ donebear task edit 8f2c1a --deadline 2026-03-05
2666
+ `).action(async (_options, command) => {
2667
+ const context = contextFromCommand(command);
2668
+ await runWithErrorHandling(context, async () => {
2669
+ await runTaskList(context, {
2670
+ state: "open",
2671
+ limit: DEFAULT_TASK_LIST_LIMIT
2672
+ }, workspaceOverride);
2673
+ });
2674
+ });
2675
+ task.command("list").alias("ls").description("List tasks").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "open").option("-n, --limit <count>", "Maximum number of tasks", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_TASK_LIST_LIMIT).option("-q, --search <query>", "Filter by title or notes").action(async (options, command) => {
2676
+ const context = contextFromCommand(command);
2677
+ await runWithErrorHandling(context, async () => {
2678
+ await runTaskList(context, options, workspaceOverride);
2679
+ });
2680
+ });
2681
+ task.command("show").description("Show task details").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
2682
+ const context = contextFromCommand(command);
2683
+ await runWithErrorHandling(context, async () => {
2684
+ await runTaskShow(context, taskId, options, workspaceOverride);
2685
+ });
2686
+ });
2687
+ task.command("read").description("Print the description (notes) of a task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
2688
+ const context = contextFromCommand(command);
2689
+ await runWithErrorHandling(context, async () => {
2690
+ await runTaskRead(context, taskId, options, workspaceOverride);
2691
+ });
2692
+ });
2693
+ task.command("append").description("Append text to a task's description").argument("<id>", "Task id or unique prefix").argument("<text>", "Text to append").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, text, options, command) => {
2694
+ const context = contextFromCommand(command);
2695
+ await runWithErrorHandling(context, async () => {
2696
+ await runTaskAppend(context, taskId, text, options, workspaceOverride);
2697
+ });
2698
+ });
2699
+ task.command("prepend").description("Prepend text to a task's description").argument("<id>", "Task id or unique prefix").argument("<text>", "Text to prepend").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, text, options, command) => {
2700
+ const context = contextFromCommand(command);
2701
+ await runWithErrorHandling(context, async () => {
2702
+ await runTaskPrepend(context, taskId, text, options, workspaceOverride);
2703
+ });
2704
+ });
2705
+ task.command("random").description("Show a random task").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "open").action(async (options, command) => {
2706
+ const context = contextFromCommand(command);
2707
+ await runWithErrorHandling(context, async () => {
2708
+ await runTaskRandom(context, options, workspaceOverride);
2709
+ });
2710
+ });
2711
+ task.command("add").alias("create").description("Create a task").argument("<title>", "Task title").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-n, --notes <text>", "Task notes").option("--when <view>", "inbox|anytime|today|upcoming|someday", "inbox").option("--deadline <date>", "Deadline (YYYY-MM-DD or ISO datetime)").option("--project <key-or-id>", "Project key or id").option("--team <key-or-id>", "Team key or id").action(async (title, options, command) => {
2712
+ const context = contextFromCommand(command);
2713
+ await runWithErrorHandling(context, async () => {
2714
+ await runTaskAdd(context, title, options, workspaceOverride);
2715
+ });
2716
+ });
2717
+ task.command("edit").description("Update task fields").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--title <title>", "Replace task title").option("--notes <text>", "Replace task notes (empty string clears)").option("--clear-notes", "Clear task notes").option("--when <view>", "Set view bucket (inbox|anytime|today|upcoming|someday)").option("--deadline <date>", "Set deadline (YYYY-MM-DD or ISO datetime)").option("--clear-deadline", "Clear deadline").option("--project <key-or-id>", "Set project (key or id)").option("--clear-project", "Remove project assignment").option("--team <key-or-id>", "Set team (key or id)").option("--clear-team", "Remove team assignment").action(async (taskId, options, command) => {
2718
+ const context = contextFromCommand(command);
2719
+ await runWithErrorHandling(context, async () => {
2720
+ await runTaskEdit(context, taskId, options, workspaceOverride);
2721
+ });
2722
+ });
2723
+ task.command("done").alias("complete").description("Mark a task as completed").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
2724
+ const context = contextFromCommand(command);
2725
+ await runWithErrorHandling(context, async () => {
2726
+ const now = Date.now();
2727
+ await runTaskMutation(context, taskId, {
2728
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2729
+ action: "UPDATE",
2730
+ data: {
2731
+ completedAt: now,
2732
+ updatedAt: now
2733
+ },
2734
+ successMessage: "Marked done"
2735
+ });
2736
+ });
2737
+ });
2738
+ task.command("reopen").alias("undo").description("Reopen a completed task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
2739
+ const context = contextFromCommand(command);
2740
+ await runWithErrorHandling(context, async () => {
2741
+ await runTaskMutation(context, taskId, {
2742
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2743
+ action: "UPDATE",
2744
+ data: {
2745
+ completedAt: null,
2746
+ updatedAt: Date.now()
2747
+ },
2748
+ successMessage: "Reopened"
2749
+ });
2750
+ });
2751
+ });
2752
+ task.command("archive").description("Archive a task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
2753
+ const context = contextFromCommand(command);
2754
+ await runWithErrorHandling(context, async () => {
2755
+ await runTaskMutation(context, taskId, {
2756
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2757
+ action: "ARCHIVE",
2758
+ data: { archivedAt: Date.now() },
2759
+ successMessage: "Archived"
2760
+ });
2761
+ });
2762
+ });
2763
+ task.command("unarchive").alias("restore").description("Unarchive a task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
2764
+ const context = contextFromCommand(command);
2765
+ await runWithErrorHandling(context, async () => {
2766
+ await runTaskMutation(context, taskId, {
2767
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2768
+ action: "UNARCHIVE",
2769
+ data: {},
2770
+ successMessage: "Unarchived"
2771
+ });
2772
+ });
2773
+ });
2774
+ const checklist = task.command("checklist").alias("cl").description("Manage task checklist items").argument("<task-id>", "Task id or prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
2775
+ const context = contextFromCommand(command);
2776
+ await runWithErrorHandling(context, async () => {
2777
+ await runChecklistList(context, taskId, options.workspace);
2778
+ });
2779
+ });
2780
+ checklist.command("add").description("Add a checklist item").argument("<task-id>", "Task id or prefix").argument("<title>", "Item title").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, title, options, command) => {
2781
+ const context = contextFromCommand(command);
2782
+ await runWithErrorHandling(context, async () => {
2783
+ const commandContext = await resolveCommandContext(context);
2784
+ const task = await resolveTaskReference(context, {
2785
+ taskRef: taskId,
2786
+ commandContext,
2787
+ workspaceRef: options.workspace?.trim() ?? null
2788
+ });
2789
+ const itemId = randomUUID();
2790
+ const now = Date.now();
2791
+ await mutateModel(context, {
2792
+ baseUrl: commandContext.apiBaseUrl,
2793
+ token: commandContext.token,
2794
+ request: {
2795
+ batchId: randomUUID(),
2796
+ transactions: [{
2797
+ clientTxId: randomUUID(),
2798
+ clientId: commandContext.localContext.clientId,
2799
+ modelName: "TaskChecklistItem",
2800
+ modelId: itemId,
2801
+ action: "INSERT",
2802
+ payload: {
2803
+ title: title.trim(),
2804
+ taskId: task.id,
2805
+ workspaceId: task.workspaceId,
2806
+ sortOrder: now,
2807
+ completedAt: null,
2808
+ createdAt: now,
2809
+ updatedAt: now
2810
+ }
2811
+ }]
2812
+ }
2813
+ });
2814
+ if (context.json) {
2815
+ writeJson({
2816
+ created: true,
2817
+ id: itemId
2818
+ });
2819
+ return;
2820
+ }
2821
+ writeSuccess(context, `Added: ${title.trim()}`);
2822
+ });
2823
+ });
2824
+ checklist.command("done").description("Mark a checklist item as complete").argument("<task-id>", "Task id or prefix").argument("<item-id>", "Checklist item id or prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, itemId, options, command) => {
2825
+ const context = contextFromCommand(command);
2826
+ await runWithErrorHandling(context, async () => {
2827
+ const commandContext = await resolveCommandContext(context);
2828
+ const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
2829
+ taskRef: taskId,
2830
+ commandContext,
2831
+ workspaceRef: options.workspace?.trim() ?? null
2832
+ })).id, itemId);
2833
+ const now = Date.now();
2834
+ await mutateModel(context, {
2835
+ baseUrl: commandContext.apiBaseUrl,
2836
+ token: commandContext.token,
2837
+ request: {
2838
+ batchId: randomUUID(),
2839
+ transactions: [{
2840
+ clientTxId: randomUUID(),
2841
+ clientId: commandContext.localContext.clientId,
2842
+ modelName: "TaskChecklistItem",
2843
+ modelId: item.id,
2844
+ action: "UPDATE",
2845
+ payload: {
2846
+ completedAt: now,
2847
+ updatedAt: now
2848
+ }
2849
+ }]
2850
+ }
2851
+ });
2852
+ if (context.json) {
2853
+ writeJson({
2854
+ ok: true,
2855
+ id: item.id
2856
+ });
2857
+ return;
2858
+ }
2859
+ writeSuccess(context, `Marked done: ${item.title}`);
2860
+ });
2861
+ });
2862
+ checklist.command("reopen").description("Reopen a completed checklist item").argument("<task-id>", "Task id or prefix").argument("<item-id>", "Checklist item id or prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, itemId, options, command) => {
2863
+ const context = contextFromCommand(command);
2864
+ await runWithErrorHandling(context, async () => {
2865
+ const commandContext = await resolveCommandContext(context);
2866
+ const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
2867
+ taskRef: taskId,
2868
+ commandContext,
2869
+ workspaceRef: options.workspace?.trim() ?? null
2870
+ })).id, itemId);
2871
+ const now = Date.now();
2872
+ await mutateModel(context, {
2873
+ baseUrl: commandContext.apiBaseUrl,
2874
+ token: commandContext.token,
2875
+ request: {
2876
+ batchId: randomUUID(),
2877
+ transactions: [{
2878
+ clientTxId: randomUUID(),
2879
+ clientId: commandContext.localContext.clientId,
2880
+ modelName: "TaskChecklistItem",
2881
+ modelId: item.id,
2882
+ action: "UPDATE",
2883
+ payload: {
2884
+ completedAt: null,
2885
+ updatedAt: now
2886
+ }
2887
+ }]
2888
+ }
2889
+ });
2890
+ if (context.json) {
2891
+ writeJson({
2892
+ ok: true,
2893
+ id: item.id
2894
+ });
2895
+ return;
2896
+ }
2897
+ writeSuccess(context, `Reopened: ${item.title}`);
2898
+ });
2899
+ });
2900
+ checklist.command("edit").description("Edit a checklist item title").argument("<task-id>", "Task id or prefix").argument("<item-id>", "Checklist item id or prefix").requiredOption("--title <title>", "New item title").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, itemId, options, command) => {
2901
+ const context = contextFromCommand(command);
2902
+ await runWithErrorHandling(context, async () => {
2903
+ const newTitle = options.title.trim();
2904
+ if (newTitle.length === 0) throw new Error("Checklist item title cannot be empty.");
2905
+ const commandContext = await resolveCommandContext(context);
2906
+ const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
2907
+ taskRef: taskId,
2908
+ commandContext,
2909
+ workspaceRef: options.workspace?.trim() ?? null
2910
+ })).id, itemId);
2911
+ const now = Date.now();
2912
+ await mutateModel(context, {
2913
+ baseUrl: commandContext.apiBaseUrl,
2914
+ token: commandContext.token,
2915
+ request: {
2916
+ batchId: randomUUID(),
2917
+ transactions: [{
2918
+ clientTxId: randomUUID(),
2919
+ clientId: commandContext.localContext.clientId,
2920
+ modelName: "TaskChecklistItem",
2921
+ modelId: item.id,
2922
+ action: "UPDATE",
2923
+ payload: {
2924
+ title: newTitle,
2925
+ updatedAt: now
2926
+ }
2927
+ }]
2928
+ }
2929
+ });
2930
+ if (context.json) {
2931
+ writeJson({
2932
+ ok: true,
2933
+ id: item.id
2934
+ });
2935
+ return;
2936
+ }
2937
+ writeSuccess(context, `Updated: ${newTitle}`);
2938
+ });
2939
+ });
2940
+ checklist.command("remove").alias("rm").description("Remove a checklist item").argument("<task-id>", "Task id or prefix").argument("<item-id>", "Checklist item id or prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, itemId, options, command) => {
2941
+ const context = contextFromCommand(command);
2942
+ await runWithErrorHandling(context, async () => {
2943
+ const commandContext = await resolveCommandContext(context);
2944
+ const item = await resolveChecklistItemReference(context, commandContext, (await resolveTaskReference(context, {
2945
+ taskRef: taskId,
2946
+ commandContext,
2947
+ workspaceRef: options.workspace?.trim() ?? null
2948
+ })).id, itemId);
2949
+ await mutateModel(context, {
2950
+ baseUrl: commandContext.apiBaseUrl,
2951
+ token: commandContext.token,
2952
+ request: {
2953
+ batchId: randomUUID(),
2954
+ transactions: [{
2955
+ clientTxId: randomUUID(),
2956
+ clientId: commandContext.localContext.clientId,
2957
+ modelName: "TaskChecklistItem",
2958
+ modelId: item.id,
2959
+ action: "ARCHIVE",
2960
+ payload: {}
2961
+ }]
2962
+ }
2963
+ });
2964
+ if (context.json) {
2965
+ writeJson({
2966
+ removed: true,
2967
+ id: item.id
2968
+ });
2969
+ return;
2970
+ }
2971
+ writeSuccess(context, `Removed: ${item.title}`);
2972
+ });
2973
+ });
2974
+ }
2975
+
2976
+ //#endregion
2977
+ //#region src/commands/search.ts
2978
+ const DEFAULT_SEARCH_LIMIT = 20;
2979
+ async function runSearch(context, query, options, workspaceOverride) {
2980
+ const commandContext = await resolveCommandContext(context);
2981
+ const requestedState = parseTaskState(options.state);
2982
+ const trimmedQuery = query.trim();
2983
+ if (trimmedQuery.length === 0) throw new Error("Search query cannot be empty.");
2984
+ const { workspace, autoSelected } = await resolveWorkspace(context, {
2985
+ token: commandContext.token,
2986
+ apiBaseUrl: commandContext.apiBaseUrl,
2987
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
2988
+ storedWorkspaceId: commandContext.localContext.workspaceId
2989
+ });
2990
+ const filtered = filterTasks(await loadTasksByWorkspace(context, {
2991
+ baseUrl: commandContext.apiBaseUrl,
2992
+ token: commandContext.token,
2993
+ workspaceId: workspace.id
2994
+ }), {
2995
+ state: requestedState,
2996
+ search: trimmedQuery
2997
+ });
2998
+ const limited = filtered.slice(0, options.limit);
2999
+ if (context.json) {
3000
+ writeJson({
3001
+ workspace,
3002
+ autoSelected,
3003
+ query: trimmedQuery,
3004
+ state: requestedState,
3005
+ total: filtered.length,
3006
+ count: limited.length,
3007
+ tasks: limited
3008
+ });
3009
+ return;
3010
+ }
3011
+ const headers = [
3012
+ "id",
3013
+ "title",
3014
+ "state",
3015
+ "deadline"
3016
+ ];
3017
+ const rows = limited.map((task) => [
3018
+ task.id,
3019
+ task.title,
3020
+ getTaskState(task),
3021
+ formatShortDate(task.deadlineAt) ?? ""
3022
+ ]);
3023
+ if (writeFormattedRows(context, filtered.length, headers, rows)) return;
3024
+ if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
3025
+ if (limited.length === 0) {
3026
+ writeInfo(context, `No tasks found matching "${trimmedQuery}".`);
3027
+ return;
3028
+ }
3029
+ const lines = [];
3030
+ lines.push(`${workspace.name} — "${trimmedQuery}" (${limited.length}/${filtered.length})`);
3031
+ for (const task of limited) {
3032
+ const marker = toTaskMarker(task);
3033
+ const deadline = formatShortDate(task.deadlineAt);
3034
+ const suffix = deadline ? ` due ${deadline}` : "";
3035
+ lines.push(`${marker} ${task.id.slice(0, 8)} ${task.title}${suffix}`);
3036
+ }
3037
+ for (const line of lines) writeInfo(context, line);
3038
+ if (context.copy) copyToClipboard(lines.join("\n"));
3039
+ }
3040
+ function registerSearchCommand(program, workspaceOverride) {
3041
+ program.command("search").description("Search tasks by title or notes").argument("<query>", "Search query").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "all").option("-n, --limit <count>", "Maximum number of results", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_SEARCH_LIMIT).addHelpText("after", `
3042
+ Examples:
3043
+ donebear search "meeting"
3044
+ donebear search "launch" --state open
3045
+ donebear search "Q1" --format csv --copy
3046
+ donebear search "review" --total
3047
+ `).action(async (query, options, command) => {
3048
+ const context = contextFromCommand(command);
3049
+ await runWithErrorHandling(context, async () => {
3050
+ await runSearch(context, query, options, workspaceOverride);
3051
+ });
3052
+ });
3053
+ }
3054
+
3055
+ //#endregion
3056
+ //#region src/commands/team.ts
3057
+ function findTeam(ref, teams) {
3058
+ const normalized = ref.trim().toLowerCase();
3059
+ for (const team of teams) {
3060
+ if (team.id === ref) return team;
3061
+ if (team.key.toLowerCase() === normalized) return team;
3062
+ if (team.id.startsWith(ref)) return team;
3063
+ }
3064
+ return null;
3065
+ }
3066
+ async function runTeamList(context, workspaceOverride, options) {
3067
+ const commandContext = await resolveCommandContext(context);
3068
+ const { workspace } = await resolveWorkspace(context, {
3069
+ token: commandContext.token,
3070
+ apiBaseUrl: commandContext.apiBaseUrl,
3071
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
3072
+ storedWorkspaceId: commandContext.localContext.workspaceId
3073
+ });
3074
+ const teams = await loadTeamsByWorkspace(context, {
3075
+ baseUrl: commandContext.apiBaseUrl,
3076
+ token: commandContext.token,
3077
+ workspaceId: workspace.id
3078
+ });
3079
+ if (context.json) {
3080
+ writeJson({
3081
+ workspace,
3082
+ teams
3083
+ });
3084
+ return;
3085
+ }
3086
+ const headers = [
3087
+ "id",
3088
+ "key",
3089
+ "name"
3090
+ ];
3091
+ const rows = teams.map((t) => [
3092
+ t.id,
3093
+ t.key,
3094
+ t.name
3095
+ ]);
3096
+ if (writeFormattedRows(context, teams.length, headers, rows)) return;
3097
+ if (teams.length === 0) {
3098
+ writeInfo(context, "No teams found.");
3099
+ return;
3100
+ }
3101
+ writeInfo(context, `Teams in ${workspace.name}:`);
3102
+ for (const team of teams) writeInfo(context, `${team.id.slice(0, 8)} [${team.key}] ${team.name}`);
3103
+ }
3104
+ async function runTeamShow(context, teamRef, workspaceOverride, options) {
3105
+ const commandContext = await resolveCommandContext(context);
3106
+ const { workspace } = await resolveWorkspace(context, {
3107
+ token: commandContext.token,
3108
+ apiBaseUrl: commandContext.apiBaseUrl,
3109
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
3110
+ storedWorkspaceId: commandContext.localContext.workspaceId
3111
+ });
3112
+ const teams = await loadTeamsByWorkspace(context, {
3113
+ baseUrl: commandContext.apiBaseUrl,
3114
+ token: commandContext.token,
3115
+ workspaceId: workspace.id
3116
+ });
3117
+ const team = findTeam(teamRef.trim(), teams);
3118
+ if (!team) throw new Error(`Team "${teamRef}" not found. Run \`donebear team list\`.`);
3119
+ if (context.json) {
3120
+ writeJson({
3121
+ workspace,
3122
+ team
3123
+ });
3124
+ return;
3125
+ }
3126
+ writeSuccess(context, team.name);
3127
+ writeInfo(context, `id: ${team.id}`);
3128
+ writeInfo(context, `key: ${team.key}`);
3129
+ writeInfo(context, `name: ${team.name}`);
3130
+ writeInfo(context, `description: ${team.description ?? "(none)"}`);
3131
+ writeInfo(context, `workspaceId: ${team.workspaceId ?? "(none)"}`);
3132
+ }
3133
+ function registerTeamCommands(program, workspaceOverride) {
3134
+ const team = program.command("team").alias("teams").description("Manage teams").addHelpText("after", `
3135
+ Examples:
3136
+ donebear team
3137
+ donebear team list
3138
+ donebear team show ENG
3139
+ donebear team list --format csv
3140
+ `).action(async (_options, command) => {
3141
+ const context = contextFromCommand(command);
3142
+ await runWithErrorHandling(context, async () => {
3143
+ await runTeamList(context, workspaceOverride, {});
3144
+ });
3145
+ });
3146
+ team.command("list").alias("ls").description("List teams in the current or specified workspace").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (options, command) => {
3147
+ const context = contextFromCommand(command);
3148
+ await runWithErrorHandling(context, async () => {
3149
+ await runTeamList(context, workspaceOverride, options);
3150
+ });
3151
+ });
3152
+ team.command("show").description("Show team details").argument("<id-or-key>", "Team id, key, or id prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (teamRef, options, command) => {
3153
+ const context = contextFromCommand(command);
3154
+ await runWithErrorHandling(context, async () => {
3155
+ await runTeamShow(context, teamRef, workspaceOverride, options);
3156
+ });
3157
+ });
3158
+ }
3159
+
3160
+ //#endregion
3161
+ //#region src/commands/today.ts
3162
+ const DEFAULT_TODAY_LIMIT = 20;
3163
+ async function runToday(context, options, workspaceOverride) {
3164
+ const commandContext = await resolveCommandContext(context);
3165
+ const { workspace, autoSelected } = await resolveWorkspace(context, {
3166
+ token: commandContext.token,
3167
+ apiBaseUrl: commandContext.apiBaseUrl,
3168
+ workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace),
3169
+ storedWorkspaceId: commandContext.localContext.workspaceId
3170
+ });
3171
+ const todayTasks = (await loadTasksByWorkspace(context, {
3172
+ baseUrl: commandContext.apiBaseUrl,
3173
+ token: commandContext.token,
3174
+ workspaceId: workspace.id
3175
+ })).filter((task) => task.todayIndexReferenceDate !== null && task.completedAt === null && task.archivedAt === null);
3176
+ const limited = todayTasks.slice(0, options.limit);
3177
+ if (context.json) {
3178
+ writeJson({
3179
+ workspace,
3180
+ autoSelected,
3181
+ total: todayTasks.length,
3182
+ count: limited.length,
3183
+ tasks: limited
3184
+ });
3185
+ return;
3186
+ }
3187
+ const headers = [
3188
+ "id",
3189
+ "title",
3190
+ "state",
3191
+ "deadline"
3192
+ ];
3193
+ const rows = limited.map((task) => [
3194
+ task.id,
3195
+ task.title,
3196
+ getTaskState(task),
3197
+ formatShortDate(task.deadlineAt) ?? ""
3198
+ ]);
3199
+ if (writeFormattedRows(context, todayTasks.length, headers, rows)) return;
3200
+ if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
3201
+ if (limited.length === 0) {
3202
+ writeInfo(context, "No tasks for today.");
3203
+ return;
3204
+ }
3205
+ const lines = [];
3206
+ lines.push(`${workspace.name} — Today (${limited.length}/${todayTasks.length})`);
3207
+ for (const task of limited) {
3208
+ const marker = toTaskMarker(task);
3209
+ const deadline = formatShortDate(task.deadlineAt);
3210
+ const suffix = deadline ? ` due ${deadline}` : "";
3211
+ lines.push(`${marker} ${task.id.slice(0, 8)} ${task.title}${suffix}`);
3212
+ }
3213
+ for (const line of lines) writeInfo(context, line);
3214
+ if (context.copy) copyToClipboard(lines.join("\n"));
3215
+ }
3216
+ function registerTodayCommand(program, workspaceOverride) {
3217
+ program.command("today").description("List today's tasks").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-n, --limit <count>", "Maximum number of tasks", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_TODAY_LIMIT).addHelpText("after", `
3218
+ Examples:
3219
+ donebear today
3220
+ donebear today --limit 10
3221
+ donebear today --format csv
3222
+ donebear today --total
3223
+ `).action(async (options, command) => {
3224
+ const context = contextFromCommand(command);
3225
+ await runWithErrorHandling(context, async () => {
3226
+ await runToday(context, options, workspaceOverride);
3227
+ });
3228
+ });
3229
+ }
3230
+
3231
+ //#endregion
3232
+ //#region src/commands/whoami.ts
3233
+ async function runWhoAmI(context) {
3234
+ const config = resolveSupabaseConfig();
3235
+ const resolvedToken = await resolveAuthToken(context, config);
3236
+ if (!resolvedToken) {
3237
+ process.exitCode = EXIT_CODES.AUTH_REQUIRED;
3238
+ if (context.json) {
3239
+ writeJson({
3240
+ ok: false,
3241
+ error: {
3242
+ message: "Not authenticated",
3243
+ exitCode: EXIT_CODES.AUTH_REQUIRED
3244
+ }
3245
+ });
3246
+ return;
3247
+ }
3248
+ writeInfo(context, "Not authenticated.");
3249
+ writeInfo(context, "Run `donebear auth login`.");
3250
+ return;
3251
+ }
3252
+ const user = await getSupabaseUser(config, resolvedToken.token);
3253
+ if (context.json) {
3254
+ writeJson({
3255
+ ok: true,
3256
+ user: {
3257
+ id: user.id,
3258
+ email: user.email ?? null
3259
+ },
3260
+ source: resolvedToken.source
3261
+ });
3262
+ return;
3263
+ }
3264
+ writeSuccess(context, "Authenticated user:");
3265
+ writeInfo(context, `id: ${user.id}`);
3266
+ writeInfo(context, `email: ${user.email ?? "(none)"}`);
3267
+ writeInfo(context, `source: ${resolvedToken.source}`);
3268
+ }
3269
+ function registerWhoAmICommand(program) {
3270
+ program.command("whoami").alias("me").description("Show the current authenticated user").action(async (_options, command) => {
3271
+ const context = contextFromCommand(command);
3272
+ await runWithErrorHandling(context, async () => {
3273
+ await runWhoAmI(context);
3274
+ });
3275
+ });
3276
+ }
3277
+
3278
+ //#endregion
3279
+ //#region src/commands/workspace.ts
3280
+ function findWorkspace(workspaceRef, workspaces) {
3281
+ const normalizedReference = workspaceRef.trim().toLowerCase();
3282
+ return workspaces.find((workspace) => {
3283
+ if (workspace.id === workspaceRef) return true;
3284
+ if (workspace.urlKey?.toLowerCase() === normalizedReference) return true;
3285
+ return workspace.name.toLowerCase() === normalizedReference;
3286
+ });
3287
+ }
3288
+ function writeCurrentWorkspaceNotAvailable(context, message) {
3289
+ if (context.json) {
3290
+ writeJson({ workspace: null });
3291
+ return;
3292
+ }
3293
+ writeInfo(context, message);
3294
+ writeInfo(context, "Run `donebear workspace use <id-or-slug>`.");
3295
+ }
3296
+ async function runWorkspaceList(context) {
3297
+ const commandContext = await resolveCommandContext(context);
3298
+ const workspaces = await listWorkspaces(context, {
3299
+ baseUrl: commandContext.apiBaseUrl,
3300
+ token: commandContext.token
3301
+ });
3302
+ if (context.json) {
3303
+ writeJson({
3304
+ workspaces,
3305
+ currentWorkspaceId: commandContext.localContext.workspaceId
3306
+ });
3307
+ return;
3308
+ }
3309
+ if (workspaces.length === 0) {
3310
+ writeInfo(context, "No workspaces found.");
3311
+ return;
3312
+ }
3313
+ writeInfo(context, "Workspaces:");
3314
+ for (const workspace of workspaces) writeInfo(context, `${workspace.id === commandContext.localContext.workspaceId ? "*" : " "} ${workspace.id} ${workspace.name}${workspace.urlKey ? ` (${workspace.urlKey})` : ""} role=${workspace.role}`);
3315
+ }
3316
+ async function runWorkspaceCurrent(context) {
3317
+ const commandContext = await resolveCommandContext(context);
3318
+ if (!commandContext.localContext.workspaceId) {
3319
+ writeCurrentWorkspaceNotAvailable(context, "No default workspace selected.");
3320
+ return;
3321
+ }
3322
+ const workspace = (await listWorkspaces(context, {
3323
+ baseUrl: commandContext.apiBaseUrl,
3324
+ token: commandContext.token
3325
+ })).find((item) => item.id === commandContext.localContext.workspaceId);
3326
+ if (!workspace) {
3327
+ writeCurrentWorkspaceNotAvailable(context, "Saved workspace is no longer accessible.");
3328
+ return;
3329
+ }
3330
+ if (context.json) {
3331
+ writeJson({ workspace });
3332
+ return;
3333
+ }
3334
+ writeSuccess(context, `Current workspace: ${workspace.name}`);
3335
+ writeInfo(context, `id: ${workspace.id}`);
3336
+ writeInfo(context, `slug: ${workspace.urlKey ?? "(none)"}`);
3337
+ writeInfo(context, `role: ${workspace.role}`);
3338
+ }
3339
+ async function runWorkspaceUse(context, reference) {
3340
+ const commandContext = await resolveCommandContext(context);
3341
+ const selected = findWorkspace(reference, await listWorkspaces(context, {
3342
+ baseUrl: commandContext.apiBaseUrl,
3343
+ token: commandContext.token
3344
+ }));
3345
+ if (!selected) throw new Error(`Workspace "${reference}" not found. Run \`donebear workspace list\`.`);
3346
+ await setCurrentWorkspace(selected.id);
3347
+ if (context.json) {
3348
+ writeJson({
3349
+ workspace: selected,
3350
+ selected: true
3351
+ });
3352
+ return;
3353
+ }
3354
+ writeSuccess(context, `Selected workspace: ${selected.name}`);
3355
+ writeInfo(context, `id: ${selected.id}`);
3356
+ }
3357
+ async function runWorkspaceClear(context) {
3358
+ await setCurrentWorkspace(null);
3359
+ if (context.json) {
3360
+ writeJson({
3361
+ cleared: true,
3362
+ workspace: null
3363
+ });
3364
+ return;
3365
+ }
3366
+ writeSuccess(context, "Cleared current workspace.");
3367
+ }
3368
+ async function runWorkspaceCreate(context, name, options) {
3369
+ const commandContext = await resolveCommandContext(context);
3370
+ const slug = (options.slug ?? toWorkspaceUrlKey(name)).trim();
3371
+ if (!isValidWorkspaceUrlKey(slug)) throw new Error("Workspace slug must be lowercase alphanumeric with hyphens and at least 3 characters");
3372
+ const workspace = await createWorkspace(context, {
3373
+ baseUrl: commandContext.apiBaseUrl,
3374
+ token: commandContext.token,
3375
+ body: {
3376
+ name,
3377
+ urlKey: slug,
3378
+ logoUrl: options.logoUrl
3379
+ }
3380
+ });
3381
+ if (options.use) await setCurrentWorkspace(workspace.id);
3382
+ if (context.json) {
3383
+ writeJson({
3384
+ workspace,
3385
+ selected: options.use
3386
+ });
3387
+ return;
3388
+ }
3389
+ writeSuccess(context, `Created workspace: ${workspace.name}`);
3390
+ writeInfo(context, `id: ${workspace.id}`);
3391
+ writeInfo(context, `slug: ${workspace.urlKey ?? slug}`);
3392
+ if (options.use) writeInfo(context, "Set as current workspace.");
3393
+ }
3394
+ async function runWorkspaceJoin(context, code, options) {
3395
+ const commandContext = await resolveCommandContext(context);
3396
+ const trimmedCode = code.trim();
3397
+ if (!trimmedCode) throw new Error("Invitation code is required.");
3398
+ const joined = await joinWorkspace(context, {
3399
+ baseUrl: commandContext.apiBaseUrl,
3400
+ token: commandContext.token,
3401
+ code: trimmedCode
3402
+ });
3403
+ if (options.use) await setCurrentWorkspace(joined.workspace.id);
3404
+ if (context.json) {
3405
+ writeJson({
3406
+ workspace: joined.workspace,
3407
+ role: joined.role,
3408
+ selected: options.use
3409
+ });
3410
+ return;
3411
+ }
3412
+ writeSuccess(context, `Joined workspace: ${joined.workspace.name}`);
3413
+ writeInfo(context, `id: ${joined.workspace.id}`);
3414
+ writeInfo(context, `role: ${joined.role}`);
3415
+ if (options.use) writeInfo(context, "Set as current workspace.");
3416
+ }
3417
+ async function runWorkspaceMembers(context, workspaceRefArg, workspaceOverride, options) {
3418
+ const commandContext = await resolveCommandContext(context);
3419
+ const { workspace } = await resolveWorkspace(context, {
3420
+ token: commandContext.token,
3421
+ apiBaseUrl: commandContext.apiBaseUrl,
3422
+ workspaceRef: workspaceOverride ?? workspaceRefArg ?? null,
3423
+ storedWorkspaceId: commandContext.localContext.workspaceId
3424
+ });
3425
+ const members = await listWorkspaceMembers(context, {
3426
+ baseUrl: commandContext.apiBaseUrl,
3427
+ token: commandContext.token,
3428
+ workspaceId: workspace.id
3429
+ });
3430
+ if (context.json) {
3431
+ writeJson({
3432
+ workspace,
3433
+ members
3434
+ });
3435
+ return;
3436
+ }
3437
+ if (context.total) {
3438
+ writeTotal(context, members.length);
3439
+ return;
3440
+ }
3441
+ const format = options.format?.trim().toLowerCase() ?? context.format;
3442
+ if (format === "csv") {
3443
+ writeCsv([
3444
+ "userId",
3445
+ "name",
3446
+ "email",
3447
+ "role"
3448
+ ], members.map((m) => [
3449
+ m.userId,
3450
+ m.user.name ?? "",
3451
+ m.user.email ?? "",
3452
+ m.role
3453
+ ]));
3454
+ return;
3455
+ }
3456
+ if (format === "tsv") {
3457
+ writeTsv([
3458
+ "userId",
3459
+ "name",
3460
+ "email",
3461
+ "role"
3462
+ ], members.map((m) => [
3463
+ m.userId,
3464
+ m.user.name ?? "",
3465
+ m.user.email ?? "",
3466
+ m.role
3467
+ ]));
3468
+ return;
3469
+ }
3470
+ if (members.length === 0) {
3471
+ writeInfo(context, "No members found.");
3472
+ return;
3473
+ }
3474
+ writeInfo(context, `Members of ${workspace.name}:`);
3475
+ for (const member of members) writeInfo(context, `${member.userId} ${member.user.name ?? "(no name)"} ${member.user.email ?? "(no email)"} ${member.role}`);
3476
+ }
3477
+ async function runWorkspaceInvitations(context, workspaceRefArg, workspaceOverride) {
3478
+ const commandContext = await resolveCommandContext(context);
3479
+ const { workspace } = await resolveWorkspace(context, {
3480
+ token: commandContext.token,
3481
+ apiBaseUrl: commandContext.apiBaseUrl,
3482
+ workspaceRef: workspaceOverride ?? workspaceRefArg ?? null,
3483
+ storedWorkspaceId: commandContext.localContext.workspaceId
3484
+ });
3485
+ const invitations = await listWorkspaceInvitations(context, {
3486
+ baseUrl: commandContext.apiBaseUrl,
3487
+ token: commandContext.token,
3488
+ workspaceId: workspace.id
3489
+ });
3490
+ if (context.json) {
3491
+ writeJson({
3492
+ workspace,
3493
+ invitations
3494
+ });
3495
+ return;
3496
+ }
3497
+ if (invitations.length === 0) {
3498
+ writeInfo(context, "No invitations found.");
3499
+ return;
3500
+ }
3501
+ writeInfo(context, `Invitations for ${workspace.name}:`);
3502
+ for (const invitation of invitations) {
3503
+ const used = invitation.usedAt ? "yes" : "no";
3504
+ writeInfo(context, `${invitation.id.slice(0, 8)} ${invitation.email ?? "(open)"} ${invitation.role} expires=${invitation.expiresAt} used=${used}`);
3505
+ }
3506
+ }
3507
+ async function runWorkspaceInvite(context, workspaceRefArg, workspaceOverride, options) {
3508
+ const commandContext = await resolveCommandContext(context);
3509
+ const { workspace } = await resolveWorkspace(context, {
3510
+ token: commandContext.token,
3511
+ apiBaseUrl: commandContext.apiBaseUrl,
3512
+ workspaceRef: workspaceOverride ?? workspaceRefArg ?? null,
3513
+ storedWorkspaceId: commandContext.localContext.workspaceId
3514
+ });
3515
+ const invitation = await createWorkspaceInvitation(context, {
3516
+ baseUrl: commandContext.apiBaseUrl,
3517
+ token: commandContext.token,
3518
+ workspaceId: workspace.id,
3519
+ email: options.email,
3520
+ role: options.role ?? "member"
3521
+ });
3522
+ if (context.json) {
3523
+ writeJson({
3524
+ workspace,
3525
+ invitation
3526
+ });
3527
+ return;
3528
+ }
3529
+ writeSuccess(context, `Invitation created for ${workspace.name}`);
3530
+ writeInfo(context, `code: ${invitation.code}`);
3531
+ writeInfo(context, `role: ${invitation.role}`);
3532
+ writeInfo(context, `expires: ${invitation.expiresAt}`);
3533
+ if (invitation.email) writeInfo(context, `email: ${invitation.email}`);
3534
+ const link = `${commandContext.apiBaseUrl}/invite/${invitation.code}`;
3535
+ writeInfo(context, `link: ${link}`);
3536
+ if (context.copy) {
3537
+ if (copyToClipboard(link)) writeInfo(context, "Link copied to clipboard.");
3538
+ }
3539
+ }
3540
+ function registerWorkspaceCommands(program, workspaceOverride) {
3541
+ const workspace = program.command("workspace").alias("ws").description("Manage workspaces").addHelpText("after", `
3542
+ Examples:
3543
+ donebear workspace
3544
+ donebear workspace use personal
3545
+ donebear workspace create "Personal" --slug personal
3546
+ donebear workspace join ABCD2345
3547
+ donebear workspace members
3548
+ donebear workspace invitations
3549
+ donebear workspace invite --email user@example.com --role member
3550
+ `).action(async (_options, command) => {
3551
+ const context = contextFromCommand(command);
3552
+ await runWithErrorHandling(context, async () => {
3553
+ await runWorkspaceList(context);
3554
+ });
3555
+ });
3556
+ workspace.command("list").alias("ls").description("List workspaces available to the current user").action(async (_options, command) => {
3557
+ const context = contextFromCommand(command);
3558
+ await runWithErrorHandling(context, async () => {
3559
+ await runWorkspaceList(context);
3560
+ });
3561
+ });
3562
+ workspace.command("current").description("Show the current default workspace").action(async (_options, command) => {
3563
+ const context = contextFromCommand(command);
3564
+ await runWithErrorHandling(context, async () => {
3565
+ await runWorkspaceCurrent(context);
3566
+ });
3567
+ });
3568
+ workspace.command("use").description("Set the default workspace").argument("<workspace>", "Workspace id, slug, or exact name").action(async (workspaceRef, _options, command) => {
3569
+ const context = contextFromCommand(command);
3570
+ await runWithErrorHandling(context, async () => {
3571
+ await runWorkspaceUse(context, workspaceRef);
3572
+ });
3573
+ });
3574
+ workspace.command("clear").description("Clear the default workspace").action(async (_options, command) => {
3575
+ const context = contextFromCommand(command);
3576
+ await runWithErrorHandling(context, async () => {
3577
+ await runWorkspaceClear(context);
3578
+ });
3579
+ });
3580
+ workspace.command("create").description("Create a workspace").argument("<name>", "Workspace display name").option("--slug <slug>", "Workspace urlKey slug").option("--logo-url <url>", "Workspace logo URL").option("--no-use", "Do not set as current workspace").action(async (name, options, command) => {
3581
+ const context = contextFromCommand(command);
3582
+ await runWithErrorHandling(context, async () => {
3583
+ await runWorkspaceCreate(context, name, options);
3584
+ });
3585
+ });
3586
+ workspace.command("join").description("Join a workspace with an invitation code").argument("<code>", "Invitation code").option("--no-use", "Do not set joined workspace as current").action(async (code, options, command) => {
3587
+ const context = contextFromCommand(command);
3588
+ await runWithErrorHandling(context, async () => {
3589
+ await runWorkspaceJoin(context, code, options);
3590
+ });
3591
+ });
3592
+ workspace.command("members").description("List members of the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").option("--format <format>", "Output format: text|json|csv|tsv").option("--total", "Print member count only").action(async (workspaceRef, options, command) => {
3593
+ const context = contextFromCommand(command);
3594
+ await runWithErrorHandling(context, async () => {
3595
+ await runWorkspaceMembers(context, workspaceRef, workspaceOverride, options);
3596
+ });
3597
+ });
3598
+ workspace.command("invitations").alias("invites").description("List pending invitations for the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").action(async (workspaceRef, _options, command) => {
3599
+ const context = contextFromCommand(command);
3600
+ await runWithErrorHandling(context, async () => {
3601
+ await runWorkspaceInvitations(context, workspaceRef, workspaceOverride);
3602
+ });
3603
+ });
3604
+ workspace.command("invite").description("Create an invitation for the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").option("--email <email>", "Email address to invite").option("--role <role>", "Role for the invitee: member|admin", "member").action(async (workspaceRef, options, command) => {
3605
+ const context = contextFromCommand(command);
3606
+ await runWithErrorHandling(context, async () => {
3607
+ await runWorkspaceInvite(context, workspaceRef, workspaceOverride, options);
3608
+ });
3609
+ });
3610
+ }
3611
+
3612
+ //#endregion
3613
+ //#region src/cli.ts
3614
+ const packageManifest = createRequire(import.meta.url)("../package.json");
3615
+ const WORKSPACE_PREFIX_REGEX = /^workspace=(.+)$/i;
3616
+ function extractWorkspaceTarget(argv) {
3617
+ const processedArgv = [...argv];
3618
+ const firstArg = processedArgv[2];
3619
+ if (firstArg) {
3620
+ const match = WORKSPACE_PREFIX_REGEX.exec(firstArg);
3621
+ if (match?.[1]) {
3622
+ processedArgv.splice(2, 1);
3623
+ return {
3624
+ processedArgv,
3625
+ workspaceOverride: match[1].trim()
3626
+ };
3627
+ }
3628
+ }
3629
+ return {
3630
+ processedArgv,
3631
+ workspaceOverride: null
3632
+ };
3633
+ }
3634
+ async function main() {
3635
+ loadCliEnvironmentFiles();
3636
+ const { processedArgv, workspaceOverride } = extractWorkspaceTarget(process.argv);
3637
+ const program = new Command();
3638
+ program.name(CLI_NAME).description("Done Bear CLI for auth, workspace, and task workflows").version(packageManifest.version).showHelpAfterError().showSuggestionAfterError().option("--json", "Output machine-readable JSON").option("--format <format>", "Output format: text|json|csv|tsv", "text").option("--copy", "Copy output to clipboard").option("--total", "Print count only").option("--token <token>", "Use an explicit token (overrides DONEBEAR_TOKEN)").option("--api-url <url>", "Manage API base URL (overrides DONEBEAR_API_URL)").option("--debug", "Enable debug logs", process.env[DONEBEAR_DEBUG_ENV] === "1").option("--no-color", "Disable color output").addHelpText("after", `
3639
+ Quick start:
3640
+ donebear auth login
3641
+ donebear workspace
3642
+ donebear task add "Plan week" --when today
3643
+ donebear task
3644
+ donebear today
3645
+ donebear search "meeting"
3646
+ donebear project
3647
+ donebear label
3648
+
3649
+ Workspace targeting:
3650
+ donebear workspace=myorg task list
3651
+
3652
+ Use --json for scripting, --format csv|tsv for export, --total for counts.
3653
+ `);
3654
+ registerAuthCommands(program);
3655
+ registerWorkspaceCommands(program, workspaceOverride);
3656
+ registerTaskCommands(program, workspaceOverride);
3657
+ registerProjectCommands(program, workspaceOverride);
3658
+ registerLabelCommands(program, workspaceOverride);
3659
+ registerTeamCommands(program, workspaceOverride);
3660
+ registerTodayCommand(program, workspaceOverride);
3661
+ registerSearchCommand(program, workspaceOverride);
3662
+ registerHistoryCommand(program, workspaceOverride);
3663
+ registerWhoAmICommand(program);
3664
+ const hasUserArgs = processedArgv.length <= 2;
3665
+ const isTTY = process.stdin.isTTY === true;
3666
+ if (hasUserArgs && isTTY) {
3667
+ await runInteractiveMode(contextFromArgv(processedArgv), program);
3668
+ return;
3669
+ }
3670
+ await program.parseAsync(processedArgv);
3671
+ }
3672
+ await runWithErrorHandling(contextFromArgv(process.argv), main);
3673
+
3674
+ //#endregion
3675
+ export { };
3676
+ //# sourceMappingURL=cli.mjs.map