donebear 0.4.2 → 0.4.3

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/AGENTS.md CHANGED
@@ -121,16 +121,16 @@ src/
121
121
  import/
122
122
  things-runner.ts # orchestrates full Things 3 import pipeline
123
123
  things-sqlite.ts # reads Things 3 SQLite database
124
- things-mapper.ts # maps Things rows to Done Bear sync transactions
125
- things-values.ts # Things date/field value decoders
126
- things-labels.ts # label normalization for import dedup
127
- things-recurrence.ts # Things recurrence to RRule conversion
128
124
  things-output.ts # import result summary formatting
129
- model-id.ts # import + idempotency-key UUIDs (composite sync ids come from @donebear/sync-ids)
130
- time-zone.ts # calendar date/time zone helpers
125
+ model-id.ts # idempotency-key UUIDs (Things import ids come from @donebear/import-core/things)
131
126
  tests/ # Vitest: npm run test --workspace=apps/cli
132
127
  ```
133
128
 
129
+ The pure Things snapshot types, mapper, recurrence conversion, date decoding,
130
+ label normalization and stable import IDs live at the explicit browser-safe
131
+ `@donebear/import-core/things` subpath. The CLI owns only SQLite file access,
132
+ execution and output.
133
+
134
134
  ## Gotchas
135
135
 
136
136
  - OAuth callback defaults to `http://127.0.0.1:8787/auth/callback`; it must be allow-listed in Supabase redirect URLs.
package/dist/cli.mjs CHANGED
@@ -2143,9 +2143,21 @@ const parsePositiveInteger = (value, label) => {
2143
2143
  };
2144
2144
  //#endregion
2145
2145
  //#region src/oauth-callback.ts
2146
+ const CALLBACK_HTML_HEADERS = {
2147
+ connection: "close",
2148
+ "content-type": "text/html; charset=utf-8"
2149
+ };
2146
2150
  const successHtml = () => `<!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>`;
2147
2151
  const escapeHtml = (text) => text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;");
2148
2152
  const errorHtml = (message) => `<!doctype html><html><head><meta charset="utf-8"/><title>Done Bear CLI</title></head><body><h1>Authentication failed</h1><p>${escapeHtml(message)}</p></body></html>`;
2153
+ const writeCallbackHtml = (request, response, statusCode, html, onFlushed) => {
2154
+ response.shouldKeepAlive = false;
2155
+ response.writeHead(statusCode, CALLBACK_HTML_HEADERS);
2156
+ response.end(html, () => {
2157
+ request.socket.destroy();
2158
+ onFlushed?.();
2159
+ });
2160
+ };
2149
2161
  const waitForOAuthCode = async (options) => await new Promise((resolve, reject) => {
2150
2162
  const host = options.redirectUrl.hostname;
2151
2163
  const port = Number(options.redirectUrl.port);
@@ -2159,6 +2171,7 @@ const waitForOAuthCode = async (options) => await new Promise((resolve, reject)
2159
2171
  if (settled) return;
2160
2172
  settled = true;
2161
2173
  clearTimeout(timeout);
2174
+ server.closeAllConnections();
2162
2175
  server.close(() => {
2163
2176
  if (result.error) {
2164
2177
  reject(result.error);
@@ -2169,41 +2182,40 @@ const waitForOAuthCode = async (options) => await new Promise((resolve, reject)
2169
2182
  };
2170
2183
  const server = createServer((request, response) => {
2171
2184
  if (!request.url) {
2172
- response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
2173
- response.end(errorHtml("Missing request URL"));
2174
- finish({ error: new CliError("OAuth callback is missing a request URL", EXIT_CODES.ERROR) });
2185
+ writeCallbackHtml(request, response, 400, errorHtml("Missing request URL"), () => {
2186
+ finish({ error: new CliError("OAuth callback is missing a request URL", EXIT_CODES.ERROR) });
2187
+ });
2175
2188
  return;
2176
2189
  }
2177
2190
  const url = new URL(request.url, options.redirectUrl.origin);
2178
2191
  if (url.pathname !== pathname) {
2179
- response.writeHead(404, { "content-type": "text/html; charset=utf-8" });
2180
- response.end(errorHtml("Invalid callback path"));
2192
+ writeCallbackHtml(request, response, 404, errorHtml("Invalid callback path"));
2181
2193
  return;
2182
2194
  }
2183
2195
  const providerError = url.searchParams.get("error");
2184
2196
  if (providerError) {
2185
2197
  const description = url.searchParams.get("error_description") ?? providerError;
2186
- response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
2187
- response.end(errorHtml(description));
2188
- finish({ error: new CliError(`OAuth provider returned an error: ${description}`, EXIT_CODES.ERROR) });
2198
+ writeCallbackHtml(request, response, 400, errorHtml(description), () => {
2199
+ finish({ error: new CliError(`OAuth provider returned an error: ${description}`, EXIT_CODES.ERROR) });
2200
+ });
2189
2201
  return;
2190
2202
  }
2191
2203
  if (url.searchParams.get("state") !== options.expectedState) {
2192
- response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
2193
- response.end(errorHtml("State verification failed"));
2194
- finish({ error: new CliError("OAuth state verification failed", EXIT_CODES.ERROR) });
2204
+ writeCallbackHtml(request, response, 400, errorHtml("State verification failed"), () => {
2205
+ finish({ error: new CliError("OAuth state verification failed", EXIT_CODES.ERROR) });
2206
+ });
2195
2207
  return;
2196
2208
  }
2197
2209
  const code = url.searchParams.get("code");
2198
2210
  if (!code) {
2199
- response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
2200
- response.end(errorHtml("Authorization code was missing"));
2201
- finish({ error: new CliError("OAuth callback is missing an authorization code", EXIT_CODES.ERROR) });
2211
+ writeCallbackHtml(request, response, 400, errorHtml("Authorization code was missing"), () => {
2212
+ finish({ error: new CliError("OAuth callback is missing an authorization code", EXIT_CODES.ERROR) });
2213
+ });
2202
2214
  return;
2203
2215
  }
2204
- response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2205
- response.end(successHtml());
2206
- finish({ code });
2216
+ writeCallbackHtml(request, response, 200, successHtml(), () => {
2217
+ finish({ code });
2218
+ });
2207
2219
  });
2208
2220
  server.on("error", (error) => {
2209
2221
  finish({ error: new CliError(`Failed to start callback server on ${host}:${port}: ${error.message}`, EXIT_CODES.ERROR) });
@@ -2428,23 +2440,32 @@ const toTaskMarker = (task) => {
2428
2440
  if (state === "archived") return "[-]";
2429
2441
  return "[ ]";
2430
2442
  };
2443
+ const completedOn = (task) => {
2444
+ if (task.completedAt === null) return null;
2445
+ const date = new Date(task.completedAt);
2446
+ return Number.isNaN(date.getTime()) ? null : formatDateOnlyEpoch(dateOnlyEpochFromLocalDate(date));
2447
+ };
2448
+ const deadlineDetail = (deadline) => deadline ? `due ${deadline}` : "";
2431
2449
  const taskListHeaders = () => [
2432
2450
  "number",
2433
2451
  "title",
2434
2452
  "state",
2435
- "deadline"
2453
+ "deadline",
2454
+ "completed"
2436
2455
  ];
2437
2456
  const taskListRows = (tasks) => tasks.map((task) => [
2438
2457
  String(task.number),
2439
2458
  task.title,
2440
2459
  getTaskState(task),
2441
- formatDateOnlyEpoch(task.deadlineAt) ?? ""
2460
+ formatDateOnlyEpoch(task.deadlineAt) ?? "",
2461
+ completedOn(task) ?? ""
2442
2462
  ]);
2443
2463
  const taskListLines = (tasks) => tasks.map((task) => {
2444
2464
  const marker = toTaskMarker(task);
2445
2465
  const deadline = formatDateOnlyEpoch(task.deadlineAt);
2446
- const suffix = deadline ? ` due ${deadline}` : "";
2447
- return `${marker} ${task.number} ${task.title}${suffix}`;
2466
+ const completed = completedOn(task);
2467
+ const detail = completed ? `done ${completed}` : deadlineDetail(deadline);
2468
+ return `${marker} ${task.number} ${task.title}${detail ? ` ${detail}` : ""}`;
2448
2469
  });
2449
2470
  //#endregion
2450
2471
  //#region src/confirm.ts
@@ -2466,29 +2487,29 @@ const confirmAction = (context, message) => {
2466
2487
  });
2467
2488
  };
2468
2489
  //#endregion
2469
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/regex.js
2490
+ //#region ../../node_modules/uuid/dist-node/regex.js
2470
2491
  var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;
2471
2492
  //#endregion
2472
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/validate.js
2493
+ //#region ../../node_modules/uuid/dist-node/validate.js
2473
2494
  function validate(uuid) {
2474
2495
  return typeof uuid === "string" && regex_default.test(uuid);
2475
2496
  }
2476
2497
  //#endregion
2477
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/parse.js
2498
+ //#region ../../node_modules/uuid/dist-node/parse.js
2478
2499
  function parse(uuid) {
2479
2500
  if (!validate(uuid)) throw TypeError("Invalid UUID");
2480
2501
  let v;
2481
2502
  return Uint8Array.of((v = parseInt(uuid.slice(0, 8), 16)) >>> 24, v >>> 16 & 255, v >>> 8 & 255, v & 255, (v = parseInt(uuid.slice(9, 13), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(14, 18), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(19, 23), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(24, 36), 16)) / 1099511627776 & 255, v / 4294967296 & 255, v >>> 24 & 255, v >>> 16 & 255, v >>> 8 & 255, v & 255);
2482
2503
  }
2483
2504
  //#endregion
2484
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/stringify.js
2505
+ //#region ../../node_modules/uuid/dist-node/stringify.js
2485
2506
  const byteToHex = [];
2486
2507
  for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
2487
2508
  function unsafeStringify(arr, offset = 0) {
2488
2509
  return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase();
2489
2510
  }
2490
2511
  //#endregion
2491
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/v35.js
2512
+ //#region ../../node_modules/uuid/dist-node/v35.js
2492
2513
  function stringToBytes(str) {
2493
2514
  str = unescape(encodeURIComponent(str));
2494
2515
  const bytes = new Uint8Array(str.length);
@@ -2517,14 +2538,14 @@ function v35(version, hash, value, namespace, buf, offset) {
2517
2538
  return unsafeStringify(bytes);
2518
2539
  }
2519
2540
  //#endregion
2520
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/sha1.js
2541
+ //#region ../../node_modules/uuid/dist-node/sha1.js
2521
2542
  function sha1(bytes) {
2522
2543
  if (Array.isArray(bytes)) bytes = Buffer.from(bytes);
2523
2544
  else if (typeof bytes === "string") bytes = Buffer.from(bytes, "utf8");
2524
2545
  return createHash("sha1").update(bytes).digest();
2525
2546
  }
2526
2547
  //#endregion
2527
- //#region ../../packages/sync-ids/node_modules/uuid/dist-node/v5.js
2548
+ //#region ../../node_modules/uuid/dist-node/v5.js
2528
2549
  function v5(value, namespace, buf, offset) {
2529
2550
  return v35(80, sha1, value, namespace, buf, offset);
2530
2551
  }
@@ -2552,9 +2573,7 @@ const createProjectMembershipId = (params) => createCompositeSyncId("ProjectMemb
2552
2573
  ]);
2553
2574
  //#endregion
2554
2575
  //#region src/import/model-id.ts
2555
- const IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
2556
2576
  const IDEMPOTENCY_ID_NAMESPACE = "4c8af5e2-1d36-5a0b-9c47-2f81b3d6e955";
2557
- const createDeterministicImportId = (params) => createDeterministicId(IMPORT_ID_NAMESPACE, `things:${params.workspaceId}:${params.modelName}:${params.sourceId}`);
2558
2577
  const createIdempotentMutationId = (params) => createDeterministicId(IDEMPOTENCY_ID_NAMESPACE, `idem:${params.workspaceId}:${params.modelName}:${params.part}:${params.key}`);
2559
2578
  //#endregion
2560
2579
  //#region src/workspace-context.ts
@@ -2631,19 +2650,35 @@ const resolveWorkspace = async (context, options) => {
2631
2650
  //#endregion
2632
2651
  //#region src/commands/task.ts
2633
2652
  const DEFAULT_TASK_LIST_LIMIT = 20;
2634
- const VALID_TASK_VIEWS = new Set([
2653
+ /** The views `--when` can place a task in: the ones with start fields. */
2654
+ const ACTIVE_TASK_VIEWS = new Set([
2635
2655
  "inbox",
2636
2656
  "anytime",
2637
2657
  "today",
2638
2658
  "upcoming",
2639
- "someday",
2659
+ "someday"
2660
+ ]);
2661
+ /** The views `--view` can filter a list by, which adds the two read-only ones. */
2662
+ const VALID_TASK_VIEWS = new Set([
2663
+ ...ACTIVE_TASK_VIEWS,
2640
2664
  "logbook",
2641
2665
  "trash"
2642
2666
  ]);
2643
2667
  const parseTaskView = (value) => {
2644
2668
  const normalized = value.trim().toLowerCase();
2645
2669
  if (VALID_TASK_VIEWS.has(normalized)) return normalized;
2646
- throw new Error(`Invalid view "${value}". Expected one of: inbox, anytime, today, upcoming, someday.`);
2670
+ throw new Error(`Invalid view "${value}". Expected one of: ${[...VALID_TASK_VIEWS].join(", ")}.`);
2671
+ };
2672
+ /**
2673
+ * The view a `--when` may name. Logbook and Trash are derived from state, not
2674
+ * from a start field, so there are no start fields to build for them: passing
2675
+ * either used to fall through `buildTaskStartFields` and quietly land the task
2676
+ * in Inbox. Completing and archiving have their own commands.
2677
+ */
2678
+ const parseTaskWhenView = (value) => {
2679
+ const view = parseTaskView(value);
2680
+ if (view === "logbook" || view === "trash") throw new Error(`Invalid --when "${value}". Expected one of: ${[...ACTIVE_TASK_VIEWS].join(", ")}. Use "task done" to complete a task and "task archive" to archive one.`);
2681
+ return view;
2647
2682
  };
2648
2683
  const parseDeadline = (value) => parseCliDateOnlyToEpoch(value, "deadline date");
2649
2684
  /**
@@ -2667,7 +2702,7 @@ const parseDeadline = (value) => parseCliDateOnlyToEpoch(value, "deadline date")
2667
2702
  * there would make "typed nothing" indistinguishable from "typed --when today".
2668
2703
  */
2669
2704
  const resolveAddView = (when, deadlineEpoch, hasContainer = false) => {
2670
- if (when) return parseTaskView(when);
2705
+ if (when) return parseTaskWhenView(when);
2671
2706
  if (deadlineEpoch !== null) return "anytime";
2672
2707
  return hasContainer ? "inbox" : "today";
2673
2708
  };
@@ -2990,7 +3025,7 @@ const applyScalarEdits = (updates, options) => {
2990
3025
  const description = options.description.trim();
2991
3026
  updates.description = description.length > 0 ? description : null;
2992
3027
  }
2993
- if (typeof options.when === "string") Object.assign(updates, buildTaskStartFields(parseTaskView(options.when)));
3028
+ if (typeof options.when === "string") Object.assign(updates, buildTaskStartFields(parseTaskWhenView(options.when)));
2994
3029
  if (options.clearDeadline) updates.deadlineAt = null;
2995
3030
  else if (typeof options.deadline === "string") updates.deadlineAt = parseDeadline(options.deadline);
2996
3031
  };
@@ -3155,33 +3190,7 @@ const runChecklistList = async (context, taskId, workspaceOption) => {
3155
3190
  writeInfo(context, `Checklist for: ${task.title}`);
3156
3191
  for (const item of sorted) writeInfo(context, `${item.completedAt === null ? "[ ]" : "[x]"} ${item.id.slice(0, 8)} ${item.title}`);
3157
3192
  };
3158
- const registerTaskCommands = (program, workspaceOverride) => {
3159
- const task = program.command("task").alias("tasks").description("Manage tasks").addHelpText("after", `
3160
- Examples:
3161
- donebear task list --json
3162
- donebear task add "Fix login bug" --when today --json
3163
- donebear task add "Write tests" --deadline 2026-03-15 --project PROJ --json
3164
- donebear task edit 12 --deadline 2026-03-05 --json
3165
- donebear task done 12 --json
3166
- donebear task list --state done --limit 50 --json
3167
-
3168
- Date format: YYYY-MM-DD (e.g. 2026-03-15)
3169
- Task number: workspace-scoped positive integer (e.g. 12)
3170
- View buckets: inbox | anytime | today | upcoming | someday
3171
- New tasks default to today; to anytime with --deadline and no --when; to
3172
- inbox with --project or --team and no --when. Pass --when inbox to capture
3173
- without scheduling.
3174
-
3175
- Agent tip: run "donebear spec task add" for full option reference
3176
- `).action(async (_options, command) => {
3177
- const context = contextFromCommand(command);
3178
- await runWithErrorHandling(context, async () => {
3179
- await runTaskList(context, {
3180
- limit: DEFAULT_TASK_LIST_LIMIT,
3181
- state: "open"
3182
- }, workspaceOverride);
3183
- });
3184
- });
3193
+ const registerTaskSubcommands = (task, workspaceOverride) => {
3185
3194
  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 description").option("-v, --view <view>", "Filter by view (inbox|anytime|today|upcoming|someday|logbook|trash)").addHelpText("after", `
3186
3195
  Examples:
3187
3196
  donebear task list --json
@@ -3308,12 +3317,8 @@ without scheduling.
3308
3317
  });
3309
3318
  });
3310
3319
  });
3311
- const checklist = task.command("checklist").alias("cl").description("Manage task checklist items").argument("<task-number>", "Workspace-scoped task number").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
3312
- const context = contextFromCommand(command);
3313
- await runWithErrorHandling(context, async () => {
3314
- await runChecklistList(context, taskId, options.workspace);
3315
- });
3316
- });
3320
+ };
3321
+ const registerChecklistSubcommands = (checklist) => {
3317
3322
  checklist.command("add").description("Add a checklist item").argument("<task-number>", "Workspace-scoped task number").argument("<title>", "Item title").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, title, options, command) => {
3318
3323
  const context = contextFromCommand(command);
3319
3324
  await runWithErrorHandling(context, async () => {
@@ -3516,6 +3521,41 @@ without scheduling.
3516
3521
  });
3517
3522
  });
3518
3523
  };
3524
+ const registerTaskCommands = (program, workspaceOverride) => {
3525
+ const task = program.command("task").alias("tasks").description("Manage tasks").addHelpText("after", `
3526
+ Examples:
3527
+ donebear task list --json
3528
+ donebear task add "Fix login bug" --when today --json
3529
+ donebear task add "Write tests" --deadline 2026-03-15 --project PROJ --json
3530
+ donebear task edit 12 --deadline 2026-03-05 --json
3531
+ donebear task done 12 --json
3532
+ donebear task list --state done --limit 50 --json
3533
+
3534
+ Date format: YYYY-MM-DD (e.g. 2026-03-15)
3535
+ Task number: workspace-scoped positive integer (e.g. 12)
3536
+ View buckets: inbox | anytime | today | upcoming | someday
3537
+ New tasks default to today; to anytime with --deadline and no --when; to
3538
+ inbox with --project or --team and no --when. Pass --when inbox to capture
3539
+ without scheduling.
3540
+
3541
+ Agent tip: run "donebear spec task add" for full option reference
3542
+ `).action(async (_options, command) => {
3543
+ const context = contextFromCommand(command);
3544
+ await runWithErrorHandling(context, async () => {
3545
+ await runTaskList(context, {
3546
+ limit: DEFAULT_TASK_LIST_LIMIT,
3547
+ state: "open"
3548
+ }, workspaceOverride);
3549
+ });
3550
+ });
3551
+ registerTaskSubcommands(task, workspaceOverride);
3552
+ registerChecklistSubcommands(task.command("checklist").alias("cl").description("Manage task checklist items").argument("<task-number>", "Workspace-scoped task number").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
3553
+ const context = contextFromCommand(command);
3554
+ await runWithErrorHandling(context, async () => {
3555
+ await runChecklistList(context, taskId, options.workspace);
3556
+ });
3557
+ }));
3558
+ };
3519
3559
  //#endregion
3520
3560
  //#region src/commands/comment.ts
3521
3561
  /**
@@ -4275,11 +4315,19 @@ state and start date rather than stored as a column:
4275
4315
  - logbook completed
4276
4316
  - trash archived
4277
4317
 
4278
- \`--when\` sets the bucket on create and edit; \`--state open|done|archived|all\`
4279
- reaches the Logbook and Trash on list.
4318
+ \`--when\` sets the bucket on create and edit, and takes the first five only:
4319
+ logbook and trash come from a task's state, so \`task done\` and \`task archive\`
4320
+ put a task there, not \`--when\`. On list, \`--state open|done|archived|all\`
4321
+ reaches the Logbook and Trash, and so does \`--view logbook\` or \`--view trash\`,
4322
+ which widen the state for you. Do not pair \`--state done\` with a bucket view: a
4323
+ completed task's view is logbook, so \`--state done --view today\` matches
4324
+ nothing. What was finished today or this week comes off the completion date,
4325
+ not the ordering: the list prints it in the \`completed\` column, \`task show
4326
+ --json\` reports it as \`completedAtIso\`, and each row from \`task list --json\`
4327
+ carries \`completedAt\`.
4280
4328
 
4281
4329
  donebear task add "Renew passport" --when upcoming --deadline 2030-03-01 --json
4282
- donebear task list --state done --json
4330
+ donebear task list --view logbook --json
4283
4331
 
4284
4332
  \`donebear task add\` defaults to today, so a task created without \`--when\`
4285
4333
  starts today and shows up on the user's working list straight away. That is the
@@ -4496,10 +4544,16 @@ a slightly wrong fit. Check \`donebear label list --json\` first and prefer an
4496
4544
  existing label when it is a reasonable match. Teams are set up in the app;
4497
4545
  there is no team create command.
4498
4546
 
4499
- Every workspace member sees every project; \`donebear project join\` and
4500
- \`donebear project leave\` only change which ones sit in your sidebar's Your
4501
- projects list. Neither grants nor removes access, both are idempotent, and
4502
- creating a project already joins you to it.`,
4547
+ A project is either visible to the whole workspace (the default) or private
4548
+ to its members. A private project and its tasks are invisible to everyone
4549
+ else — absent from \`project list\`, \`task list\` and \`search\`, not merely
4550
+ hidden. Visibility is set in the app.
4551
+
4552
+ On a workspace-visible project, \`donebear project join\` and \`donebear
4553
+ project leave\` only change which ones sit in your sidebar's Your projects
4554
+ list: neither grants nor removes access. On a private project those same rows
4555
+ are the access, so leaving one gives up your ability to see it. Both are
4556
+ idempotent, and creating a project already joins you to it.`,
4503
4557
  title: "Projects, teams, and labels",
4504
4558
  whenToUse: "Deciding where a task belongs, creating a project or label, or joining/leaving a project."
4505
4559
  },
@@ -4892,59 +4946,14 @@ const getUtcCalendarDayEndEpoch = (date) => {
4892
4946
  return Date.UTC(parts.year, parts.month - 1, parts.day, 23, 59, 59, 999);
4893
4947
  };
4894
4948
  //#endregion
4895
- //#region ../../packages/sync-ids/src/meet-done-bear.ts
4896
- /**
4897
- * Ids for the "Meet Done Bear" sample project every new workspace is seeded
4898
- * with.
4899
- *
4900
- * `manage-api` seeds the rows; `manage-frontend` and the CLI have to recognise
4901
- * and trash the same rows when an import lands, and neither can read the ids
4902
- * off the server. All three therefore re-derive them from the workspace id,
4903
- * which only works while the namespace and the row counts agree — so they live
4904
- * here rather than in each app.
4905
- *
4906
- * Unlike the composite ids in `./index`, these have no Swift port and no entry
4907
- * in `test-vectors.json`: iOS never seeds or dismisses the sample project.
4908
- */
4909
- const MEET_DONE_BEAR_ID_NAMESPACE = "b7e1d4a0-6c2f-4a8e-9d13-5f0c8a2b7e91";
4910
- const createMeetDoneBearProjectId = (workspaceId) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Project:${workspaceId}`);
4911
- const createMeetDoneBearHeadingId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Heading:${workspaceId}:${index}`);
4912
- const createMeetDoneBearTaskId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Task:${workspaceId}:${index}`);
4913
- //#endregion
4914
- //#region src/import/meet-done-bear.ts
4915
- /**
4916
- * Sample rows that still exist in the pre-import bootstrap. Missing or
4917
- * already-trashed rows are omitted so a later import does not fail.
4918
- */
4919
- const listMeetDoneBearArchiveTargets = (options) => {
4920
- const projectId = createMeetDoneBearProjectId(options.workspaceId);
4921
- if (!options.existingProjectIds.has(projectId)) return [];
4922
- const targets = [{
4923
- modelId: projectId,
4924
- modelName: "Project"
4925
- }];
4926
- const headingIds = options.existingHeadingIds;
4927
- if (headingIds) for (let index = 0; index < 3; index += 1) {
4928
- const modelId = createMeetDoneBearHeadingId(options.workspaceId, index);
4929
- if (headingIds.has(modelId)) targets.push({
4930
- modelId,
4931
- modelName: "ProjectHeading"
4932
- });
4933
- }
4934
- for (let index = 0; index < 9; index += 1) {
4935
- const modelId = createMeetDoneBearTaskId(options.workspaceId, index);
4936
- if (options.existingTaskIds.has(modelId)) targets.push({
4937
- modelId,
4938
- modelName: "Task"
4939
- });
4940
- }
4941
- return targets;
4942
- };
4949
+ //#region ../../packages/import-core/src/things/ids.ts
4950
+ const THINGS_IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
4951
+ const createDeterministicImportId = (params) => createDeterministicId(THINGS_IMPORT_ID_NAMESPACE, `things:${params.workspaceId}:${params.modelName}:${params.sourceId}`);
4943
4952
  //#endregion
4944
- //#region src/import/things-labels.ts
4953
+ //#region ../../packages/import-core/src/things/labels.ts
4945
4954
  const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase();
4946
4955
  //#endregion
4947
- //#region src/import/things-values.ts
4956
+ //#region ../../packages/import-core/src/things/values.ts
4948
4957
  var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
4949
4958
  (function webpackUniversalModuleDefinition(root, factory) {
4950
4959
  if (typeof exports === "object" && typeof module === "object") module.exports = factory();
@@ -7759,7 +7768,7 @@ const combineThingsReminderAt = (options) => {
7759
7768
  return baseInstant === null ? null : baseInstant + offset;
7760
7769
  };
7761
7770
  //#endregion
7762
- //#region src/import/things-recurrence.ts
7771
+ //#region ../../packages/import-core/src/things/recurrence.ts
7763
7772
  const THINGS_FREQUENCY_TO_RRULE = new Map([
7764
7773
  [4, "YEARLY"],
7765
7774
  [8, "MONTHLY"],
@@ -7900,7 +7909,7 @@ const resolveThingsRepeatInstanceDate = (task, timeZone) => {
7900
7909
  return decodeThingsPackedCalendarDate(task.startDate, repeatTimeZone) ?? decodeThingsPackedCalendarDate(task.nextInstanceStartDate, repeatTimeZone) ?? null;
7901
7910
  };
7902
7911
  //#endregion
7903
- //#region src/import/things-mapper.ts
7912
+ //#region ../../packages/import-core/src/things/mapper.ts
7904
7913
  const MAX_CHECKLIST_TITLE_LENGTH = 500;
7905
7914
  const classifyTaskKind = (task) => {
7906
7915
  if (task.type === 2) return "heading";
@@ -8558,7 +8567,7 @@ const buildThingsImportPlan = (options) => {
8558
8567
  maxLength: 24
8559
8568
  }),
8560
8569
  timeZone: options.timeZone,
8561
- todayDateOnly: dateOnlyEpochFromLocalDate(),
8570
+ todayDateOnly: localDateToDateOnlyEpoch(/* @__PURE__ */ new Date()),
8562
8571
  workspaceId
8563
8572
  };
8564
8573
  const teams = options.skipTeams ? [] : buildTeamActions(ctx, snapshot.areas);
@@ -8623,6 +8632,56 @@ const buildThingsImportPlan = (options) => {
8623
8632
  };
8624
8633
  };
8625
8634
  //#endregion
8635
+ //#region ../../packages/sync-ids/src/meet-done-bear.ts
8636
+ /**
8637
+ * Ids for the "Meet Done Bear" sample project every new workspace is seeded
8638
+ * with.
8639
+ *
8640
+ * `manage-api` seeds the rows; `manage-frontend` and the CLI have to recognise
8641
+ * and trash the same rows when an import lands, and neither can read the ids
8642
+ * off the server. All three therefore re-derive them from the workspace id,
8643
+ * which only works while the namespace and the row counts agree — so they live
8644
+ * here rather than in each app.
8645
+ *
8646
+ * iOS ports `createMeetDoneBearProjectId` / `isMeetDoneBearProjectId` in
8647
+ * `DeterministicIds.swift` so Quiet Invite can ignore seed tasks. There is
8648
+ * still no vector section — dedicated Swift tests pin the same namespace.
8649
+ */
8650
+ const MEET_DONE_BEAR_ID_NAMESPACE = "b7e1d4a0-6c2f-4a8e-9d13-5f0c8a2b7e91";
8651
+ const createMeetDoneBearProjectId = (workspaceId) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Project:${workspaceId}`);
8652
+ const createMeetDoneBearHeadingId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Heading:${workspaceId}:${index}`);
8653
+ const createMeetDoneBearTaskId = (workspaceId, index) => createDeterministicId(MEET_DONE_BEAR_ID_NAMESPACE, `MeetDoneBear:Task:${workspaceId}:${index}`);
8654
+ //#endregion
8655
+ //#region src/import/meet-done-bear.ts
8656
+ /**
8657
+ * Sample rows that still exist in the pre-import bootstrap. Missing or
8658
+ * already-trashed rows are omitted so a later import does not fail.
8659
+ */
8660
+ const listMeetDoneBearArchiveTargets = (options) => {
8661
+ const projectId = createMeetDoneBearProjectId(options.workspaceId);
8662
+ if (!options.existingProjectIds.has(projectId)) return [];
8663
+ const targets = [{
8664
+ modelId: projectId,
8665
+ modelName: "Project"
8666
+ }];
8667
+ const headingIds = options.existingHeadingIds;
8668
+ if (headingIds) for (let index = 0; index < 3; index += 1) {
8669
+ const modelId = createMeetDoneBearHeadingId(options.workspaceId, index);
8670
+ if (headingIds.has(modelId)) targets.push({
8671
+ modelId,
8672
+ modelName: "ProjectHeading"
8673
+ });
8674
+ }
8675
+ for (let index = 0; index < 9; index += 1) {
8676
+ const modelId = createMeetDoneBearTaskId(options.workspaceId, index);
8677
+ if (options.existingTaskIds.has(modelId)) targets.push({
8678
+ modelId,
8679
+ modelName: "Task"
8680
+ });
8681
+ }
8682
+ return targets;
8683
+ };
8684
+ //#endregion
8626
8685
  //#region src/import/things-sqlite.ts
8627
8686
  const THINGS_CONTAINER_PATH = join(homedir(), "Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac");
8628
8687
  const THINGS_BUNDLE_NAME = "Things Database.thingsdatabase";
@@ -10878,17 +10937,16 @@ const COMMAND_SPECS = {
10878
10937
  command: "task list",
10879
10938
  examples: [
10880
10939
  "donebear task list --json",
10881
- "donebear task list --state done --json",
10940
+ "donebear task list --view logbook --json",
10882
10941
  "donebear task list --view today --json",
10883
10942
  "donebear task list --search \"login\" --json"
10884
10943
  ],
10885
10944
  json_output: {
10886
10945
  autoSelected: "boolean",
10887
10946
  count: "number",
10888
- limited: "TaskRecord[]",
10889
- ok: "boolean",
10890
- search: "string | null",
10891
10947
  state: "string",
10948
+ tasks: "TaskRecord[]",
10949
+ total: "number",
10892
10950
  view: "string | null",
10893
10951
  workspace: "WorkspaceSummary"
10894
10952
  },
@@ -10913,13 +10971,15 @@ const COMMAND_SPECS = {
10913
10971
  type: "string"
10914
10972
  },
10915
10973
  "--view": {
10916
- description: "Filter by view bucket",
10974
+ description: "Filter by view bucket. logbook and trash widen the state to done and archived, so they read the completed and archived tasks whatever --state says; every other view reads --state, and pairing --state done with one of them matches nothing",
10917
10975
  enum: [
10918
10976
  "inbox",
10919
10977
  "anytime",
10920
10978
  "today",
10921
10979
  "upcoming",
10922
- "someday"
10980
+ "someday",
10981
+ "logbook",
10982
+ "trash"
10923
10983
  ],
10924
10984
  type: "string"
10925
10985
  },