donebear 0.3.8 → 0.3.9

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 CHANGED
@@ -1,25 +1,256 @@
1
1
  #!/usr/bin/env node
2
- import { a as toCliError, b as DONEBEAR_TOKEN_ENV, d as DEFAULT_OAUTH_CALLBACK_PATH, f as DEFAULT_OAUTH_CALLBACK_PORT, i as EXIT_CODES, l as CONFIG_DIR, n as setCurrentWorkspace, o as AUTH_FILE_PATH, r as CliError, s as CLI_NAME, t as ensureLocalContext } from "./context-store-DuCNavgl.mjs";
2
+ import { a as toCliError, b as DONEBEAR_TOKEN_ENV, d as DEFAULT_OAUTH_CALLBACK_PATH, f as DEFAULT_OAUTH_CALLBACK_PORT, i as EXIT_CODES, l as CONFIG_DIR, n as setCurrentWorkspace, o as AUTH_FILE_PATH, r as CliError, s as CLI_NAME, t as ensureLocalContext } from "./context-store-BBEVWudm.mjs";
3
3
  import { n as readStdin, t as promptForValue } from "./prompt-GfcWO85b.mjs";
4
4
  import { createRequire } from "node:module";
5
- import { Command, InvalidArgumentError } from "commander";
6
- import { homedir } from "node:os";
7
- import { basename, join, resolve } from "node:path";
8
5
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
9
- import { accessSync, constants, existsSync, readdirSync, statSync } from "node:fs";
10
- import { config } from "dotenv";
11
- import { createHash, randomBytes, randomUUID } from "node:crypto";
6
+ import { basename, join, resolve } from "node:path";
7
+ import { createInterface } from "node:readline";
8
+ import { homedir } from "node:os";
12
9
  import { execSync, spawn } from "node:child_process";
13
10
  import { styleText } from "node:util";
14
11
  import jmespath from "jmespath";
15
12
  import { stringify } from "yaml";
13
+ import { accessSync, constants, existsSync, readdirSync, statSync } from "node:fs";
14
+ import { config } from "dotenv";
15
+ import { Command, InvalidArgumentError } from "commander";
16
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
16
17
  import { createServer } from "node:http";
17
18
  import { fileURLToPath } from "node:url";
18
19
  import Database from "better-sqlite3";
19
- import { createInterface } from "node:readline";
20
20
  //#region \0rolldown/runtime.js
21
21
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
22
22
  //#endregion
23
+ //#region src/output.ts
24
+ const writeLine = (stream, message) => {
25
+ stream.write(`${message}\n`);
26
+ };
27
+ const colorize = (context, format, message) => {
28
+ if (!context.color) return message;
29
+ return styleText(format, message);
30
+ };
31
+ const writeJson = (value) => {
32
+ writeLine(process.stdout, JSON.stringify(value, null, 2));
33
+ };
34
+ const writeYaml = (value) => {
35
+ process.stdout.write(stringify(value));
36
+ };
37
+ const writeInfo = (context, message) => {
38
+ writeLine(process.stdout, colorize(context, "cyan", message));
39
+ };
40
+ const writeSuccess = (context, message) => {
41
+ writeLine(process.stdout, colorize(context, "green", message));
42
+ };
43
+ const writeWarning = (context, message) => {
44
+ writeLine(process.stderr, colorize(context, "yellow", message));
45
+ };
46
+ const writeError = (context, message) => {
47
+ writeLine(process.stderr, colorize(context, "red", message));
48
+ };
49
+ const writeDebug = (context, message) => {
50
+ if (!context.debug) return;
51
+ writeLine(process.stderr, colorize(context, "gray", `[debug] ${message}`));
52
+ };
53
+ const writeTotal = (context, count) => {
54
+ writeLine(process.stdout, colorize(context, "cyan", String(count)));
55
+ };
56
+ const escapeCsvField = (value) => {
57
+ if (value.includes(",") || value.includes("\"") || value.includes("\n")) return `"${value.replaceAll("\"", "\"\"")}"`;
58
+ return value;
59
+ };
60
+ const writeCsv = (headers, rows) => {
61
+ writeLine(process.stdout, headers.map(escapeCsvField).join(","));
62
+ for (const row of rows) writeLine(process.stdout, row.map(escapeCsvField).join(","));
63
+ };
64
+ const writeTsv = (headers, rows) => {
65
+ writeLine(process.stdout, headers.join(" "));
66
+ for (const row of rows) writeLine(process.stdout, row.join(" "));
67
+ };
68
+ /**
69
+ * Write structured data to stdout, applying --jq filter and --limit if set.
70
+ * Use this for all JSON/YAML/structured output from commands.
71
+ */
72
+ const writeOutput = (context, data) => {
73
+ let result = data;
74
+ if (context.jq) try {
75
+ result = jmespath.search(result, context.jq);
76
+ } catch (error) {
77
+ throw new CliError(`Invalid --jq expression: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODES.VALIDATION);
78
+ }
79
+ if (context.limit !== null && Array.isArray(result)) result = result.slice(0, context.limit);
80
+ switch (context.format) {
81
+ case "yaml":
82
+ writeYaml(result);
83
+ break;
84
+ case "json":
85
+ writeJson(result);
86
+ break;
87
+ default:
88
+ writeJson(result);
89
+ break;
90
+ }
91
+ if (context.copy) copyToClipboard(JSON.stringify(result, null, 2));
92
+ };
93
+ /**
94
+ * Handles --total, --format csv, --format tsv, --format yaml output.
95
+ * Returns true if the format was handled; caller renders text mode if false.
96
+ */
97
+ const writeFormattedRows = (context, totalCount, headers, rows) => {
98
+ if (context.total) {
99
+ writeTotal(context, totalCount);
100
+ return true;
101
+ }
102
+ if (context.format === "yaml") {
103
+ const objects = rows.map((row) => Object.fromEntries(headers.map((h, i) => [h, row[i]])));
104
+ writeYaml(objects);
105
+ if (context.copy) copyToClipboard(stringify(objects));
106
+ return true;
107
+ }
108
+ if (context.format === "csv") {
109
+ writeCsv(headers, rows);
110
+ if (context.copy) copyToClipboard([headers.join(","), ...rows.map((r) => r.join(","))].join("\n"));
111
+ return true;
112
+ }
113
+ if (context.format === "tsv") {
114
+ writeTsv(headers, rows);
115
+ if (context.copy) copyToClipboard([headers.join(" "), ...rows.map((r) => r.join(" "))].join("\n"));
116
+ return true;
117
+ }
118
+ return false;
119
+ };
120
+ const copyToClipboard = (text) => {
121
+ try {
122
+ if (process.platform === "darwin") execSync("pbcopy", { input: text });
123
+ else if (process.platform === "win32") execSync("clip", { input: text });
124
+ else try {
125
+ execSync("xclip -selection clipboard", { input: text });
126
+ } catch {
127
+ execSync("xsel --clipboard --input", { input: text });
128
+ }
129
+ return true;
130
+ } catch {
131
+ return false;
132
+ }
133
+ };
134
+ //#endregion
135
+ //#region src/commands/interactive.ts
136
+ const HISTORY_FILE = join(CONFIG_DIR, "history.json");
137
+ const MAX_HISTORY = 200;
138
+ const isHistoryFile = (value) => {
139
+ if (typeof value !== "object" || value === null) return false;
140
+ const record = value;
141
+ return record.version === 1 && Array.isArray(record.entries) && record.entries.every((e) => typeof e === "string");
142
+ };
143
+ const loadHistory = async (historyFile) => {
144
+ try {
145
+ const raw = await readFile(historyFile, "utf-8");
146
+ const parsed = JSON.parse(raw);
147
+ if (isHistoryFile(parsed)) return parsed.entries;
148
+ return [];
149
+ } catch {
150
+ return [];
151
+ }
152
+ };
153
+ const tokenize = (input) => {
154
+ const tokens = [];
155
+ let current = "";
156
+ let inQuote = false;
157
+ let quoteChar = "";
158
+ for (const char of input) if (inQuote) if (char === quoteChar) inQuote = false;
159
+ else current += char;
160
+ else if (char === "\"" || char === "'") {
161
+ inQuote = true;
162
+ quoteChar = char;
163
+ } else if (char === " ") {
164
+ if (current.length > 0) {
165
+ tokens.push(current);
166
+ current = "";
167
+ }
168
+ } else current += char;
169
+ if (current.length > 0) tokens.push(current);
170
+ return tokens;
171
+ };
172
+ const buildCompleter = (program) => (line) => {
173
+ const allCommands = program.commands.flatMap((cmd) => [cmd.name(), ...cmd.aliases()]);
174
+ const hits = allCommands.filter((c) => c.startsWith(line));
175
+ return [hits.length > 0 ? hits : allCommands, line];
176
+ };
177
+ const runInteractiveMode = async (context, program) => {
178
+ const history = await loadHistory(HISTORY_FILE);
179
+ const rl = createInterface({
180
+ completer: buildCompleter(program),
181
+ historySize: MAX_HISTORY,
182
+ input: process.stdin,
183
+ output: process.stdout,
184
+ prompt: "donebear> "
185
+ });
186
+ for (const entry of [...history].toReversed()) rl.history.push(entry);
187
+ writeSuccess(context, "Done Bear interactive mode. Type \"exit\" or Ctrl+D to quit.");
188
+ writeInfo(context, "Commands: task, workspace, project, label, team, today, search, auth, whoami");
189
+ rl.prompt();
190
+ const sessionEntries = [];
191
+ for await (const line of rl) {
192
+ const trimmed = line.trim();
193
+ if (!trimmed) {
194
+ rl.prompt();
195
+ continue;
196
+ }
197
+ if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") break;
198
+ const tokens = tokenize(trimmed);
199
+ try {
200
+ await program.parseAsync([
201
+ "node",
202
+ "donebear",
203
+ ...tokens
204
+ ]);
205
+ } catch {}
206
+ sessionEntries.push(trimmed);
207
+ rl.prompt();
208
+ }
209
+ rl.close();
210
+ if (sessionEntries.length > 0) try {
211
+ const payload = {
212
+ entries: [...await loadHistory(HISTORY_FILE), ...sessionEntries].slice(-MAX_HISTORY),
213
+ version: 1
214
+ };
215
+ await mkdir(CONFIG_DIR, {
216
+ mode: 448,
217
+ recursive: true
218
+ });
219
+ await writeFile(HISTORY_FILE, `${JSON.stringify(payload, null, 2)}\n`, {
220
+ encoding: "utf-8",
221
+ mode: 384
222
+ });
223
+ } catch {}
224
+ };
225
+ //#endregion
226
+ //#region src/env.ts
227
+ const DEFAULT_ENV_FILES = [
228
+ ".env.local",
229
+ ".env",
230
+ "apps/manage-frontend/.env.local",
231
+ "apps/manage-api/.env"
232
+ ];
233
+ let loaded = false;
234
+ const loadCliEnvironmentFiles = () => {
235
+ if (loaded) return;
236
+ for (const relativePath of DEFAULT_ENV_FILES) {
237
+ const absolutePath = resolve(process.cwd(), relativePath);
238
+ if (!existsSync(absolutePath)) continue;
239
+ config({
240
+ override: false,
241
+ path: absolutePath,
242
+ quiet: true
243
+ });
244
+ }
245
+ loaded = true;
246
+ };
247
+ const readEnvValue = (key) => {
248
+ const value = process.env[key];
249
+ if (typeof value !== "string") return;
250
+ const trimmed = value.trim();
251
+ return trimmed.length > 0 ? trimmed : void 0;
252
+ };
253
+ //#endregion
23
254
  //#region src/jwt.ts
24
255
  const parseJwtBase64Url = (input) => {
25
256
  const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
@@ -140,34 +371,6 @@ const clearStoredAuthSession = async () => {
140
371
  await rm(AUTH_FILE_PATH, { force: true });
141
372
  };
142
373
  //#endregion
143
- //#region src/env.ts
144
- const DEFAULT_ENV_FILES = [
145
- ".env.local",
146
- ".env",
147
- "apps/manage-frontend/.env.local",
148
- "apps/manage-api/.env"
149
- ];
150
- let loaded = false;
151
- const loadCliEnvironmentFiles = () => {
152
- if (loaded) return;
153
- for (const relativePath of DEFAULT_ENV_FILES) {
154
- const absolutePath = resolve(process.cwd(), relativePath);
155
- if (!existsSync(absolutePath)) continue;
156
- config({
157
- override: false,
158
- path: absolutePath,
159
- quiet: true
160
- });
161
- }
162
- loaded = true;
163
- };
164
- const readEnvValue = (key) => {
165
- const value = process.env[key];
166
- if (typeof value !== "string") return;
167
- const trimmed = value.trim();
168
- return trimmed.length > 0 ? trimmed : void 0;
169
- };
170
- //#endregion
171
374
  //#region src/supabase.ts
172
375
  const resolveSupabaseConfig = () => {
173
376
  const url = readEnvValue("DONEBEAR_SUPABASE_URL") ?? "https://kdarvputxmlxqemisnbh.supabase.co";
@@ -385,136 +588,24 @@ const parseDateOnlyString = (value) => {
385
588
  };
386
589
  return isValidDateOnlyParts(parts) ? parts : null;
387
590
  };
388
- const parseApiDateOnlyToEpoch = (value) => {
389
- const parts = parseDateOnlyString(value);
390
- return parts ? dateOnlyPartsToEpoch(parts) : null;
391
- };
392
- const parseApiInstantToEpoch = (value) => {
393
- if (typeof value !== "string") return null;
394
- const trimmed = value.trim();
395
- if (trimmed.length === 0 || !ISO_INSTANT_REGEX.test(trimmed)) return null;
396
- const parsed = Date.parse(trimmed);
397
- return Number.isNaN(parsed) ? null : parsed;
398
- };
399
- const parseCliDateOnlyToEpoch = (value, fieldLabel) => {
400
- if (!value) return null;
401
- const parts = parseDateOnlyString(value);
402
- if (!parts) throw new Error(`Invalid ${fieldLabel}: ${value}. Expected YYYY-MM-DD.`);
403
- return dateOnlyPartsToEpoch(parts);
404
- };
405
- const dateOnlyEpochFromLocalDate = (now = /* @__PURE__ */ new Date()) => Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
406
- //#endregion
407
- //#region src/output.ts
408
- const writeLine = (stream, message) => {
409
- stream.write(`${message}\n`);
410
- };
411
- const colorize = (context, format, message) => {
412
- if (!context.color) return message;
413
- return styleText(format, message);
414
- };
415
- const writeJson = (value) => {
416
- writeLine(process.stdout, JSON.stringify(value, null, 2));
417
- };
418
- const writeYaml = (value) => {
419
- process.stdout.write(stringify(value));
420
- };
421
- const writeInfo = (context, message) => {
422
- writeLine(process.stdout, colorize(context, "cyan", message));
423
- };
424
- const writeSuccess = (context, message) => {
425
- writeLine(process.stdout, colorize(context, "green", message));
426
- };
427
- const writeWarning = (context, message) => {
428
- writeLine(process.stderr, colorize(context, "yellow", message));
429
- };
430
- const writeError = (context, message) => {
431
- writeLine(process.stderr, colorize(context, "red", message));
432
- };
433
- const writeDebug = (context, message) => {
434
- if (!context.debug) return;
435
- writeLine(process.stderr, colorize(context, "gray", `[debug] ${message}`));
436
- };
437
- const writeTotal = (context, count) => {
438
- writeLine(process.stdout, colorize(context, "cyan", String(count)));
439
- };
440
- const escapeCsvField = (value) => {
441
- if (value.includes(",") || value.includes("\"") || value.includes("\n")) return `"${value.replaceAll("\"", "\"\"")}"`;
442
- return value;
443
- };
444
- const writeCsv = (headers, rows) => {
445
- writeLine(process.stdout, headers.map(escapeCsvField).join(","));
446
- for (const row of rows) writeLine(process.stdout, row.map(escapeCsvField).join(","));
447
- };
448
- const writeTsv = (headers, rows) => {
449
- writeLine(process.stdout, headers.join(" "));
450
- for (const row of rows) writeLine(process.stdout, row.join(" "));
451
- };
452
- /**
453
- * Write structured data to stdout, applying --jq filter and --limit if set.
454
- * Use this for all JSON/YAML/structured output from commands.
455
- */
456
- const writeOutput = (context, data) => {
457
- let result = data;
458
- if (context.jq) try {
459
- result = jmespath.search(result, context.jq);
460
- } catch (error) {
461
- throw new CliError(`Invalid --jq expression: ${error instanceof Error ? error.message : String(error)}`, EXIT_CODES.VALIDATION);
462
- }
463
- if (context.limit !== null && Array.isArray(result)) result = result.slice(0, context.limit);
464
- switch (context.format) {
465
- case "yaml":
466
- writeYaml(result);
467
- break;
468
- case "json":
469
- writeJson(result);
470
- break;
471
- default:
472
- writeJson(result);
473
- break;
474
- }
475
- if (context.copy) copyToClipboard(JSON.stringify(result, null, 2));
476
- };
477
- /**
478
- * Handles --total, --format csv, --format tsv, --format yaml output.
479
- * Returns true if the format was handled; caller renders text mode if false.
480
- */
481
- const writeFormattedRows = (context, totalCount, headers, rows) => {
482
- if (context.total) {
483
- writeTotal(context, totalCount);
484
- return true;
485
- }
486
- if (context.format === "yaml") {
487
- const objects = rows.map((row) => Object.fromEntries(headers.map((h, i) => [h, row[i]])));
488
- writeYaml(objects);
489
- if (context.copy) copyToClipboard(stringify(objects));
490
- return true;
491
- }
492
- if (context.format === "csv") {
493
- writeCsv(headers, rows);
494
- if (context.copy) copyToClipboard([headers.join(","), ...rows.map((r) => r.join(","))].join("\n"));
495
- return true;
496
- }
497
- if (context.format === "tsv") {
498
- writeTsv(headers, rows);
499
- if (context.copy) copyToClipboard([headers.join(" "), ...rows.map((r) => r.join(" "))].join("\n"));
500
- return true;
501
- }
502
- return false;
591
+ const parseApiDateOnlyToEpoch = (value) => {
592
+ const parts = parseDateOnlyString(value);
593
+ return parts ? dateOnlyPartsToEpoch(parts) : null;
503
594
  };
504
- const copyToClipboard = (text) => {
505
- try {
506
- if (process.platform === "darwin") execSync("pbcopy", { input: text });
507
- else if (process.platform === "win32") execSync("clip", { input: text });
508
- else try {
509
- execSync("xclip -selection clipboard", { input: text });
510
- } catch {
511
- execSync("xsel --clipboard --input", { input: text });
512
- }
513
- return true;
514
- } catch {
515
- return false;
516
- }
595
+ const parseApiInstantToEpoch = (value) => {
596
+ if (typeof value !== "string") return null;
597
+ const trimmed = value.trim();
598
+ if (trimmed.length === 0 || !ISO_INSTANT_REGEX.test(trimmed)) return null;
599
+ const parsed = Date.parse(trimmed);
600
+ return Number.isNaN(parsed) ? null : parsed;
517
601
  };
602
+ const parseCliDateOnlyToEpoch = (value, fieldLabel) => {
603
+ if (!value) return null;
604
+ const parts = parseDateOnlyString(value);
605
+ if (!parts) throw new Error(`Invalid ${fieldLabel}: ${value}. Expected YYYY-MM-DD.`);
606
+ return dateOnlyPartsToEpoch(parts);
607
+ };
608
+ const dateOnlyEpochFromLocalDate = (now = /* @__PURE__ */ new Date()) => Date.UTC(now.getFullYear(), now.getMonth(), now.getDate());
518
609
  //#endregion
519
610
  //#region src/manage-api.ts
520
611
  const TASK_FIELDS = `
@@ -1999,11 +2090,11 @@ Examples:
1999
2090
  });
2000
2091
  });
2001
2092
  };
2093
+ const packageManifest = createRequire(import.meta.url)("../package.json");
2002
2094
  //#endregion
2003
2095
  //#region src/commands/doctor.ts
2004
- const packageManifest$1 = createRequire(import.meta.url)("../package.json");
2005
2096
  const checkCliVersion = async () => {
2006
- const current = packageManifest$1.version;
2097
+ const current = packageManifest.version;
2007
2098
  try {
2008
2099
  const response = await fetch("https://registry.npmjs.org/donebear/latest", { signal: AbortSignal.timeout(5e3) });
2009
2100
  if (!response.ok) return {
@@ -2068,7 +2159,7 @@ const checkAuth = async (context) => {
2068
2159
  };
2069
2160
  const checkWorkspace = async () => {
2070
2161
  try {
2071
- const { ensureLocalContext } = await import("./context-store-CK4yIl9e.mjs");
2162
+ const { ensureLocalContext } = await import("./context-store-ClAx1T2N.mjs");
2072
2163
  const localContext = await ensureLocalContext();
2073
2164
  if (!localContext.workspaceId) return {
2074
2165
  message: "No workspace selected (run `donebear workspace use`)",
@@ -2283,6 +2374,7 @@ const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase(
2283
2374
  //#region src/import/model-id.ts
2284
2375
  const IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
2285
2376
  const SYNC_COMPOSITE_ID_NAMESPACE = "92a73695-d772-4b43-9fb4-d79f5fbef300";
2377
+ const IDEMPOTENCY_ID_NAMESPACE = "4c8af5e2-1d36-5a0b-9c47-2f81b3d6e955";
2286
2378
  const uuidToBytes = (uuid) => {
2287
2379
  const normalized = uuid.replaceAll("-", "");
2288
2380
  if (normalized.length !== 32) throw new Error(`Invalid UUID namespace: ${uuid}`);
@@ -2311,6 +2403,7 @@ const createUuidV5 = (name, namespace) => {
2311
2403
  };
2312
2404
  const createCompositeSyncId = (model, parts) => createUuidV5(`${model}:${JSON.stringify(parts)}`, SYNC_COMPOSITE_ID_NAMESPACE);
2313
2405
  const createDeterministicImportId = (params) => createUuidV5(`things:${params.workspaceId}:${params.modelName}:${params.sourceId}`, IMPORT_ID_NAMESPACE);
2406
+ const createIdempotentMutationId = (params) => createUuidV5(`idem:${params.workspaceId}:${params.modelName}:${params.part}:${params.key}`, IDEMPOTENCY_ID_NAMESPACE);
2314
2407
  const createTaskLabelId = (params) => createCompositeSyncId("TaskLabel", [
2315
2408
  params.workspaceId,
2316
2409
  params.taskId,
@@ -6527,97 +6620,6 @@ Notes:
6527
6620
  });
6528
6621
  };
6529
6622
  //#endregion
6530
- //#region src/commands/interactive.ts
6531
- const HISTORY_FILE = join(CONFIG_DIR, "history.json");
6532
- const MAX_HISTORY = 200;
6533
- const isHistoryFile = (value) => {
6534
- if (typeof value !== "object" || value === null) return false;
6535
- const record = value;
6536
- return record.version === 1 && Array.isArray(record.entries) && record.entries.every((e) => typeof e === "string");
6537
- };
6538
- const loadHistory = async (historyFile) => {
6539
- try {
6540
- const raw = await readFile(historyFile, "utf-8");
6541
- const parsed = JSON.parse(raw);
6542
- if (isHistoryFile(parsed)) return parsed.entries;
6543
- return [];
6544
- } catch {
6545
- return [];
6546
- }
6547
- };
6548
- const tokenize = (input) => {
6549
- const tokens = [];
6550
- let current = "";
6551
- let inQuote = false;
6552
- let quoteChar = "";
6553
- for (const char of input) if (inQuote) if (char === quoteChar) inQuote = false;
6554
- else current += char;
6555
- else if (char === "\"" || char === "'") {
6556
- inQuote = true;
6557
- quoteChar = char;
6558
- } else if (char === " ") {
6559
- if (current.length > 0) {
6560
- tokens.push(current);
6561
- current = "";
6562
- }
6563
- } else current += char;
6564
- if (current.length > 0) tokens.push(current);
6565
- return tokens;
6566
- };
6567
- const buildCompleter = (program) => (line) => {
6568
- const allCommands = program.commands.flatMap((cmd) => [cmd.name(), ...cmd.aliases()]);
6569
- const hits = allCommands.filter((c) => c.startsWith(line));
6570
- return [hits.length > 0 ? hits : allCommands, line];
6571
- };
6572
- const runInteractiveMode = async (context, program) => {
6573
- const history = await loadHistory(HISTORY_FILE);
6574
- const rl = createInterface({
6575
- completer: buildCompleter(program),
6576
- historySize: MAX_HISTORY,
6577
- input: process.stdin,
6578
- output: process.stdout,
6579
- prompt: "donebear> "
6580
- });
6581
- for (const entry of [...history].toReversed()) rl.history.push(entry);
6582
- writeSuccess(context, "Done Bear interactive mode. Type \"exit\" or Ctrl+D to quit.");
6583
- writeInfo(context, "Commands: task, workspace, project, label, team, today, search, auth, whoami");
6584
- rl.prompt();
6585
- const sessionEntries = [];
6586
- for await (const line of rl) {
6587
- const trimmed = line.trim();
6588
- if (!trimmed) {
6589
- rl.prompt();
6590
- continue;
6591
- }
6592
- if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") break;
6593
- const tokens = tokenize(trimmed);
6594
- try {
6595
- await program.parseAsync([
6596
- "node",
6597
- "donebear",
6598
- ...tokens
6599
- ]);
6600
- } catch {}
6601
- sessionEntries.push(trimmed);
6602
- rl.prompt();
6603
- }
6604
- rl.close();
6605
- if (sessionEntries.length > 0) try {
6606
- const payload = {
6607
- entries: [...await loadHistory(HISTORY_FILE), ...sessionEntries].slice(-MAX_HISTORY),
6608
- version: 1
6609
- };
6610
- await mkdir(CONFIG_DIR, {
6611
- mode: 448,
6612
- recursive: true
6613
- });
6614
- await writeFile(HISTORY_FILE, `${JSON.stringify(payload, null, 2)}\n`, {
6615
- encoding: "utf-8",
6616
- mode: 384
6617
- });
6618
- } catch {}
6619
- };
6620
- //#endregion
6621
6623
  //#region src/commands/label.ts
6622
6624
  const sortLabels = (labels, sort) => {
6623
6625
  const sorted = [...labels];
@@ -7091,17 +7093,23 @@ const runProjectAdd = async (context, name, options) => {
7091
7093
  token: commandContext.token,
7092
7094
  workspaceRef: normalizeWorkspaceRef(options.workspace)
7093
7095
  });
7094
- const projectId = randomUUID();
7096
+ const idempotencyId = (part) => options.idempotencyKey ? createIdempotentMutationId({
7097
+ key: options.idempotencyKey,
7098
+ modelName: "Project",
7099
+ part,
7100
+ workspaceId: workspace.id
7101
+ }) : null;
7102
+ const projectId = idempotencyId("model") ?? randomUUID();
7095
7103
  const now = Date.now();
7096
7104
  const key = generateProjectSlug(normalizedName, projectId);
7097
7105
  await mutateModel(context, {
7098
7106
  baseUrl: commandContext.apiBaseUrl,
7099
7107
  request: {
7100
- batchId: randomUUID(),
7108
+ batchId: idempotencyId("batch") ?? randomUUID(),
7101
7109
  transactions: [{
7102
7110
  action: "INSERT",
7103
- clientId: commandContext.localContext.clientId,
7104
- clientTxId: randomUUID(),
7111
+ clientId: options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId,
7112
+ clientTxId: idempotencyId("tx") ?? randomUUID(),
7105
7113
  modelId: projectId,
7106
7114
  modelName: "Project",
7107
7115
  payload: {
@@ -7268,7 +7276,7 @@ Agent tip: run "donebear spec project add" for full option reference
7268
7276
  await runProjectShow(context, projectRef, { workspace: workspaceOverride ?? normalizeWorkspaceRef(options.workspace) ?? void 0 });
7269
7277
  });
7270
7278
  });
7271
- project.command("add").alias("create").description("Create a project").argument("[name]", "Project name (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-d, --description <text>", "Project description").option("--target-date <date>", "Target date (YYYY-MM-DD)").action(async (name, options, command) => {
7279
+ project.command("add").alias("create").description("Create a project").argument("[name]", "Project name (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-d, --description <text>", "Project description").option("--target-date <date>", "Target date (YYYY-MM-DD)").option("--idempotency-key <key>", "Stable key making retries safe (repeated calls create only one project)").action(async (name, options, command) => {
7272
7280
  const context = contextFromCommand(command);
7273
7281
  await runWithErrorHandling(context, async () => {
7274
7282
  await runProjectAdd(context, name, {
@@ -7509,7 +7517,13 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7509
7517
  token: commandContext.token,
7510
7518
  workspaceId: workspace.id
7511
7519
  }).then((teams) => resolveEntityId(teams, options.team, "Team")) : Promise.resolve(null)]);
7512
- const taskId = randomUUID();
7520
+ const idempotencyId = (part) => options.idempotencyKey ? createIdempotentMutationId({
7521
+ key: options.idempotencyKey,
7522
+ modelName: "Task",
7523
+ part,
7524
+ workspaceId: workspace.id
7525
+ }) : null;
7526
+ const taskId = idempotencyId("model") ?? randomUUID();
7513
7527
  const now = Date.now();
7514
7528
  const view = parseTaskView(options.when);
7515
7529
  const startFields = buildTaskStartFields(view);
@@ -7517,11 +7531,11 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
7517
7531
  await mutateModel(context, {
7518
7532
  baseUrl: commandContext.apiBaseUrl,
7519
7533
  request: {
7520
- batchId: randomUUID(),
7534
+ batchId: idempotencyId("batch") ?? randomUUID(),
7521
7535
  transactions: [{
7522
7536
  action: "INSERT",
7523
- clientId: commandContext.localContext.clientId,
7524
- clientTxId: randomUUID(),
7537
+ clientId: options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId,
7538
+ clientTxId: idempotencyId("tx") ?? randomUUID(),
7525
7539
  modelId: taskId,
7526
7540
  modelName: "Task",
7527
7541
  payload: {
@@ -7883,15 +7897,15 @@ Views: inbox | anytime | today | upcoming | someday | logbook | trash
7883
7897
  await runTaskRandom(context, options, workspaceOverride);
7884
7898
  });
7885
7899
  });
7886
- task.command("add").alias("create").description("Create a task").argument("[title]", "Task title (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-n, --notes <text>", "Task notes (or pipe via stdin)").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").addHelpText("after", `
7900
+ task.command("add").alias("create").description("Create a task").argument("[title]", "Task title (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-n, --notes <text>", "Task notes (or pipe via stdin)").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").option("--idempotency-key <key>", "Stable key making retries safe (repeated calls create only one task)").addHelpText("after", `
7887
7901
  Examples:
7888
7902
  donebear task add "Fix login bug" --when today --json
7889
- donebear task add "Write tests" --deadline 2026-03-15 --project PROJ --json
7903
+ donebear task add "Write tests" --deadline 2030-03-15 --project PROJ --json
7890
7904
  donebear task add "Review PR" --when anytime --notes "Check for XSS" --json
7891
7905
  echo "Multi-line notes" | donebear task add "My task"
7892
7906
  donebear task add # prompts for title interactively
7893
7907
 
7894
- Date format: YYYY-MM-DD (e.g. 2026-03-15)
7908
+ Date format: YYYY-MM-DD (e.g. 2030-03-15)
7895
7909
  View (--when): inbox | anytime | today | upcoming | someday
7896
7910
  `).action(async (title, options, command) => {
7897
7911
  const context = contextFromCommand(command);
@@ -8271,9 +8285,15 @@ const OVERVIEW = {
8271
8285
  subcommands: ["things"]
8272
8286
  },
8273
8287
  label: {
8274
- description: "List workspace labels",
8288
+ description: "Manage workspace labels and task label assignments",
8275
8289
  quick_example: "donebear label list --json",
8276
- subcommands: ["list", "show"]
8290
+ subcommands: [
8291
+ "list",
8292
+ "show",
8293
+ "create",
8294
+ "add",
8295
+ "remove"
8296
+ ]
8277
8297
  },
8278
8298
  project: {
8279
8299
  description: "Manage projects",
@@ -8311,6 +8331,7 @@ const OVERVIEW = {
8311
8331
  "read",
8312
8332
  "append",
8313
8333
  "prepend",
8334
+ "random",
8314
8335
  "checklist"
8315
8336
  ]
8316
8337
  },
@@ -8436,10 +8457,21 @@ const COMMAND_SPECS = {
8436
8457
  user: "{ id, email }"
8437
8458
  },
8438
8459
  non_interactive: "For CI/scripts, set DONEBEAR_TOKEN=<token> instead of using auth login.",
8439
- options: { "--no-open": {
8440
- description: "Print auth URL instead of opening browser",
8441
- type: "boolean"
8442
- } },
8460
+ options: {
8461
+ "--no-open": {
8462
+ description: "Print auth URL instead of opening browser",
8463
+ type: "boolean"
8464
+ },
8465
+ "--port": {
8466
+ default: 8787,
8467
+ description: "Loopback callback port for the OAuth redirect",
8468
+ type: "number"
8469
+ },
8470
+ "--timeout": {
8471
+ description: "Seconds to wait for the OAuth callback",
8472
+ type: "number"
8473
+ }
8474
+ },
8443
8475
  usage: "donebear auth login [options]"
8444
8476
  },
8445
8477
  "auth status": {
@@ -8561,6 +8593,10 @@ const COMMAND_SPECS = {
8561
8593
  enum: ["note", "none"],
8562
8594
  type: "string"
8563
8595
  },
8596
+ "--skip-teams": {
8597
+ description: "Skip importing Things Areas as Teams and TeamProject links",
8598
+ type: "boolean"
8599
+ },
8564
8600
  "--workspace": {
8565
8601
  description: "Workspace id, slug, or name",
8566
8602
  type: "string"
@@ -8576,7 +8612,15 @@ const COMMAND_SPECS = {
8576
8612
  ok: "boolean",
8577
8613
  workspace: "WorkspaceSummary"
8578
8614
  },
8579
- options: { "--workspace": { type: "string" } },
8615
+ options: {
8616
+ "--sort": {
8617
+ default: "alpha",
8618
+ description: "Sort order: alpha|count",
8619
+ enum: ["alpha", "count"],
8620
+ type: "string"
8621
+ },
8622
+ "--workspace": { type: "string" }
8623
+ },
8580
8624
  usage: "donebear label list [options]"
8581
8625
  },
8582
8626
  "project add": {
@@ -8595,6 +8639,10 @@ const COMMAND_SPECS = {
8595
8639
  notes: "URL slug is auto-generated from name (e.g. 'Q2 Launch' → 'q2-launch-a1b2c3d4').",
8596
8640
  options: {
8597
8641
  "--description": { type: "string" },
8642
+ "--idempotency-key": {
8643
+ description: "Stable key making retries safe (repeated calls create only one project)",
8644
+ type: "string"
8645
+ },
8598
8646
  "--target-date": {
8599
8647
  description: "Target completion date",
8600
8648
  format: "YYYY-MM-DD",
@@ -8633,8 +8681,25 @@ const COMMAND_SPECS = {
8633
8681
  ok: "boolean"
8634
8682
  },
8635
8683
  options: {
8684
+ "--clear-description": {
8685
+ description: "Clear the project description",
8686
+ type: "boolean"
8687
+ },
8688
+ "--clear-target-date": {
8689
+ description: "Clear the project target date",
8690
+ type: "boolean"
8691
+ },
8636
8692
  "--description": { type: "string" },
8637
8693
  "--name": { type: "string" },
8694
+ "--status": {
8695
+ description: "Set status",
8696
+ enum: [
8697
+ "active",
8698
+ "done",
8699
+ "archived"
8700
+ ],
8701
+ type: "string"
8702
+ },
8638
8703
  "--target-date": {
8639
8704
  format: "YYYY-MM-DD",
8640
8705
  type: "string"
@@ -8734,6 +8799,10 @@ const COMMAND_SPECS = {
8734
8799
  format: "YYYY-MM-DD",
8735
8800
  type: "string"
8736
8801
  },
8802
+ "--idempotency-key": {
8803
+ description: "Stable key making retries safe (repeated calls create only one task)",
8804
+ type: "string"
8805
+ },
8737
8806
  "--notes": {
8738
8807
  description: "Body / description text (or pipe via stdin)",
8739
8808
  type: "string"
@@ -8824,15 +8893,35 @@ const COMMAND_SPECS = {
8824
8893
  type: "string"
8825
8894
  } },
8826
8895
  command: "task edit",
8827
- examples: ["donebear task edit abc1 --title \"Updated title\" --json", "donebear task edit abc1 --when today --deadline 2026-03-20 --json"],
8896
+ examples: [
8897
+ "donebear task edit abc1 --title \"Updated title\" --json",
8898
+ "donebear task edit abc1 --when today --deadline 2030-01-15 --json",
8899
+ "donebear task edit abc1 --clear-deadline --json"
8900
+ ],
8828
8901
  json_output: {
8829
8902
  action: "string",
8830
8903
  id: "string",
8831
8904
  ok: "boolean"
8832
8905
  },
8833
8906
  options: {
8907
+ "--clear-deadline": {
8908
+ description: "Clear the due date",
8909
+ type: "boolean"
8910
+ },
8911
+ "--clear-notes": {
8912
+ description: "Clear notes/description",
8913
+ type: "boolean"
8914
+ },
8915
+ "--clear-project": {
8916
+ description: "Remove the task from its project",
8917
+ type: "boolean"
8918
+ },
8919
+ "--clear-team": {
8920
+ description: "Remove the task from its team",
8921
+ type: "boolean"
8922
+ },
8834
8923
  "--deadline": {
8835
- description: "Set due date (pass empty string to clear)",
8924
+ description: "Set due date",
8836
8925
  format: "YYYY-MM-DD",
8837
8926
  type: "string"
8838
8927
  },
@@ -9098,9 +9187,12 @@ const TOP_LEVEL_SPECS = {
9098
9187
  },
9099
9188
  label: {
9100
9189
  command: "label",
9101
- description: "List workspace labels",
9190
+ description: "Manage workspace labels and task label assignments",
9102
9191
  subcommands: {
9192
+ add: "Add a label to a task",
9193
+ create: "Create a new label",
9103
9194
  list: "List all labels",
9195
+ remove: "Remove a label from a task",
9104
9196
  show: "Get label details"
9105
9197
  }
9106
9198
  },
@@ -9131,6 +9223,7 @@ const TOP_LEVEL_SPECS = {
9131
9223
  edit: "Update task fields",
9132
9224
  list: "List tasks (--state, --view, --search, --limit)",
9133
9225
  prepend: "Prepend text to task notes",
9226
+ random: "Pick a random task (--state to filter)",
9134
9227
  read: "Print task notes as plain text",
9135
9228
  reopen: "Reopen a completed task",
9136
9229
  show: "Get task details by id",
@@ -9757,30 +9850,8 @@ Examples:
9757
9850
  });
9758
9851
  };
9759
9852
  //#endregion
9760
- //#region src/cli.ts
9761
- const packageManifest = createRequire(import.meta.url)("../package.json");
9762
- const WORKSPACE_PREFIX_REGEX = /^workspace=(.+)$/iu;
9763
- const extractWorkspaceTarget = (argv) => {
9764
- const processedArgv = [...argv];
9765
- const firstArg = processedArgv[2];
9766
- if (firstArg) {
9767
- const match = WORKSPACE_PREFIX_REGEX.exec(firstArg);
9768
- if (match?.[1]) {
9769
- processedArgv.splice(2, 1);
9770
- return {
9771
- processedArgv,
9772
- workspaceOverride: match[1].trim()
9773
- };
9774
- }
9775
- }
9776
- return {
9777
- processedArgv,
9778
- workspaceOverride: null
9779
- };
9780
- };
9781
- const main = async () => {
9782
- loadCliEnvironmentFiles();
9783
- const { processedArgv, workspaceOverride } = extractWorkspaceTarget(process.argv);
9853
+ //#region src/program.ts
9854
+ const buildProgram = (workspaceOverride = null) => {
9784
9855
  const program = new Command();
9785
9856
  program.name(CLI_NAME).description("Done Bear CLI for auth, workspace, and task workflows").version(packageManifest.version, "-v, --version").showHelpAfterError().showSuggestionAfterError();
9786
9857
  addGlobalOptions(program);
@@ -9824,6 +9895,33 @@ Filtering & output:
9824
9895
  registerSearchCommand(program, workspaceOverride);
9825
9896
  registerHistoryCommand(program, workspaceOverride);
9826
9897
  registerWhoAmICommand(program);
9898
+ return program;
9899
+ };
9900
+ //#endregion
9901
+ //#region src/cli.ts
9902
+ const WORKSPACE_PREFIX_REGEX = /^workspace=(.+)$/iu;
9903
+ const extractWorkspaceTarget = (argv) => {
9904
+ const processedArgv = [...argv];
9905
+ const firstArg = processedArgv[2];
9906
+ if (firstArg) {
9907
+ const match = WORKSPACE_PREFIX_REGEX.exec(firstArg);
9908
+ if (match?.[1]) {
9909
+ processedArgv.splice(2, 1);
9910
+ return {
9911
+ processedArgv,
9912
+ workspaceOverride: match[1].trim()
9913
+ };
9914
+ }
9915
+ }
9916
+ return {
9917
+ processedArgv,
9918
+ workspaceOverride: null
9919
+ };
9920
+ };
9921
+ const main = async () => {
9922
+ loadCliEnvironmentFiles();
9923
+ const { processedArgv, workspaceOverride } = extractWorkspaceTarget(process.argv);
9924
+ const program = buildProgram(workspaceOverride);
9827
9925
  const hasUserArgs = processedArgv.length <= 2;
9828
9926
  const isTTY = process.stdin.isTTY === true;
9829
9927
  if (hasUserArgs && isTTY) {