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 +6 -6
- package/dist/cli.mjs +194 -134
- package/dist/cli.mjs.map +1 -1
- package/package.json +2 -6
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 #
|
|
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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
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
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
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
|
-
|
|
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
|
|
2187
|
-
|
|
2188
|
-
|
|
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
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
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
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
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
|
|
2205
|
-
|
|
2206
|
-
|
|
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
|
|
2447
|
-
|
|
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 ../../
|
|
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 ../../
|
|
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 ../../
|
|
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 ../../
|
|
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 ../../
|
|
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 ../../
|
|
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 ../../
|
|
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
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
3312
|
-
|
|
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
|
|
4279
|
-
|
|
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 --
|
|
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
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
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/
|
|
4896
|
-
|
|
4897
|
-
|
|
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
|
|
4953
|
+
//#region ../../packages/import-core/src/things/labels.ts
|
|
4945
4954
|
const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase();
|
|
4946
4955
|
//#endregion
|
|
4947
|
-
//#region
|
|
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
|
|
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
|
|
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:
|
|
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 --
|
|
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
|
},
|