donebear 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +10 -10
- package/LICENSE.md +21 -0
- package/README.md +20 -8
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +1302 -407
- package/dist/cli.mjs.map +1 -1
- package/dist/context-store-BuhPB0I9.mjs +2 -0
- package/dist/{context-store-BBEVWudm.mjs → context-store-ByUXl7Cn.mjs} +18 -3
- package/dist/context-store-ByUXl7Cn.mjs.map +1 -0
- package/dist/{prompt-GfcWO85b.mjs → prompt-Bv9qkjNw.mjs} +16 -4
- package/dist/prompt-Bv9qkjNw.mjs.map +1 -0
- package/dist/prompt-ujp1V6r9.mjs +2 -0
- package/package.json +20 -18
- package/dist/context-store-BBEVWudm.mjs.map +0 -1
- package/dist/context-store-ClAx1T2N.mjs +0 -3
- package/dist/prompt-B7mDo4K7.mjs +0 -3
- package/dist/prompt-GfcWO85b.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { n as readStdin, t as promptForValue } from "./prompt-
|
|
2
|
+
import { S as DONEBEAR_TOKEN_ENV, _ as DONEBEAR_API_URL_ENV, a as SyncMutationRejectedError, c as CLI_NAME, f as DEFAULT_APP_URL, i as EXIT_CODES, m as DEFAULT_OAUTH_CALLBACK_PORT, n as setCurrentWorkspace, o as toCliError, p as DEFAULT_OAUTH_CALLBACK_PATH, r as CliError, s as AUTH_FILE_PATH, t as ensureLocalContext, u as CONFIG_DIR } from "./context-store-ByUXl7Cn.mjs";
|
|
3
|
+
import { n as readStdin, t as promptForValue } from "./prompt-Bv9qkjNw.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { basename, join, resolve } from "node:path";
|
|
@@ -14,11 +14,31 @@ import { accessSync, constants, existsSync, readdirSync, statSync } from "node:f
|
|
|
14
14
|
import { config } from "dotenv";
|
|
15
15
|
import { Command, InvalidArgumentError } from "commander";
|
|
16
16
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
18
|
import { createServer } from "node:http";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
19
19
|
import Database from "better-sqlite3";
|
|
20
20
|
//#region \0rolldown/runtime.js
|
|
21
|
+
var __create = Object.create;
|
|
22
|
+
var __defProp = Object.defineProperty;
|
|
23
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
24
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
25
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
26
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
21
27
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
28
|
+
var __copyProps = (to, from, except, desc) => {
|
|
29
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
30
|
+
key = keys[i];
|
|
31
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
32
|
+
get: ((k) => from[k]).bind(null, key),
|
|
33
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return to;
|
|
37
|
+
};
|
|
38
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
39
|
+
value: mod,
|
|
40
|
+
enumerable: true
|
|
41
|
+
}) : target, mod));
|
|
22
42
|
//#endregion
|
|
23
43
|
//#region src/output.ts
|
|
24
44
|
const writeLine = (stream, message) => {
|
|
@@ -84,9 +104,7 @@ const writeOutput = (context, data) => {
|
|
|
84
104
|
case "json":
|
|
85
105
|
writeJson(result);
|
|
86
106
|
break;
|
|
87
|
-
default:
|
|
88
|
-
writeJson(result);
|
|
89
|
-
break;
|
|
107
|
+
default: writeJson(result);
|
|
90
108
|
}
|
|
91
109
|
if (context.copy) copyToClipboard(JSON.stringify(result, null, 2));
|
|
92
110
|
};
|
|
@@ -155,9 +173,10 @@ const tokenize = (input) => {
|
|
|
155
173
|
let current = "";
|
|
156
174
|
let inQuote = false;
|
|
157
175
|
let quoteChar = "";
|
|
158
|
-
for (const char of input) if (inQuote)
|
|
159
|
-
|
|
160
|
-
|
|
176
|
+
for (const char of input) if (inQuote) {
|
|
177
|
+
if (char === quoteChar) inQuote = false;
|
|
178
|
+
else current += char;
|
|
179
|
+
} else if (char === "\"" || char === "'") {
|
|
161
180
|
inQuote = true;
|
|
162
181
|
quoteChar = char;
|
|
163
182
|
} else if (char === " ") {
|
|
@@ -209,7 +228,7 @@ const runInteractiveMode = async (context, program) => {
|
|
|
209
228
|
rl.close();
|
|
210
229
|
if (sessionEntries.length > 0) try {
|
|
211
230
|
const payload = {
|
|
212
|
-
entries: [...await loadHistory(HISTORY_FILE), ...sessionEntries].slice(-
|
|
231
|
+
entries: [...await loadHistory(HISTORY_FILE), ...sessionEntries].slice(-200),
|
|
213
232
|
version: 1
|
|
214
233
|
};
|
|
215
234
|
await mkdir(CONFIG_DIR, {
|
|
@@ -346,7 +365,8 @@ const parseAuthFile = (payload) => {
|
|
|
346
365
|
};
|
|
347
366
|
const readStoredAuthSession = async () => {
|
|
348
367
|
try {
|
|
349
|
-
|
|
368
|
+
const raw = await readFile(AUTH_FILE_PATH, "utf-8");
|
|
369
|
+
return parseAuthFile(raw).session;
|
|
350
370
|
} catch (error) {
|
|
351
371
|
if (isRecord$1(error) && error.code === "ENOENT") return null;
|
|
352
372
|
if (error instanceof CliError) throw error;
|
|
@@ -501,6 +521,8 @@ const resolveTokenStatus = (token) => {
|
|
|
501
521
|
*/
|
|
502
522
|
const TASK_FIELDS = {
|
|
503
523
|
bootstrapFields: [
|
|
524
|
+
"agentAssigneeId",
|
|
525
|
+
"revision",
|
|
504
526
|
"id",
|
|
505
527
|
"number",
|
|
506
528
|
"title",
|
|
@@ -534,6 +556,7 @@ const TASK_FIELDS = {
|
|
|
534
556
|
"repeatNextAt"
|
|
535
557
|
],
|
|
536
558
|
insertFields: [
|
|
559
|
+
"agentAssigneeId",
|
|
537
560
|
"archivedAt",
|
|
538
561
|
"assigneeId",
|
|
539
562
|
"completedAt",
|
|
@@ -554,6 +577,7 @@ const TASK_FIELDS = {
|
|
|
554
577
|
"repeatStatus",
|
|
555
578
|
"repeatTemplateId",
|
|
556
579
|
"repeatType",
|
|
580
|
+
"revision",
|
|
557
581
|
"sortOrder",
|
|
558
582
|
"start",
|
|
559
583
|
"startBucket",
|
|
@@ -566,6 +590,8 @@ const TASK_FIELDS = {
|
|
|
566
590
|
"workspaceId"
|
|
567
591
|
],
|
|
568
592
|
iosKeys: [
|
|
593
|
+
"agentAssigneeId",
|
|
594
|
+
"revision",
|
|
569
595
|
"id",
|
|
570
596
|
"number",
|
|
571
597
|
"title",
|
|
@@ -618,11 +644,14 @@ const TASK_FIELDS = {
|
|
|
618
644
|
"projectId",
|
|
619
645
|
"teamId",
|
|
620
646
|
"assigneeId",
|
|
647
|
+
"agentAssigneeId",
|
|
648
|
+
"revision",
|
|
621
649
|
"headingId",
|
|
622
650
|
"repeatRule",
|
|
623
651
|
"repeatTemplateId"
|
|
624
652
|
],
|
|
625
653
|
updateFields: [
|
|
654
|
+
"agentAssigneeId",
|
|
626
655
|
"title",
|
|
627
656
|
"description",
|
|
628
657
|
"sortOrder",
|
|
@@ -654,7 +683,7 @@ const TASK_FIELDS = {
|
|
|
654
683
|
}.readProjection.join("\n");
|
|
655
684
|
//#endregion
|
|
656
685
|
//#region ../../packages/task-logic/src/date-only.ts
|
|
657
|
-
const DATE_ONLY_DAY_MS =
|
|
686
|
+
const DATE_ONLY_DAY_MS = 864e5;
|
|
658
687
|
const DATE_ONLY_INPUT_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/u;
|
|
659
688
|
const isFiniteEpoch$1 = (value) => typeof value === "number" && Number.isFinite(value);
|
|
660
689
|
const isCanonicalDateOnlyEpoch = (value) => value % DATE_ONLY_DAY_MS === 0;
|
|
@@ -918,6 +947,71 @@ const createSyncMutateClient = ({ baseUrl, errorMapper = defaultErrorMapper$1, f
|
|
|
918
947
|
return { mutate };
|
|
919
948
|
};
|
|
920
949
|
//#endregion
|
|
950
|
+
//#region ../../packages/manage-client/src/bound-task-update.ts
|
|
951
|
+
/** Task fields a bound agent may change through a conditional command. */
|
|
952
|
+
const BOUND_TASK_PATCH_FIELDS = [
|
|
953
|
+
"archivedAt",
|
|
954
|
+
"completedAt",
|
|
955
|
+
"deadlineAt",
|
|
956
|
+
"description",
|
|
957
|
+
"headingId",
|
|
958
|
+
"projectId",
|
|
959
|
+
"reminderAt",
|
|
960
|
+
"start",
|
|
961
|
+
"startBucket",
|
|
962
|
+
"startDate",
|
|
963
|
+
"teamId",
|
|
964
|
+
"title",
|
|
965
|
+
"todayIndexReferenceDate"
|
|
966
|
+
];
|
|
967
|
+
/**
|
|
968
|
+
* Bound keys write through the conditional Task command; unbound callers
|
|
969
|
+
* retain native sync. Returns false when the caller is not a bound agent.
|
|
970
|
+
*/
|
|
971
|
+
const updateBoundTasks = async (input) => {
|
|
972
|
+
const noun = input.noun ?? "change";
|
|
973
|
+
if (!input.token.startsWith("db_")) return false;
|
|
974
|
+
const workspaceId = input.updates[0]?.task.workspaceId;
|
|
975
|
+
if (!workspaceId || input.updates.some(({ task }) => task.workspaceId !== workspaceId)) throw new Error("Change tasks in one explicit workspace.");
|
|
976
|
+
const binding = await input.send(`/api/task-agents/self?workspaceId=${encodeURIComponent(workspaceId)}`);
|
|
977
|
+
if (binding.workspaceId !== workspaceId || binding.agent === void 0) throw new Error("Task agent binding is unavailable.");
|
|
978
|
+
if (binding.agent === null) return false;
|
|
979
|
+
if (binding.agent.disabledAt !== null) throw new Error("This task agent is disabled.");
|
|
980
|
+
if (input.updates.some(({ task }) => task.agentAssigneeId !== binding.agent?.id)) throw new Error("A task agent can change only tasks assigned to it.");
|
|
981
|
+
if (input.updates.some(({ task }) => !Number.isInteger(task.revision) || task.revision < 1)) throw new Error("The change requires the original selected task revision.");
|
|
982
|
+
const allowed = new Set(BOUND_TASK_PATCH_FIELDS);
|
|
983
|
+
for (const { patch } of input.updates) {
|
|
984
|
+
const refused = Object.keys(patch).filter((key) => !allowed.has(key));
|
|
985
|
+
if (refused.length > 0) throw new Error(`A task agent cannot change ${refused.join(", ")}. Hand the task off instead.`);
|
|
986
|
+
}
|
|
987
|
+
let changed = 0;
|
|
988
|
+
for (const { task, patch } of input.updates.filter(({ task: selected }, index, updates) => updates.findIndex((candidate) => candidate.task.id === selected.id) === index)) try {
|
|
989
|
+
const result = await input.send("/api/task-commands", {
|
|
990
|
+
expectedRevision: task.revision,
|
|
991
|
+
kind: "update",
|
|
992
|
+
operationId: crypto.randomUUID(),
|
|
993
|
+
patch,
|
|
994
|
+
taskId: task.id,
|
|
995
|
+
workspaceId
|
|
996
|
+
});
|
|
997
|
+
if (result.kind !== "update" || result.task?.id !== task.id || Object.entries(patch).some(([key, value]) => result.task?.[key] !== value) || result.task.revision !== task.revision + 1) throw new Error("The change has no matching committed receipt; do not retry automatically.");
|
|
998
|
+
changed += 1;
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
throw new Error(`${noun[0]?.toUpperCase()}${noun.slice(1)} failed with ${changed} confirmed ${noun}(s). The current request may have an unknown outcome; do not retry automatically. ${error instanceof Error ? error.message : "No response."}`, { cause: error });
|
|
1001
|
+
}
|
|
1002
|
+
return true;
|
|
1003
|
+
};
|
|
1004
|
+
/** Archive is the one bound write the CLI also drives; it keeps its own wording. */
|
|
1005
|
+
const archiveBoundTasks = (input) => updateBoundTasks({
|
|
1006
|
+
noun: "archive",
|
|
1007
|
+
send: input.send,
|
|
1008
|
+
token: input.token,
|
|
1009
|
+
updates: input.tasks.map((task) => ({
|
|
1010
|
+
patch: { archivedAt: input.archivedAt },
|
|
1011
|
+
task
|
|
1012
|
+
}))
|
|
1013
|
+
});
|
|
1014
|
+
//#endregion
|
|
921
1015
|
//#region ../../packages/manage-client/src/index.ts
|
|
922
1016
|
const TASKS_QUERY = `
|
|
923
1017
|
query DonebearTasks($first: Int!, $after: String, $workspaceId: ID!) {
|
|
@@ -956,6 +1050,7 @@ const toInstantEpoch = (value) => {
|
|
|
956
1050
|
return Number.isFinite(epoch) ? epoch : null;
|
|
957
1051
|
};
|
|
958
1052
|
const toTaskRecord$1 = (node) => ({
|
|
1053
|
+
agentAssigneeId: node.agentAssigneeId,
|
|
959
1054
|
archivedAt: toInstantEpoch(node.archivedAt),
|
|
960
1055
|
assigneeId: node.assigneeId,
|
|
961
1056
|
completedAt: toInstantEpoch(node.completedAt),
|
|
@@ -969,6 +1064,7 @@ const toTaskRecord$1 = (node) => ({
|
|
|
969
1064
|
projectId: node.projectId,
|
|
970
1065
|
repeatRule: node.repeatRule,
|
|
971
1066
|
repeatTemplateId: node.repeatTemplateId,
|
|
1067
|
+
revision: node.revision,
|
|
972
1068
|
start: node.start,
|
|
973
1069
|
startBucket: node.startBucket,
|
|
974
1070
|
startDate: parseDateOnlyInputToEpoch(node.startDate),
|
|
@@ -1291,7 +1387,7 @@ const WORKSPACE_INVITATION_CREATE_MUTATION = `
|
|
|
1291
1387
|
mutation WorkspaceInvitationCreate($input: WorkspaceInvitationCreateInput!) {
|
|
1292
1388
|
workspaceInvitationCreate(input: $input) {
|
|
1293
1389
|
success lastSyncId
|
|
1294
|
-
invitation { id email code role expiresAt createdAt usedAt usedByUserId workspaceId }
|
|
1390
|
+
invitation { id email code role expiresAt createdAt usedAt usedByUserId workspaceId kind projectId }
|
|
1295
1391
|
}
|
|
1296
1392
|
}
|
|
1297
1393
|
`;
|
|
@@ -1369,6 +1465,7 @@ const toTaskRecord = (value) => {
|
|
|
1369
1465
|
if (typeof row.id !== "string" || typeof row.number !== "number" || typeof row.title !== "string") return null;
|
|
1370
1466
|
if (typeof row.creatorId !== "string" || typeof row.workspaceId !== "string" || typeof row.start !== "string" || typeof row.startBucket !== "string") return null;
|
|
1371
1467
|
return {
|
|
1468
|
+
agentAssigneeId: typeof row.agentAssigneeId === "string" ? row.agentAssigneeId : null,
|
|
1372
1469
|
archivedAt: toNullableNumber(row.archivedAt),
|
|
1373
1470
|
assigneeId: typeof row.assigneeId === "string" ? row.assigneeId : null,
|
|
1374
1471
|
completedAt: toNullableNumber(row.completedAt),
|
|
@@ -1382,6 +1479,7 @@ const toTaskRecord = (value) => {
|
|
|
1382
1479
|
projectId: typeof row.projectId === "string" ? row.projectId : null,
|
|
1383
1480
|
repeatRule: typeof row.repeatRule === "string" ? row.repeatRule : null,
|
|
1384
1481
|
repeatTemplateId: typeof row.repeatTemplateId === "string" ? row.repeatTemplateId : null,
|
|
1482
|
+
revision: typeof row.revision === "number" ? row.revision : 0,
|
|
1385
1483
|
start: row.start,
|
|
1386
1484
|
startBucket: row.startBucket,
|
|
1387
1485
|
startDate: toNullableNumber(row.startDate),
|
|
@@ -1530,12 +1628,14 @@ const toTaskAttachmentRecord = (value) => {
|
|
|
1530
1628
|
};
|
|
1531
1629
|
/** A task's comment thread, oldest first. */
|
|
1532
1630
|
const loadCommentsByTask = async (context, options) => {
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1631
|
+
const url = `${options.baseUrl}/sync/batch`;
|
|
1632
|
+
const body = { requests: [{
|
|
1633
|
+
indexedKey: "taskId",
|
|
1634
|
+
keyValue: options.taskId,
|
|
1635
|
+
modelName: "Comment"
|
|
1636
|
+
}] };
|
|
1637
|
+
const rows = await requestNdjson(context, url, {
|
|
1638
|
+
body,
|
|
1539
1639
|
token: options.token
|
|
1540
1640
|
});
|
|
1541
1641
|
const results = [];
|
|
@@ -1546,12 +1646,14 @@ const loadCommentsByTask = async (context, options) => {
|
|
|
1546
1646
|
return results.toSorted((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
|
|
1547
1647
|
};
|
|
1548
1648
|
const loadReadyTaskAttachmentsByTask = async (context, options) => {
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1649
|
+
const url = `${options.baseUrl}/sync/batch`;
|
|
1650
|
+
const body = { requests: [{
|
|
1651
|
+
indexedKey: "taskId",
|
|
1652
|
+
keyValue: options.taskId,
|
|
1653
|
+
modelName: "TaskAttachment"
|
|
1654
|
+
}] };
|
|
1655
|
+
const rows = await requestNdjson(context, url, {
|
|
1656
|
+
body,
|
|
1555
1657
|
token: options.token
|
|
1556
1658
|
});
|
|
1557
1659
|
const results = [];
|
|
@@ -1564,7 +1666,8 @@ const loadReadyTaskAttachmentsByTask = async (context, options) => {
|
|
|
1564
1666
|
const resolveApiBaseUrl = (context) => {
|
|
1565
1667
|
const fromContext = context.apiUrlOverride?.trim();
|
|
1566
1668
|
if (fromContext) return normalizeApiUrl(fromContext);
|
|
1567
|
-
|
|
1669
|
+
const fromEnv = readEnvValue(DONEBEAR_API_URL_ENV);
|
|
1670
|
+
return normalizeApiUrl(fromEnv ?? "https://api.donebear.com");
|
|
1568
1671
|
};
|
|
1569
1672
|
const mapGraphqlFailure = (failure) => {
|
|
1570
1673
|
if (failure.type === "http") {
|
|
@@ -1600,11 +1703,12 @@ const listWorkspaces = async (_context, options) => await createGraphqlClient({
|
|
|
1600
1703
|
getToken: () => options.token
|
|
1601
1704
|
}).listWorkspaces();
|
|
1602
1705
|
const createWorkspace = async (context, options) => {
|
|
1603
|
-
|
|
1706
|
+
const data = await requestGraphql(context, options.baseUrl, {
|
|
1604
1707
|
query: WORKSPACE_CREATE_MUTATION,
|
|
1605
1708
|
token: options.token,
|
|
1606
1709
|
variables: { input: options.body }
|
|
1607
|
-
})
|
|
1710
|
+
});
|
|
1711
|
+
return toWorkspaceRecordFromGraphqlNode(data.workspaceCreate.workspace);
|
|
1608
1712
|
};
|
|
1609
1713
|
const joinWorkspace = async (context, options) => {
|
|
1610
1714
|
const data = await requestGraphql(context, options.baseUrl, {
|
|
@@ -1634,12 +1738,14 @@ const loadTasksByWorkspaceIds = (context, options) => paginateGraphql(context, {
|
|
|
1634
1738
|
variables: { workspaceIds: options.workspaceIds }
|
|
1635
1739
|
});
|
|
1636
1740
|
const loadTaskById = async (context, options) => {
|
|
1637
|
-
const
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1741
|
+
const url = `${options.baseUrl}/sync/batch`;
|
|
1742
|
+
const body = { requests: [{
|
|
1743
|
+
indexedKey: "id",
|
|
1744
|
+
keyValue: options.taskId,
|
|
1745
|
+
modelName: "Task"
|
|
1746
|
+
}] };
|
|
1747
|
+
const rows = await requestNdjson(context, url, {
|
|
1748
|
+
body,
|
|
1643
1749
|
token: options.token
|
|
1644
1750
|
});
|
|
1645
1751
|
for (const row of rows) {
|
|
@@ -1697,12 +1803,14 @@ const loadTeamsByWorkspaceIds = (context, options) => paginateGraphql(context, {
|
|
|
1697
1803
|
variables: { workspaceIds: options.workspaceIds }
|
|
1698
1804
|
});
|
|
1699
1805
|
const loadChecklistItemsByTask = async (context, options) => {
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1806
|
+
const url = `${options.baseUrl}/sync/batch`;
|
|
1807
|
+
const body = { requests: [{
|
|
1808
|
+
indexedKey: "taskId",
|
|
1809
|
+
keyValue: options.taskId,
|
|
1810
|
+
modelName: "TaskChecklistItem"
|
|
1811
|
+
}] };
|
|
1812
|
+
const rows = await requestNdjson(context, url, {
|
|
1813
|
+
body,
|
|
1706
1814
|
token: options.token
|
|
1707
1815
|
});
|
|
1708
1816
|
const results = [];
|
|
@@ -1722,7 +1830,10 @@ const mapSyncMutateFailure = (failure) => {
|
|
|
1722
1830
|
const exitCode = status === 401 || status === 403 ? EXIT_CODES.AUTH_REQUIRED : EXIT_CODES.ERROR;
|
|
1723
1831
|
return new CliError(extractErrorMessage(body), exitCode);
|
|
1724
1832
|
}
|
|
1725
|
-
if (failure.type === "mutation")
|
|
1833
|
+
if (failure.type === "mutation") {
|
|
1834
|
+
const result = failure.response.results.find((item) => !item.success);
|
|
1835
|
+
return new SyncMutationRejectedError(result?.error ?? "Sync mutation failed", failure.response.results);
|
|
1836
|
+
}
|
|
1726
1837
|
if (failure.type === "timeout") return new CliError(`Sync mutation timed out after ${failure.timeoutMs}ms`, EXIT_CODES.ERROR);
|
|
1727
1838
|
if (failure.cause instanceof Error) return failure.cause;
|
|
1728
1839
|
return new CliError("Sync mutation request failed", EXIT_CODES.ERROR);
|
|
@@ -1766,14 +1877,17 @@ const listWorkspaceInvitations = async (context, options) => await paginateGraph
|
|
|
1766
1877
|
const createWorkspaceInvitation = async (context, options) => {
|
|
1767
1878
|
const input = omitUndefined({
|
|
1768
1879
|
email: options.email,
|
|
1880
|
+
kind: options.kind,
|
|
1881
|
+
projectId: options.projectId,
|
|
1769
1882
|
role: options.role,
|
|
1770
1883
|
workspaceId: options.workspaceId
|
|
1771
1884
|
});
|
|
1772
|
-
|
|
1885
|
+
const data = await requestGraphql(context, options.baseUrl, {
|
|
1773
1886
|
query: WORKSPACE_INVITATION_CREATE_MUTATION,
|
|
1774
1887
|
token: options.token,
|
|
1775
1888
|
variables: { input }
|
|
1776
|
-
})
|
|
1889
|
+
});
|
|
1890
|
+
return toWorkspaceInvitationRecordFromGraphqlNode(data.workspaceInvitationCreate.invitation);
|
|
1777
1891
|
};
|
|
1778
1892
|
const listWorkspaceHistory = async (context, options) => {
|
|
1779
1893
|
const variables = omitUndefined({
|
|
@@ -1853,6 +1967,56 @@ const requireUserId = async (context, commandContext) => {
|
|
|
1853
1967
|
token: commandContext.token
|
|
1854
1968
|
})).id;
|
|
1855
1969
|
};
|
|
1970
|
+
const packageManifest = createRequire(import.meta.url)("../package.json");
|
|
1971
|
+
//#endregion
|
|
1972
|
+
//#region src/crash-report.ts
|
|
1973
|
+
const POSTHOG_HOST = "https://us.i.posthog.com";
|
|
1974
|
+
const SHUTDOWN_TIMEOUT_MS = 2e3;
|
|
1975
|
+
const buildKey = () => "";
|
|
1976
|
+
const hasNodeErrorCode = (error) => [error, error.cause].some((candidate) => typeof candidate === "object" && candidate !== null && "code" in candidate && typeof candidate.code === "string");
|
|
1977
|
+
const isCliBug = (error) => (error instanceof TypeError || error instanceof ReferenceError || error instanceof RangeError) && !hasNodeErrorCode(error);
|
|
1978
|
+
const crashReportingEnabled = (key, env) => key.length > 0 && ![
|
|
1979
|
+
"1",
|
|
1980
|
+
"true",
|
|
1981
|
+
"yes"
|
|
1982
|
+
].includes(env.DO_NOT_TRACK?.trim().toLowerCase() ?? "");
|
|
1983
|
+
const REDACTED_MESSAGE = "[message not sent]";
|
|
1984
|
+
const WINDOWS_PATH = /^[A-Za-z]:\\/u;
|
|
1985
|
+
/** The report as sent: home paths cut to `~`, every exception message gone. */
|
|
1986
|
+
const redactCrashReport = (event, home) => {
|
|
1987
|
+
if (!event) return event;
|
|
1988
|
+
const forms = home ? [.../* @__PURE__ */ new Set([
|
|
1989
|
+
home,
|
|
1990
|
+
home.replaceAll("\\", "/"),
|
|
1991
|
+
pathToFileURL(home, { windows: WINDOWS_PATH.test(home) }).pathname
|
|
1992
|
+
])] : [];
|
|
1993
|
+
let json = JSON.stringify(event);
|
|
1994
|
+
for (const form of forms) json = json.replaceAll(JSON.stringify(form).slice(1, -1), "~");
|
|
1995
|
+
const redacted = JSON.parse(json);
|
|
1996
|
+
const exceptions = redacted.properties?.$exception_list;
|
|
1997
|
+
for (const exception of exceptions ?? []) exception.value = REDACTED_MESSAGE;
|
|
1998
|
+
return redacted;
|
|
1999
|
+
};
|
|
2000
|
+
const reportCrash = async (error) => {
|
|
2001
|
+
const key = buildKey();
|
|
2002
|
+
if (!(isCliBug(error) && crashReportingEnabled(key, process.env))) return;
|
|
2003
|
+
try {
|
|
2004
|
+
const { PostHog } = await import("posthog-node");
|
|
2005
|
+
const home = homedir();
|
|
2006
|
+
const client = new PostHog(key, {
|
|
2007
|
+
before_send: (event) => redactCrashReport(event, home),
|
|
2008
|
+
disableGeoip: true,
|
|
2009
|
+
flushAt: 1,
|
|
2010
|
+
host: POSTHOG_HOST
|
|
2011
|
+
});
|
|
2012
|
+
client.captureException(error, void 0, {
|
|
2013
|
+
cli_version: packageManifest.version,
|
|
2014
|
+
node_version: process.version,
|
|
2015
|
+
os_platform: process.platform
|
|
2016
|
+
});
|
|
2017
|
+
await client.shutdown(SHUTDOWN_TIMEOUT_MS);
|
|
2018
|
+
} catch {}
|
|
2019
|
+
};
|
|
1856
2020
|
//#endregion
|
|
1857
2021
|
//#region src/options.ts
|
|
1858
2022
|
const parseOutputFormat = (value) => {
|
|
@@ -1942,6 +2106,7 @@ const runWithErrorHandling = async (context, action) => {
|
|
|
1942
2106
|
}
|
|
1943
2107
|
if (context.debug && error instanceof Error && typeof error.stack === "string") writeDebug(context, error.stack);
|
|
1944
2108
|
process.exitCode = cliError.exitCode;
|
|
2109
|
+
if (cliError.exitCode === EXIT_CODES.ERROR) await reportCrash(error);
|
|
1945
2110
|
}
|
|
1946
2111
|
};
|
|
1947
2112
|
//#endregion
|
|
@@ -2023,8 +2188,9 @@ const runCreate = async (context, label, options) => {
|
|
|
2023
2188
|
};
|
|
2024
2189
|
const runList = async (context) => {
|
|
2025
2190
|
const { token } = await requireAuthToken(context);
|
|
2191
|
+
const baseUrl = resolveApiBaseUrl(context);
|
|
2026
2192
|
const keys = await listApiKeys(context, {
|
|
2027
|
-
baseUrl
|
|
2193
|
+
baseUrl,
|
|
2028
2194
|
token
|
|
2029
2195
|
});
|
|
2030
2196
|
if (context.json) {
|
|
@@ -2046,8 +2212,9 @@ const runList = async (context) => {
|
|
|
2046
2212
|
};
|
|
2047
2213
|
const runRevoke = async (context, keyId) => {
|
|
2048
2214
|
const { token } = await requireAuthToken(context);
|
|
2215
|
+
const baseUrl = resolveApiBaseUrl(context);
|
|
2049
2216
|
await revokeApiKey(context, {
|
|
2050
|
-
baseUrl
|
|
2217
|
+
baseUrl,
|
|
2051
2218
|
keyId,
|
|
2052
2219
|
token
|
|
2053
2220
|
});
|
|
@@ -2231,9 +2398,187 @@ const createOAuthState = () => randomBytes(24).toString("hex");
|
|
|
2231
2398
|
const createCodeVerifier = () => randomBytes(64).toString("base64url");
|
|
2232
2399
|
const createCodeChallenge = (verifier) => createHash("sha256").update(verifier).digest().toString("base64url");
|
|
2233
2400
|
//#endregion
|
|
2401
|
+
//#region src/api/agent-signup.ts
|
|
2402
|
+
const parseJson = async (response) => {
|
|
2403
|
+
const raw = await response.text();
|
|
2404
|
+
if (raw.length === 0) return null;
|
|
2405
|
+
try {
|
|
2406
|
+
return JSON.parse(raw);
|
|
2407
|
+
} catch {
|
|
2408
|
+
return raw;
|
|
2409
|
+
}
|
|
2410
|
+
};
|
|
2411
|
+
const throwForFailure = (status, payload) => {
|
|
2412
|
+
const message = typeof payload === "string" && payload.length > 0 ? payload : extractErrorMessage(payload);
|
|
2413
|
+
const record = typeof payload === "object" && payload !== null ? payload : {};
|
|
2414
|
+
const code = typeof record.code === "string" ? record.code : null;
|
|
2415
|
+
if (status === 409 || code === "DUPLICATE_EMAIL") throw new CliError(message, EXIT_CODES.AUTH_REQUIRED, "Run `donebear auth login` with this email. Do not start another signup.");
|
|
2416
|
+
if (status === 429 || code === "RATE_LIMIT_EXCEEDED") throw new CliError(message, EXIT_CODES.ERROR, "Wait, then retry.");
|
|
2417
|
+
if (status === 400 || code === "INVALID_REQUEST") throw new CliError(message, EXIT_CODES.VALIDATION);
|
|
2418
|
+
throw new CliError(message, EXIT_CODES.ERROR);
|
|
2419
|
+
};
|
|
2420
|
+
const request = async (endpoint, options) => {
|
|
2421
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
2422
|
+
if (options.token) headers.set("Authorization", `Bearer ${options.token}`);
|
|
2423
|
+
if (options.body !== void 0) headers.set("Content-Type", "application/json");
|
|
2424
|
+
let response;
|
|
2425
|
+
try {
|
|
2426
|
+
response = await fetch(endpoint, {
|
|
2427
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
2428
|
+
headers,
|
|
2429
|
+
method: options.method ?? "GET"
|
|
2430
|
+
});
|
|
2431
|
+
} catch (error) {
|
|
2432
|
+
throw new CliError("Cannot connect to Done Bear API.", EXIT_CODES.NETWORK, error instanceof Error ? error.message : void 0);
|
|
2433
|
+
}
|
|
2434
|
+
const payload = await parseJson(response);
|
|
2435
|
+
if (!response.ok) throwForFailure(response.status, payload);
|
|
2436
|
+
return payload;
|
|
2437
|
+
};
|
|
2438
|
+
const startAgentSignup = async (_context, apiBaseUrl, input) => {
|
|
2439
|
+
return await request(`${apiBaseUrl}/api/agent-signup`, {
|
|
2440
|
+
body: {
|
|
2441
|
+
email: input.email,
|
|
2442
|
+
...input.name ? { name: input.name } : {}
|
|
2443
|
+
},
|
|
2444
|
+
method: "POST"
|
|
2445
|
+
});
|
|
2446
|
+
};
|
|
2447
|
+
const pollAgentSignup = async (_context, apiBaseUrl, pollToken) => {
|
|
2448
|
+
return await request(`${apiBaseUrl}/api/agent-signup/poll`, {
|
|
2449
|
+
method: "GET",
|
|
2450
|
+
token: pollToken
|
|
2451
|
+
});
|
|
2452
|
+
};
|
|
2453
|
+
const verifyAgentSignupCode = async (_context, apiBaseUrl, input) => {
|
|
2454
|
+
return await request(`${apiBaseUrl}/api/agent-signup/verify`, {
|
|
2455
|
+
body: input,
|
|
2456
|
+
method: "POST"
|
|
2457
|
+
});
|
|
2458
|
+
};
|
|
2459
|
+
//#endregion
|
|
2460
|
+
//#region src/commands/auth-signup.ts
|
|
2461
|
+
const MISSING_EMAIL_MESSAGE = "Pass --email with the human's address. Do not invent one.";
|
|
2462
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
2463
|
+
const defaultClock = {
|
|
2464
|
+
now: () => Date.now(),
|
|
2465
|
+
sleep: (ms, signal) => new Promise((resolve, reject) => {
|
|
2466
|
+
if (signal.aborted) {
|
|
2467
|
+
resolve();
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
const timer = setTimeout(resolve, ms);
|
|
2471
|
+
signal.addEventListener("abort", () => {
|
|
2472
|
+
clearTimeout(timer);
|
|
2473
|
+
reject(/* @__PURE__ */ new Error("aborted"));
|
|
2474
|
+
}, { once: true });
|
|
2475
|
+
})
|
|
2476
|
+
};
|
|
2477
|
+
const isComplete = (value) => value.status === "complete";
|
|
2478
|
+
const printWaiting = (context, verifyUrl, instructions) => {
|
|
2479
|
+
writeWarning(context, instructions.agent);
|
|
2480
|
+
writeWarning(context, instructions.human);
|
|
2481
|
+
writeWarning(context, `Verify link: ${verifyUrl}`);
|
|
2482
|
+
writeWarning(context, "Waiting for the human to verify. The CLI will continue once that happens.");
|
|
2483
|
+
};
|
|
2484
|
+
const sessionFromComplete = (complete) => ({
|
|
2485
|
+
accessToken: complete.apiKey.key,
|
|
2486
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2487
|
+
expiresAt: null,
|
|
2488
|
+
provider: "oauth2",
|
|
2489
|
+
refreshToken: null,
|
|
2490
|
+
tokenType: "bearer",
|
|
2491
|
+
user: {
|
|
2492
|
+
email: complete.user.email,
|
|
2493
|
+
id: complete.user.id
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
const printComplete = (context, complete, verifyUrl) => {
|
|
2497
|
+
if (context.json) {
|
|
2498
|
+
writeJson({
|
|
2499
|
+
apiKey: complete.apiKey,
|
|
2500
|
+
email: complete.email,
|
|
2501
|
+
instructions: {
|
|
2502
|
+
agent: "Store the API key as DONEBEAR_TOKEN. Paste mcp.cursor into the MCP config. Do not log the full key.",
|
|
2503
|
+
human: "Verification is done."
|
|
2504
|
+
},
|
|
2505
|
+
mcp: complete.mcp,
|
|
2506
|
+
ok: true,
|
|
2507
|
+
sessionStored: true,
|
|
2508
|
+
status: "complete",
|
|
2509
|
+
user: complete.user,
|
|
2510
|
+
verifyUrl,
|
|
2511
|
+
workspace: complete.workspace
|
|
2512
|
+
});
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
writeSuccess(context, "Signup complete. API key stored in the CLI session.");
|
|
2516
|
+
writeInfo(context, `Signed in as ${complete.user.email}`);
|
|
2517
|
+
writeInfo(context, `Workspace: ${complete.workspace.name} (${complete.workspace.urlKey ?? complete.workspace.id})`);
|
|
2518
|
+
writeInfo(context, "API key (shown once):");
|
|
2519
|
+
writeInfo(context, complete.apiKey.key);
|
|
2520
|
+
writeInfo(context, "Paste-ready MCP config for Cursor:");
|
|
2521
|
+
writeInfo(context, JSON.stringify(complete.mcp.cursor, null, 2));
|
|
2522
|
+
writeInfo(context, complete.mcp.claude);
|
|
2523
|
+
};
|
|
2524
|
+
const runAgentSignup = async (context, options, clock = defaultClock) => {
|
|
2525
|
+
const email = options.email?.trim() ?? "";
|
|
2526
|
+
if (email.length === 0) throw new CliError(MISSING_EMAIL_MESSAGE, EXIT_CODES.VALIDATION, "Example: donebear auth signup --email person@example.com");
|
|
2527
|
+
const apiBaseUrl = resolveApiBaseUrl(context);
|
|
2528
|
+
const started = await startAgentSignup(context, apiBaseUrl, {
|
|
2529
|
+
email,
|
|
2530
|
+
name: options.name?.trim() || void 0
|
|
2531
|
+
});
|
|
2532
|
+
printWaiting(context, started.verifyUrl, started.instructions);
|
|
2533
|
+
const abort = new AbortController();
|
|
2534
|
+
const canPrompt = options.interactive && process.stdin.isTTY && !context.json && !context.quiet;
|
|
2535
|
+
let submittedCode = null;
|
|
2536
|
+
const promptTask = canPrompt ? promptForValue(context, "6-digit email code", abort.signal).then((value) => {
|
|
2537
|
+
submittedCode = value;
|
|
2538
|
+
}) : Promise.resolve();
|
|
2539
|
+
const deadline = clock.now() + options.timeout * 1e3;
|
|
2540
|
+
try {
|
|
2541
|
+
while (clock.now() < deadline) {
|
|
2542
|
+
if (submittedCode) {
|
|
2543
|
+
const verified = await verifyAgentSignupCode(context, apiBaseUrl, {
|
|
2544
|
+
code: submittedCode,
|
|
2545
|
+
pollToken: started.pollToken
|
|
2546
|
+
});
|
|
2547
|
+
if (isComplete(verified)) {
|
|
2548
|
+
abort.abort();
|
|
2549
|
+
await writeStoredAuthSession(sessionFromComplete(verified));
|
|
2550
|
+
printComplete(context, verified, started.verifyUrl);
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
if (verified.status === "consumed") throw new CliError(verified.message, EXIT_CODES.ERROR);
|
|
2554
|
+
submittedCode = null;
|
|
2555
|
+
}
|
|
2556
|
+
const polled = await pollAgentSignup(context, apiBaseUrl, started.pollToken);
|
|
2557
|
+
if (isComplete(polled)) {
|
|
2558
|
+
abort.abort();
|
|
2559
|
+
await writeStoredAuthSession(sessionFromComplete(polled));
|
|
2560
|
+
printComplete(context, polled, started.verifyUrl);
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
if (polled.status === "consumed") throw new CliError(polled.message, EXIT_CODES.ERROR);
|
|
2564
|
+
try {
|
|
2565
|
+
await clock.sleep(POLL_INTERVAL_MS, abort.signal);
|
|
2566
|
+
} catch {
|
|
2567
|
+
break;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
} finally {
|
|
2571
|
+
abort.abort();
|
|
2572
|
+
await promptTask.catch(() => void 0);
|
|
2573
|
+
}
|
|
2574
|
+
throw new CliError("Signup timed out before the email was verified.", EXIT_CODES.ERROR, "Ask the human to open the verify link, then run this command again if the code expired.");
|
|
2575
|
+
};
|
|
2576
|
+
const parseSignupTimeout = (value) => parsePositiveInteger(value, "Timeout");
|
|
2577
|
+
//#endregion
|
|
2234
2578
|
//#region src/commands/auth.ts
|
|
2235
2579
|
const runLogin = async (context, options) => {
|
|
2236
|
-
const
|
|
2580
|
+
const config = resolveSupabaseConfig();
|
|
2581
|
+
const { authorizeUrl, tokenUrl } = buildOAuthUrls(config);
|
|
2237
2582
|
const clientId = resolveOAuthClientId();
|
|
2238
2583
|
const redirectUrl = new URL(`http://127.0.0.1:${options.port}${DEFAULT_OAUTH_CALLBACK_PATH}`);
|
|
2239
2584
|
const state = createOAuthState();
|
|
@@ -2262,10 +2607,11 @@ const runLogin = async (context, options) => {
|
|
|
2262
2607
|
writeInfo(context, authUrlString);
|
|
2263
2608
|
}
|
|
2264
2609
|
const authorizationCode = await callbackPromise;
|
|
2265
|
-
const
|
|
2610
|
+
const tokenResponse = await exchangeAuthorizationCode({
|
|
2266
2611
|
clientId,
|
|
2267
2612
|
tokenUrl
|
|
2268
|
-
}, authorizationCode, codeVerifier, redirectUrl.toString())
|
|
2613
|
+
}, authorizationCode, codeVerifier, redirectUrl.toString());
|
|
2614
|
+
const storedSession = tokenResponseToStoredSession(tokenResponse);
|
|
2269
2615
|
await writeStoredAuthSession(storedSession);
|
|
2270
2616
|
if (context.json) {
|
|
2271
2617
|
writeJson({
|
|
@@ -2280,7 +2626,8 @@ const runLogin = async (context, options) => {
|
|
|
2280
2626
|
if (storedSession.user?.email) writeInfo(context, `Signed in as ${storedSession.user.email}`);
|
|
2281
2627
|
};
|
|
2282
2628
|
const runStatus = async (context) => {
|
|
2283
|
-
const
|
|
2629
|
+
const config = getOptionalSupabaseConfig();
|
|
2630
|
+
const token = await resolveAuthToken(context, config);
|
|
2284
2631
|
if (!token) {
|
|
2285
2632
|
if (context.json) writeJson({
|
|
2286
2633
|
authenticated: false,
|
|
@@ -2333,6 +2680,7 @@ const getOptionalSupabaseConfig = () => {
|
|
|
2333
2680
|
const registerAuthCommands = (program) => {
|
|
2334
2681
|
const auth = program.command("auth").description("Authenticate donebear CLI").addHelpText("after", `
|
|
2335
2682
|
Examples:
|
|
2683
|
+
donebear auth signup --email person@example.com --json
|
|
2336
2684
|
donebear auth login
|
|
2337
2685
|
donebear auth login --no-open (print URL, don't open browser)
|
|
2338
2686
|
donebear auth status --json
|
|
@@ -2361,6 +2709,25 @@ Non-interactive / CI use:
|
|
|
2361
2709
|
await runLogin(context, options);
|
|
2362
2710
|
});
|
|
2363
2711
|
});
|
|
2712
|
+
auth.command("signup").description("Create a Done Bear account from a human email (agent signup)").option("--email <email>", "Human email to verify. Do not invent one.").option("--name <name>", "Optional display name").option("--timeout <seconds>", "Seconds to wait for email verification", parseSignupTimeout, 600).option("--no-interactive", "Never prompt for the email code; poll the verify link instead").addHelpText("after", `
|
|
2713
|
+
Creates a real human account. One person must verify the email (code or link).
|
|
2714
|
+
Do not invent an email address. If the address is already registered, run
|
|
2715
|
+
\`donebear auth login\` instead.
|
|
2716
|
+
|
|
2717
|
+
Examples:
|
|
2718
|
+
donebear auth signup --email person@example.com
|
|
2719
|
+
donebear auth signup --email person@example.com --json --no-interactive
|
|
2720
|
+
`).action(async (options, command) => {
|
|
2721
|
+
const context = contextFromCommand(command);
|
|
2722
|
+
await runWithErrorHandling(context, async () => {
|
|
2723
|
+
await runAgentSignup(context, {
|
|
2724
|
+
email: options.email,
|
|
2725
|
+
interactive: options.interactive !== false,
|
|
2726
|
+
name: options.name,
|
|
2727
|
+
timeout: options.timeout
|
|
2728
|
+
});
|
|
2729
|
+
});
|
|
2730
|
+
});
|
|
2364
2731
|
auth.command("status").description("Check authentication status").action(async (_options, command) => {
|
|
2365
2732
|
const context = contextFromCommand(command);
|
|
2366
2733
|
await runWithErrorHandling(context, async () => {
|
|
@@ -2418,7 +2785,7 @@ const parseTaskNumberRef = (value) => {
|
|
|
2418
2785
|
const normalized = value.trim();
|
|
2419
2786
|
if (!/^[1-9]\d*$/u.test(normalized)) throw new Error(`Invalid task number "${normalized}". Use the positive number shown on the task.`);
|
|
2420
2787
|
const number = Number(normalized);
|
|
2421
|
-
if (!Number.isSafeInteger(number) || number > MAX_TASK_NUMBER) throw new
|
|
2788
|
+
if (!Number.isSafeInteger(number) || number > MAX_TASK_NUMBER) throw new Error(`Invalid task number "${normalized}". Number is too large.`);
|
|
2422
2789
|
return number;
|
|
2423
2790
|
};
|
|
2424
2791
|
const parseTaskState = (value) => {
|
|
@@ -2487,29 +2854,29 @@ const confirmAction = (context, message) => {
|
|
|
2487
2854
|
});
|
|
2488
2855
|
};
|
|
2489
2856
|
//#endregion
|
|
2490
|
-
//#region ../../node_modules/uuid/dist-node/regex.js
|
|
2857
|
+
//#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/regex.js
|
|
2491
2858
|
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;
|
|
2492
2859
|
//#endregion
|
|
2493
|
-
//#region ../../node_modules/uuid/dist-node/validate.js
|
|
2860
|
+
//#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/validate.js
|
|
2494
2861
|
function validate(uuid) {
|
|
2495
2862
|
return typeof uuid === "string" && regex_default.test(uuid);
|
|
2496
2863
|
}
|
|
2497
2864
|
//#endregion
|
|
2498
|
-
//#region ../../node_modules/uuid/dist-node/parse.js
|
|
2865
|
+
//#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/parse.js
|
|
2499
2866
|
function parse(uuid) {
|
|
2500
2867
|
if (!validate(uuid)) throw TypeError("Invalid UUID");
|
|
2501
2868
|
let v;
|
|
2502
2869
|
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);
|
|
2503
2870
|
}
|
|
2504
2871
|
//#endregion
|
|
2505
|
-
//#region ../../node_modules/uuid/dist-node/stringify.js
|
|
2872
|
+
//#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/stringify.js
|
|
2506
2873
|
const byteToHex = [];
|
|
2507
2874
|
for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
|
|
2508
2875
|
function unsafeStringify(arr, offset = 0) {
|
|
2509
2876
|
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();
|
|
2510
2877
|
}
|
|
2511
2878
|
//#endregion
|
|
2512
|
-
//#region ../../node_modules/uuid/dist-node/v35.js
|
|
2879
|
+
//#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/v35.js
|
|
2513
2880
|
function stringToBytes(str) {
|
|
2514
2881
|
str = unescape(encodeURIComponent(str));
|
|
2515
2882
|
const bytes = new Uint8Array(str.length);
|
|
@@ -2538,14 +2905,14 @@ function v35(version, hash, value, namespace, buf, offset) {
|
|
|
2538
2905
|
return unsafeStringify(bytes);
|
|
2539
2906
|
}
|
|
2540
2907
|
//#endregion
|
|
2541
|
-
//#region ../../node_modules/uuid/dist-node/sha1.js
|
|
2908
|
+
//#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/sha1.js
|
|
2542
2909
|
function sha1(bytes) {
|
|
2543
2910
|
if (Array.isArray(bytes)) bytes = Buffer.from(bytes);
|
|
2544
2911
|
else if (typeof bytes === "string") bytes = Buffer.from(bytes, "utf8");
|
|
2545
2912
|
return createHash("sha1").update(bytes).digest();
|
|
2546
2913
|
}
|
|
2547
2914
|
//#endregion
|
|
2548
|
-
//#region ../../node_modules/uuid/dist-node/v5.js
|
|
2915
|
+
//#region ../../node_modules/.pnpm/uuid@14.0.2/node_modules/uuid/dist-node/v5.js
|
|
2549
2916
|
function v5(value, namespace, buf, offset) {
|
|
2550
2917
|
return v35(80, sha1, value, namespace, buf, offset);
|
|
2551
2918
|
}
|
|
@@ -2566,6 +2933,11 @@ const createTeamProjectId = (params) => createCompositeSyncId("TeamProject", [
|
|
|
2566
2933
|
params.teamId,
|
|
2567
2934
|
params.projectId
|
|
2568
2935
|
]);
|
|
2936
|
+
const createTeamMembershipId = (params) => createCompositeSyncId("TeamMembership", [
|
|
2937
|
+
params.workspaceId,
|
|
2938
|
+
params.teamId,
|
|
2939
|
+
params.userId
|
|
2940
|
+
]);
|
|
2569
2941
|
const createProjectMembershipId = (params) => createCompositeSyncId("ProjectMembership", [
|
|
2570
2942
|
params.workspaceId,
|
|
2571
2943
|
params.projectId,
|
|
@@ -2651,7 +3023,7 @@ const resolveWorkspace = async (context, options) => {
|
|
|
2651
3023
|
//#region src/commands/task.ts
|
|
2652
3024
|
const DEFAULT_TASK_LIST_LIMIT = 20;
|
|
2653
3025
|
/** The views `--when` can place a task in: the ones with start fields. */
|
|
2654
|
-
const ACTIVE_TASK_VIEWS = new Set([
|
|
3026
|
+
const ACTIVE_TASK_VIEWS = /* @__PURE__ */ new Set([
|
|
2655
3027
|
"inbox",
|
|
2656
3028
|
"anytime",
|
|
2657
3029
|
"today",
|
|
@@ -2659,7 +3031,7 @@ const ACTIVE_TASK_VIEWS = new Set([
|
|
|
2659
3031
|
"someday"
|
|
2660
3032
|
]);
|
|
2661
3033
|
/** The views `--view` can filter a list by, which adds the two read-only ones. */
|
|
2662
|
-
const VALID_TASK_VIEWS = new Set([
|
|
3034
|
+
const VALID_TASK_VIEWS = /* @__PURE__ */ new Set([
|
|
2663
3035
|
...ACTIVE_TASK_VIEWS,
|
|
2664
3036
|
"logbook",
|
|
2665
3037
|
"trash"
|
|
@@ -2949,7 +3321,18 @@ const runTaskMutation = async (context, taskRef, payload) => {
|
|
|
2949
3321
|
taskRef,
|
|
2950
3322
|
workspaceRef: payload.workspaceRef
|
|
2951
3323
|
});
|
|
2952
|
-
|
|
3324
|
+
if (!(payload.action === "ARCHIVE" && !context.dryRun && await archiveBoundTasks({
|
|
3325
|
+
archivedAt: Number(payload.data.archivedAt),
|
|
3326
|
+
send: (path, body) => requestJson(context, `${commandContext.apiBaseUrl}${path}`, {
|
|
3327
|
+
token: commandContext.token,
|
|
3328
|
+
...body === void 0 ? {} : {
|
|
3329
|
+
body,
|
|
3330
|
+
method: "POST"
|
|
3331
|
+
}
|
|
3332
|
+
}),
|
|
3333
|
+
tasks: [task],
|
|
3334
|
+
token: commandContext.token
|
|
3335
|
+
}))) await mutateModel(context, {
|
|
2953
3336
|
baseUrl: commandContext.apiBaseUrl,
|
|
2954
3337
|
request: {
|
|
2955
3338
|
batchId: randomUUID(),
|
|
@@ -2959,7 +3342,10 @@ const runTaskMutation = async (context, taskRef, payload) => {
|
|
|
2959
3342
|
clientTxId: randomUUID(),
|
|
2960
3343
|
modelId: task.id,
|
|
2961
3344
|
modelName: "Task",
|
|
2962
|
-
payload:
|
|
3345
|
+
payload: {
|
|
3346
|
+
...payload.data,
|
|
3347
|
+
...task.agentAssigneeId ? { expectedRevision: task.revision } : {}
|
|
3348
|
+
}
|
|
2963
3349
|
}]
|
|
2964
3350
|
},
|
|
2965
3351
|
token: commandContext.token
|
|
@@ -2989,11 +3375,12 @@ const resolveProjectId = async (context, commandContext, projectRef, workspaceRe
|
|
|
2989
3375
|
token: commandContext.token,
|
|
2990
3376
|
workspaceRef
|
|
2991
3377
|
});
|
|
2992
|
-
|
|
3378
|
+
const projects = await loadProjectsByWorkspace(context, {
|
|
2993
3379
|
baseUrl: commandContext.apiBaseUrl,
|
|
2994
3380
|
token: commandContext.token,
|
|
2995
3381
|
workspaceId: workspace.id
|
|
2996
|
-
})
|
|
3382
|
+
});
|
|
3383
|
+
return resolveEntityId(projects, projectRef, "Project");
|
|
2997
3384
|
};
|
|
2998
3385
|
const resolveTeamId = async (context, commandContext, teamRef, workspaceRef) => {
|
|
2999
3386
|
const { workspace } = await resolveWorkspace(context, {
|
|
@@ -3002,11 +3389,12 @@ const resolveTeamId = async (context, commandContext, teamRef, workspaceRef) =>
|
|
|
3002
3389
|
token: commandContext.token,
|
|
3003
3390
|
workspaceRef
|
|
3004
3391
|
});
|
|
3005
|
-
|
|
3392
|
+
const teams = await loadTeamsByWorkspace(context, {
|
|
3006
3393
|
baseUrl: commandContext.apiBaseUrl,
|
|
3007
3394
|
token: commandContext.token,
|
|
3008
3395
|
workspaceId: workspace.id
|
|
3009
|
-
})
|
|
3396
|
+
});
|
|
3397
|
+
return resolveEntityId(teams, teamRef, "Team");
|
|
3010
3398
|
};
|
|
3011
3399
|
const validateTaskEditOptions = (options) => {
|
|
3012
3400
|
if (options.clearDescription && typeof options.description === "string") throw new Error("Use either --description or --clear-description, not both.");
|
|
@@ -3049,8 +3437,9 @@ const runTaskEdit = async (context, taskRef, options, workspaceOverride) => {
|
|
|
3049
3437
|
});
|
|
3050
3438
|
};
|
|
3051
3439
|
const runTaskRead = async (context, taskRef, options, workspaceOverride) => {
|
|
3440
|
+
const commandContext = await resolveCommandContext(context);
|
|
3052
3441
|
const task = await resolveTaskReference(context, {
|
|
3053
|
-
commandContext
|
|
3442
|
+
commandContext,
|
|
3054
3443
|
taskRef,
|
|
3055
3444
|
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
3056
3445
|
});
|
|
@@ -3065,14 +3454,16 @@ const runTaskRead = async (context, taskRef, options, workspaceOverride) => {
|
|
|
3065
3454
|
writeInfo(context, task.description ?? "(empty)");
|
|
3066
3455
|
};
|
|
3067
3456
|
const runTaskAppend = async (context, taskRef, text, options, workspaceOverride) => {
|
|
3457
|
+
const commandContext = await resolveCommandContext(context);
|
|
3458
|
+
const newDesc = `${(await resolveTaskReference(context, {
|
|
3459
|
+
commandContext,
|
|
3460
|
+
taskRef,
|
|
3461
|
+
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
3462
|
+
})).description ?? ""}\n${text.trim()}`;
|
|
3068
3463
|
await runTaskMutation(context, taskRef, {
|
|
3069
3464
|
action: "UPDATE",
|
|
3070
3465
|
data: {
|
|
3071
|
-
description:
|
|
3072
|
-
commandContext: await resolveCommandContext(context),
|
|
3073
|
-
taskRef,
|
|
3074
|
-
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
3075
|
-
})).description ?? ""}\n${text.trim()}`,
|
|
3466
|
+
description: newDesc,
|
|
3076
3467
|
updatedAt: Date.now()
|
|
3077
3468
|
},
|
|
3078
3469
|
successMessage: "Appended",
|
|
@@ -3080,15 +3471,17 @@ const runTaskAppend = async (context, taskRef, text, options, workspaceOverride)
|
|
|
3080
3471
|
});
|
|
3081
3472
|
};
|
|
3082
3473
|
const runTaskPrepend = async (context, taskRef, text, options, workspaceOverride) => {
|
|
3474
|
+
const commandContext = await resolveCommandContext(context);
|
|
3083
3475
|
const existing = (await resolveTaskReference(context, {
|
|
3084
|
-
commandContext
|
|
3476
|
+
commandContext,
|
|
3085
3477
|
taskRef,
|
|
3086
3478
|
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
3087
3479
|
})).description ?? "";
|
|
3480
|
+
const newDesc = existing.length > 0 ? `${text.trim()}\n${existing}` : text.trim();
|
|
3088
3481
|
await runTaskMutation(context, taskRef, {
|
|
3089
3482
|
action: "UPDATE",
|
|
3090
3483
|
data: {
|
|
3091
|
-
description:
|
|
3484
|
+
description: newDesc,
|
|
3092
3485
|
updatedAt: Date.now()
|
|
3093
3486
|
},
|
|
3094
3487
|
successMessage: "Prepended",
|
|
@@ -3104,11 +3497,12 @@ const runTaskRandom = async (context, options, workspaceOverride) => {
|
|
|
3104
3497
|
token: commandContext.token,
|
|
3105
3498
|
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
3106
3499
|
});
|
|
3107
|
-
const
|
|
3500
|
+
const tasks = await loadTasksByWorkspace(context, {
|
|
3108
3501
|
baseUrl: commandContext.apiBaseUrl,
|
|
3109
3502
|
token: commandContext.token,
|
|
3110
3503
|
workspaceId: workspace.id
|
|
3111
|
-
})
|
|
3504
|
+
});
|
|
3505
|
+
const filtered = filterTasks(tasks, {
|
|
3112
3506
|
search: null,
|
|
3113
3507
|
state: requestedState,
|
|
3114
3508
|
view: null
|
|
@@ -3188,7 +3582,10 @@ const runChecklistList = async (context, taskId, workspaceOption) => {
|
|
|
3188
3582
|
return;
|
|
3189
3583
|
}
|
|
3190
3584
|
writeInfo(context, `Checklist for: ${task.title}`);
|
|
3191
|
-
for (const item of sorted)
|
|
3585
|
+
for (const item of sorted) {
|
|
3586
|
+
const marker = item.completedAt === null ? "[ ]" : "[x]";
|
|
3587
|
+
writeInfo(context, `${marker} ${item.id.slice(0, 8)} ${item.title}`);
|
|
3588
|
+
}
|
|
3192
3589
|
};
|
|
3193
3590
|
const registerTaskSubcommands = (task, workspaceOverride) => {
|
|
3194
3591
|
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", `
|
|
@@ -3374,11 +3771,12 @@ const registerChecklistSubcommands = (checklist) => {
|
|
|
3374
3771
|
const context = contextFromCommand(command);
|
|
3375
3772
|
await runWithErrorHandling(context, async () => {
|
|
3376
3773
|
const commandContext = await resolveCommandContext(context);
|
|
3377
|
-
const
|
|
3774
|
+
const resolvedTask = await resolveTaskReference(context, {
|
|
3378
3775
|
commandContext,
|
|
3379
3776
|
taskRef: taskId,
|
|
3380
3777
|
workspaceRef: options.workspace?.trim() ?? null
|
|
3381
|
-
})
|
|
3778
|
+
});
|
|
3779
|
+
const item = await resolveChecklistItemReference(context, commandContext, resolvedTask.id, itemId);
|
|
3382
3780
|
const now = Date.now();
|
|
3383
3781
|
await mutateModel(context, {
|
|
3384
3782
|
baseUrl: commandContext.apiBaseUrl,
|
|
@@ -3412,11 +3810,12 @@ const registerChecklistSubcommands = (checklist) => {
|
|
|
3412
3810
|
const context = contextFromCommand(command);
|
|
3413
3811
|
await runWithErrorHandling(context, async () => {
|
|
3414
3812
|
const commandContext = await resolveCommandContext(context);
|
|
3415
|
-
const
|
|
3813
|
+
const resolvedTask = await resolveTaskReference(context, {
|
|
3416
3814
|
commandContext,
|
|
3417
3815
|
taskRef: taskId,
|
|
3418
3816
|
workspaceRef: options.workspace?.trim() ?? null
|
|
3419
|
-
})
|
|
3817
|
+
});
|
|
3818
|
+
const item = await resolveChecklistItemReference(context, commandContext, resolvedTask.id, itemId);
|
|
3420
3819
|
const now = Date.now();
|
|
3421
3820
|
await mutateModel(context, {
|
|
3422
3821
|
baseUrl: commandContext.apiBaseUrl,
|
|
@@ -3452,11 +3851,12 @@ const registerChecklistSubcommands = (checklist) => {
|
|
|
3452
3851
|
const newTitle = options.title.trim();
|
|
3453
3852
|
if (newTitle.length === 0) throw new Error("Checklist item title cannot be empty.");
|
|
3454
3853
|
const commandContext = await resolveCommandContext(context);
|
|
3455
|
-
const
|
|
3854
|
+
const resolvedTask = await resolveTaskReference(context, {
|
|
3456
3855
|
commandContext,
|
|
3457
3856
|
taskRef: taskId,
|
|
3458
3857
|
workspaceRef: options.workspace?.trim() ?? null
|
|
3459
|
-
})
|
|
3858
|
+
});
|
|
3859
|
+
const item = await resolveChecklistItemReference(context, commandContext, resolvedTask.id, itemId);
|
|
3460
3860
|
const now = Date.now();
|
|
3461
3861
|
await mutateModel(context, {
|
|
3462
3862
|
baseUrl: commandContext.apiBaseUrl,
|
|
@@ -3490,11 +3890,12 @@ const registerChecklistSubcommands = (checklist) => {
|
|
|
3490
3890
|
const context = contextFromCommand(command);
|
|
3491
3891
|
await runWithErrorHandling(context, async () => {
|
|
3492
3892
|
const commandContext = await resolveCommandContext(context);
|
|
3493
|
-
const
|
|
3893
|
+
const resolvedTask = await resolveTaskReference(context, {
|
|
3494
3894
|
commandContext,
|
|
3495
3895
|
taskRef: taskId,
|
|
3496
3896
|
workspaceRef: options.workspace?.trim() ?? null
|
|
3497
|
-
})
|
|
3897
|
+
});
|
|
3898
|
+
const item = await resolveChecklistItemReference(context, commandContext, resolvedTask.id, itemId);
|
|
3498
3899
|
await mutateModel(context, {
|
|
3499
3900
|
baseUrl: commandContext.apiBaseUrl,
|
|
3500
3901
|
request: {
|
|
@@ -3549,12 +3950,13 @@ Agent tip: run "donebear spec task add" for full option reference
|
|
|
3549
3950
|
});
|
|
3550
3951
|
});
|
|
3551
3952
|
registerTaskSubcommands(task, workspaceOverride);
|
|
3552
|
-
|
|
3953
|
+
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) => {
|
|
3553
3954
|
const context = contextFromCommand(command);
|
|
3554
3955
|
await runWithErrorHandling(context, async () => {
|
|
3555
3956
|
await runChecklistList(context, taskId, options.workspace);
|
|
3556
3957
|
});
|
|
3557
|
-
})
|
|
3958
|
+
});
|
|
3959
|
+
registerChecklistSubcommands(checklist);
|
|
3558
3960
|
};
|
|
3559
3961
|
//#endregion
|
|
3560
3962
|
//#region src/commands/comment.ts
|
|
@@ -3716,11 +4118,12 @@ respond to a teammate's comment, add your own instead.
|
|
|
3716
4118
|
await runWithErrorHandling(context, async () => {
|
|
3717
4119
|
const body = await resolveCommentBody(options.body);
|
|
3718
4120
|
const commandContext = await resolveCommandContext(context);
|
|
3719
|
-
const
|
|
4121
|
+
const task = await resolveTaskReference(context, {
|
|
3720
4122
|
commandContext,
|
|
3721
4123
|
taskRef,
|
|
3722
4124
|
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
3723
|
-
})
|
|
4125
|
+
});
|
|
4126
|
+
const existing = await resolveCommentReference(context, commandContext, task.id, commentRef);
|
|
3724
4127
|
await mutateModel(context, {
|
|
3725
4128
|
baseUrl: commandContext.apiBaseUrl,
|
|
3726
4129
|
request: {
|
|
@@ -3756,11 +4159,12 @@ workspace owner or admin can remove anyone's.
|
|
|
3756
4159
|
const context = contextFromCommand(command);
|
|
3757
4160
|
await runWithErrorHandling(context, async () => {
|
|
3758
4161
|
const commandContext = await resolveCommandContext(context);
|
|
3759
|
-
const
|
|
4162
|
+
const task = await resolveTaskReference(context, {
|
|
3760
4163
|
commandContext,
|
|
3761
4164
|
taskRef,
|
|
3762
4165
|
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
3763
|
-
})
|
|
4166
|
+
});
|
|
4167
|
+
const existing = await resolveCommentReference(context, commandContext, task.id, commentRef);
|
|
3764
4168
|
await mutateModel(context, {
|
|
3765
4169
|
baseUrl: commandContext.apiBaseUrl,
|
|
3766
4170
|
request: {
|
|
@@ -3918,7 +4322,6 @@ Examples:
|
|
|
3918
4322
|
});
|
|
3919
4323
|
});
|
|
3920
4324
|
};
|
|
3921
|
-
const packageManifest = createRequire(import.meta.url)("../package.json");
|
|
3922
4325
|
//#endregion
|
|
3923
4326
|
//#region src/commands/doctor.ts
|
|
3924
4327
|
const checkCliVersion = async () => {
|
|
@@ -3960,13 +4363,14 @@ const maskToken = (token) => {
|
|
|
3960
4363
|
};
|
|
3961
4364
|
const checkAuth = async (context) => {
|
|
3962
4365
|
try {
|
|
3963
|
-
const
|
|
4366
|
+
const config = (() => {
|
|
3964
4367
|
try {
|
|
3965
4368
|
return resolveSupabaseConfig();
|
|
3966
4369
|
} catch {
|
|
3967
4370
|
return null;
|
|
3968
4371
|
}
|
|
3969
|
-
})()
|
|
4372
|
+
})();
|
|
4373
|
+
const token = await resolveAuthToken(context, config);
|
|
3970
4374
|
if (!token) return {
|
|
3971
4375
|
message: "No API key or token found",
|
|
3972
4376
|
name: "Auth",
|
|
@@ -3987,7 +4391,7 @@ const checkAuth = async (context) => {
|
|
|
3987
4391
|
};
|
|
3988
4392
|
const checkWorkspace = async () => {
|
|
3989
4393
|
try {
|
|
3990
|
-
const { ensureLocalContext } = await import("./context-store-
|
|
4394
|
+
const { ensureLocalContext } = await import("./context-store-BuhPB0I9.mjs");
|
|
3991
4395
|
const localContext = await ensureLocalContext();
|
|
3992
4396
|
if (!localContext.workspaceId) return {
|
|
3993
4397
|
message: "No workspace selected (run `donebear workspace use`)",
|
|
@@ -4009,7 +4413,10 @@ const checkWorkspace = async () => {
|
|
|
4009
4413
|
};
|
|
4010
4414
|
const detectClaudeDesktop = (home) => {
|
|
4011
4415
|
if (process.platform === "darwin") return existsSync(join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"));
|
|
4012
|
-
if (process.platform === "win32")
|
|
4416
|
+
if (process.platform === "win32") {
|
|
4417
|
+
const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
|
|
4418
|
+
return existsSync(join(appData, "Claude", "claude_desktop_config.json"));
|
|
4419
|
+
}
|
|
4013
4420
|
return existsSync(join(home, ".config", "claude", "config.json"));
|
|
4014
4421
|
};
|
|
4015
4422
|
const detectAgents = () => {
|
|
@@ -4309,6 +4716,10 @@ state and start date rather than stored as a column:
|
|
|
4309
4716
|
|
|
4310
4717
|
- inbox captured, not yet placed
|
|
4311
4718
|
- today start date today or earlier
|
|
4719
|
+
|
|
4720
|
+
A private email address (Settings on the web) can also drop a message into
|
|
4721
|
+
today as a task: subject is the title, body is the notes. Slack capture is
|
|
4722
|
+
unchanged.
|
|
4312
4723
|
- upcoming start date in the future
|
|
4313
4724
|
- anytime actionable now, no date pressure
|
|
4314
4725
|
- someday parked, off every working list
|
|
@@ -4324,7 +4735,9 @@ completed task's view is logbook, so \`--state done --view today\` matches
|
|
|
4324
4735
|
nothing. What was finished today or this week comes off the completion date,
|
|
4325
4736
|
not the ordering: the list prints it in the \`completed\` column, \`task show
|
|
4326
4737
|
--json\` reports it as \`completedAtIso\`, and each row from \`task list --json\`
|
|
4327
|
-
carries \`completedAt\`.
|
|
4738
|
+
carries \`completedAt\`. The iOS and web apps can keep completed to-dos on Today
|
|
4739
|
+
until a day roll or Log Completed (Settings → Preferences → Logging). The CLI
|
|
4740
|
+
still treats every completed task as logbook.
|
|
4328
4741
|
|
|
4329
4742
|
donebear task add "Renew passport" --when upcoming --deadline 2030-03-01 --json
|
|
4330
4743
|
donebear task list --view logbook --json
|
|
@@ -4382,8 +4795,16 @@ Clear a date with the explicit flag rather than passing an empty value:
|
|
|
4382
4795
|
donebear task edit 12 --clear-deadline --json
|
|
4383
4796
|
donebear project edit launch --clear-target-date --json
|
|
4384
4797
|
|
|
4385
|
-
|
|
4386
|
-
asks to make something repeat, say so rather than creating a run of copies
|
|
4798
|
+
Repeat rules are configured in the app and cannot be edited from the CLI. If a user
|
|
4799
|
+
asks to make something repeat, say so rather than creating a run of copies.
|
|
4800
|
+
|
|
4801
|
+
Upcoming copies can be edited or completed early. Fixed repeats keep their
|
|
4802
|
+
calendar schedule; after-completion repeats count from actual completion.
|
|
4803
|
+
CLI date edits affect the copy only. Use When in the app to choose Make
|
|
4804
|
+
Exception or Update Rule for a fixed series. Custom and count-limited
|
|
4805
|
+
patterns need the Repeat editor.
|
|
4806
|
+
In the app, search for Repeating to see rules and upcoming copies. Select multiple
|
|
4807
|
+
repeating tasks and use More to pause, resume, or stop their series.`,
|
|
4387
4808
|
title: "Dates and deadlines",
|
|
4388
4809
|
whenToUse: "Setting or clearing a due date, or choosing between a deadline and a start date."
|
|
4389
4810
|
},
|
|
@@ -4400,12 +4821,18 @@ never have to read a task first just to preserve its other fields.
|
|
|
4400
4821
|
- cleared use the paired flag: \`--clear-deadline\`, \`--clear-project\`,
|
|
4401
4822
|
\`--clear-team\`, \`--clear-description\`.
|
|
4402
4823
|
|
|
4824
|
+
The description is markdown. Write a link as a bare URL (\`https://…\`), which
|
|
4825
|
+
every client shows as a link; \`<https://…>\` shows its angle brackets on iOS
|
|
4826
|
+
and here.
|
|
4827
|
+
|
|
4403
4828
|
Not on \`task edit\`:
|
|
4404
4829
|
|
|
4405
4830
|
- labels \`donebear label add|remove\`
|
|
4406
4831
|
- completion \`donebear task done|reopen\`
|
|
4407
4832
|
- archiving \`donebear task archive\` (moves to Trash, not a delete)
|
|
4408
4833
|
|
|
4834
|
+
An API key bound to a task agent archives only its own assigned tasks, using the selected revision. A changed task returns a conflict; reload and review it before retrying. Pass \`-w <workspace>\` to select the intended workspace.
|
|
4835
|
+
|
|
4409
4836
|
Task numbers are positive integers scoped to the workspace. Projects, teams, and
|
|
4410
4837
|
labels also resolve by key or name.
|
|
4411
4838
|
|
|
@@ -4425,7 +4852,10 @@ thing that needs care.
|
|
|
4425
4852
|
2. Check for truncation. Results carry a count; if you passed \`--limit\`, you
|
|
4426
4853
|
are looking at a page, not the set.
|
|
4427
4854
|
3. Show the user the titles, not the ids, and get agreement before writing.
|
|
4428
|
-
4. Then write, one call per task.
|
|
4855
|
+
4. Then write, one call per task. Archive writes are not atomic: a later
|
|
4856
|
+
conflict leaves earlier tasks in Trash. Inspect Trash and re-read the failed
|
|
4857
|
+
task before retrying. After a timeout, reconcile current task state first;
|
|
4858
|
+
never retry the whole set blindly.
|
|
4429
4859
|
|
|
4430
4860
|
donebear task list --state open --view today --json --jq "[].{number: number, title: title}"
|
|
4431
4861
|
|
|
@@ -4593,9 +5023,21 @@ off unless the user named a second one:
|
|
|
4593
5023
|
Ids and names never cross workspaces. A missing task, project, or label is
|
|
4594
5024
|
missing from *this* workspace before it is deleted.
|
|
4595
5025
|
|
|
5026
|
+
A workspace owner can turn on domain join from Settings → Members. People who
|
|
5027
|
+
later sign in with a verified address on that work domain join as members,
|
|
5028
|
+
without an invite code. Personal email hosts cannot be used. The CLI has no
|
|
5029
|
+
domain-join command; joining by code remains \`donebear workspace join\`.
|
|
5030
|
+
|
|
4596
5031
|
Auth precedence: \`DONEBEAR_TOKEN\` in the environment, then the stored session
|
|
4597
|
-
from \`donebear auth login\`. Set the env var for
|
|
4598
|
-
browser login flow will hang in CI.
|
|
5032
|
+
from \`donebear auth login\` or \`donebear auth signup\`. Set the env var for
|
|
5033
|
+
anything non-interactive; the browser login flow will hang in CI.
|
|
5034
|
+
|
|
5035
|
+
New accounts from a coding agent: \`donebear auth signup --email <human-email>
|
|
5036
|
+
--json --no-interactive\`. Do not invent an email. One human step verifies it
|
|
5037
|
+
(link or 6-digit code). The CLI then stores a \`db_\` key with read+write and
|
|
5038
|
+
prints paste-ready MCP config for \`https://mcp.donebear.com/mcp\`. If that
|
|
5039
|
+
email is already registered, run \`donebear auth login\` instead. WorkOS
|
|
5040
|
+
\`agent_auth\` and RFC 8628 device code are not offered.
|
|
4599
5041
|
|
|
4600
5042
|
export DONEBEAR_TOKEN=<token>
|
|
4601
5043
|
donebear whoami --json
|
|
@@ -4605,6 +5047,15 @@ write fails with an explicit message rather than silently doing nothing — tell
|
|
|
4605
5047
|
the user they need a write-scoped key rather than trying to work around it.
|
|
4606
5048
|
\`donebear doctor --json\` reports which of these is actually in effect.
|
|
4607
5049
|
|
|
5050
|
+
## Invites and share links
|
|
5051
|
+
|
|
5052
|
+
\`donebear workspace invite --email user@example.com\` sends a single-use
|
|
5053
|
+
email invite. \`donebear workspace invite --share --copy\` mints a reusable
|
|
5054
|
+
join link at \`https://donebear.com/join?code=\`. \`--project <id>\` on
|
|
5055
|
+
\`--share\` scopes the link: whoever opens it joins the whole workspace as a
|
|
5056
|
+
member, gets that project, and uses a seat. Join with
|
|
5057
|
+
\`donebear workspace join <code>\` or by opening the link while signed in.
|
|
5058
|
+
|
|
4608
5059
|
## Task attachments (read-only)
|
|
4609
5060
|
|
|
4610
5061
|
Tasks can carry file/image attachments, capped at 10 MB per file and roughly
|
|
@@ -4625,6 +5076,9 @@ Three commands orient you, in order of cost:
|
|
|
4625
5076
|
donebear guide <topic> concepts and conventions, no auth
|
|
4626
5077
|
donebear context --json the live workspace: counts, recent tasks, projects
|
|
4627
5078
|
|
|
5079
|
+
No account yet: \`donebear auth signup --email <human-email> --json --no-interactive\`.
|
|
5080
|
+
Do not invent the email. A human verifies it, then the CLI stores a \`db_\` key.
|
|
5081
|
+
|
|
4628
5082
|
\`donebear spec <command> <subcommand>\` narrows to one command's flags, output
|
|
4629
5083
|
shape, and examples. Read that before guessing at a flag.
|
|
4630
5084
|
|
|
@@ -4653,22 +5107,25 @@ workspace from the token.`,
|
|
|
4653
5107
|
|
|
4654
5108
|
Two importers exist, and they are not the same thing.
|
|
4655
5109
|
|
|
5110
|
+
**In the apps**, Settings → Integrations & Import (web) and Settings → Import
|
|
5111
|
+
(iOS: Reminders, Todoist, Things) cover Todoist, TickTick, Microsoft To Do, Any.do, Apple
|
|
5112
|
+
Reminders, Things 3, Trello, Asana, Notion, Google Tasks, and plain CSV, JSON or
|
|
5113
|
+
iCal files. Things 3 is no longer CLI-only: on iPhone, Things → Settings →
|
|
5114
|
+
General → Diagnostics → Enter Code → \`491348\` → Send Things Database, then
|
|
5115
|
+
upload the \`.aar\` under Settings → Import → Things, or \`main.sqlite\` on the web.
|
|
5116
|
+
|
|
4656
5117
|
**In the terminal**, \`donebear import things\` reads a copy of a Things 3
|
|
4657
5118
|
SQLite database and writes it into a workspace. It is the only import this CLI
|
|
4658
|
-
performs. \`donebear spec import things\` lists its flags.
|
|
5119
|
+
performs. \`donebear spec import things\` lists its flags. It is the power-user
|
|
5120
|
+
path; send people to the apps first.
|
|
4659
5121
|
|
|
4660
5122
|
On macOS it finds that database itself — \`donebear import things --dry-run\`
|
|
4661
|
-
with no other flags is the whole command.
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
most common way this import stalls. \`--database-url\` is for a database
|
|
4665
|
-
somewhere unusual: a backup, an external disk, another machine's copy.
|
|
5123
|
+
with no other flags is the whole command. \`--database-url\` is for a database
|
|
5124
|
+
somewhere unusual: an iPhone export, a backup, an external disk, another
|
|
5125
|
+
machine's copy.
|
|
4666
5126
|
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
Google Tasks, and plain CSV, JSON or iCal files. If a user asks how to move a
|
|
4670
|
-
backlog in from anything other than Things, that is where to send them; there
|
|
4671
|
-
is no CLI equivalent to offer.
|
|
5127
|
+
If a user asks how to move a backlog in from anything other than Things, that is
|
|
5128
|
+
the web import screen; there is no CLI equivalent to offer.
|
|
4672
5129
|
|
|
4673
5130
|
Each of those is a file the user exports and uploads — Todoist from Settings →
|
|
4674
5131
|
Backups, Google Tasks from Google Takeout, Microsoft To Do from a read-only
|
|
@@ -4677,6 +5134,13 @@ that may change: a read-only connect flow is being built so Done Bear can make
|
|
|
4677
5134
|
that call instead, so treat the Graph export as the current route and check the
|
|
4678
5135
|
import screen before promising a connection.
|
|
4679
5136
|
|
|
5137
|
+
Every importer, here and in the apps, only reads the source; none of them
|
|
5138
|
+
writes, edits or deletes anything in the app being left. The one path that
|
|
5139
|
+
does is Reminders Inbox on iPhone (Settings → Import → Reminders Inbox), which
|
|
5140
|
+
is a capture bridge rather than an import: a chosen Reminders list shows at the
|
|
5141
|
+
top of Inbox, and importing a to-do from there creates the task and removes it
|
|
5142
|
+
from Reminders after a confirmation. It has no CLI equivalent.
|
|
5143
|
+
|
|
4680
5144
|
The other direction is here though: \`donebear export\` writes a portable JSON
|
|
4681
5145
|
archive of a workspace, and the web importer restores one.
|
|
4682
5146
|
|
|
@@ -4709,9 +5173,8 @@ only what is new.
|
|
|
4709
5173
|
|
|
4710
5174
|
There is no undo for \`donebear import things\`; a run has to be unpicked by
|
|
4711
5175
|
hand. The web importer does offer one on its completion screen, but the receipt
|
|
4712
|
-
lives in the browser that ran the import
|
|
4713
|
-
|
|
4714
|
-
looking for a flag.`,
|
|
5176
|
+
lives in the browser that ran the import. An iOS import is undone with the
|
|
5177
|
+
usual undo for that change, not from the web receipt.`,
|
|
4715
5178
|
title: "Importing from another app",
|
|
4716
5179
|
whenToUse: "Moving a backlog in from Things 3 or another task manager, or restoring an export."
|
|
4717
5180
|
}
|
|
@@ -4866,11 +5329,14 @@ const summarizeThingsImportResult = (result) => {
|
|
|
4866
5329
|
`source: ${result.counts.source.tasks} tasks, ${result.counts.source.projects} projects, ${result.counts.source.headings} headings, ${result.counts.source.areas} areas, ${result.counts.source.tags} tags, ${result.counts.source.checklistItems} checklist items`,
|
|
4867
5330
|
`planned: ${result.counts.planned.teams ?? 0} teams, ${result.counts.planned.labels ?? 0} labels, ${result.counts.planned.projects ?? 0} projects, ${result.counts.planned.teamProjects ?? 0} team-project links, ${result.counts.planned.headings ?? 0} headings, ${result.counts.planned.tasks ?? 0} tasks, ${result.counts.planned.taskLabels ?? 0} task-label links, ${result.counts.planned.checklistItems ?? 0} checklist items`
|
|
4868
5331
|
];
|
|
5332
|
+
const { leftBehind } = result.counts;
|
|
5333
|
+
if (leftBehind.tasks > 0 || leftBehind.projects > 0) infoLines.push(`left behind (--open-only): ${leftBehind.tasks} to-dos, ${leftBehind.projects} projects`);
|
|
4869
5334
|
if (result.dryRun) infoLines.push("dry run: no mutations sent");
|
|
4870
5335
|
else infoLines.push(`mutations: ${result.execution.inserted} inserted, ${result.execution.updated} updated, ${result.execution.skipped} skipped across ${result.execution.batches} batch(es)`);
|
|
4871
5336
|
return {
|
|
4872
5337
|
infoLines,
|
|
4873
|
-
successMessage: result.dryRun ? "Things 3 import dry run complete" : "Things 3 import complete"
|
|
5338
|
+
successMessage: result.dryRun ? "Things 3 import dry run complete" : "Things 3 import complete",
|
|
5339
|
+
warningLines: result.issues.map((issue) => issue.message)
|
|
4874
5340
|
};
|
|
4875
5341
|
};
|
|
4876
5342
|
//#endregion
|
|
@@ -4946,15 +5412,50 @@ const getUtcCalendarDayEndEpoch = (date) => {
|
|
|
4946
5412
|
return Date.UTC(parts.year, parts.month - 1, parts.day, 23, 59, 59, 999);
|
|
4947
5413
|
};
|
|
4948
5414
|
//#endregion
|
|
5415
|
+
//#region ../../packages/import-core/src/issues.ts
|
|
5416
|
+
/**
|
|
5417
|
+
* Codes are part of the contract between a provider and the completion report:
|
|
5418
|
+
* the UI groups by code, so adding a message variant must not mean adding a
|
|
5419
|
+
* code.
|
|
5420
|
+
*/
|
|
5421
|
+
const ImportIssueCode = {
|
|
5422
|
+
/** The user chose to import without turning areas into teams. */
|
|
5423
|
+
AREAS_NOT_IMPORTED: "areas_not_imported",
|
|
5424
|
+
ASSIGNEE_UNRESOLVED: "assignee_unresolved",
|
|
5425
|
+
ATTACHMENT_NOT_TRANSFERRED: "attachment_not_transferred",
|
|
5426
|
+
DUPLICATE_SOURCE_ID: "duplicate_source_id",
|
|
5427
|
+
FILE_TOO_LARGE: "file_too_large",
|
|
5428
|
+
INVALID_DATE: "invalid_date",
|
|
5429
|
+
INVALID_RECORD: "invalid_record",
|
|
5430
|
+
/** Kept out of the plan because the user asked for open tasks only. */
|
|
5431
|
+
LEFT_BEHIND: "left_behind",
|
|
5432
|
+
MISSING_TITLE: "missing_title",
|
|
5433
|
+
PARENT_NOT_FOUND: "parent_not_found",
|
|
5434
|
+
RECURRENCE_APPROXIMATED: "recurrence_approximated",
|
|
5435
|
+
RECURRENCE_UNSUPPORTED: "recurrence_unsupported",
|
|
5436
|
+
TIMEZONE_UNKNOWN: "timezone_unknown",
|
|
5437
|
+
UNKNOWN_FILE_SHAPE: "unknown_file_shape",
|
|
5438
|
+
UNSUPPORTED_FIELD: "unsupported_field"
|
|
5439
|
+
};
|
|
5440
|
+
ImportIssueCode.AREAS_NOT_IMPORTED, ImportIssueCode.LEFT_BEHIND;
|
|
5441
|
+
const createIssue = (code, message, options = {}) => ({
|
|
5442
|
+
code,
|
|
5443
|
+
field: options.field ?? null,
|
|
5444
|
+
message,
|
|
5445
|
+
severity: options.severity ?? "warning",
|
|
5446
|
+
sourceRecordId: options.sourceRecordId ?? null
|
|
5447
|
+
});
|
|
5448
|
+
//#endregion
|
|
4949
5449
|
//#region ../../packages/import-core/src/things/ids.ts
|
|
4950
5450
|
const THINGS_IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
|
|
4951
5451
|
const createDeterministicImportId = (params) => createDeterministicId(THINGS_IMPORT_ID_NAMESPACE, `things:${params.workspaceId}:${params.modelName}:${params.sourceId}`);
|
|
4952
5452
|
//#endregion
|
|
4953
5453
|
//#region ../../packages/import-core/src/things/labels.ts
|
|
4954
5454
|
const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase();
|
|
5455
|
+
const normalizeThingsTeamLookupKey = (value) => value.trim().toLocaleLowerCase();
|
|
4955
5456
|
//#endregion
|
|
4956
5457
|
//#region ../../packages/import-core/src/things/values.ts
|
|
4957
|
-
var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
5458
|
+
var import_rrule = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
4958
5459
|
(function webpackUniversalModuleDefinition(root, factory) {
|
|
4959
5460
|
if (typeof exports === "object" && typeof module === "object") module.exports = factory();
|
|
4960
5461
|
else if (typeof define === "function" && define.amd) define([], factory);
|
|
@@ -4964,17 +5465,23 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
4964
5465
|
return (() => {
|
|
4965
5466
|
"use strict";
|
|
4966
5467
|
var __webpack_require__ = {};
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
};
|
|
5468
|
+
(() => {
|
|
5469
|
+
__webpack_require__.d = (exports$1, definition) => {
|
|
5470
|
+
for (var key in definition) if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports$1, key)) Object.defineProperty(exports$1, key, {
|
|
5471
|
+
enumerable: true,
|
|
5472
|
+
get: definition[key]
|
|
5473
|
+
});
|
|
5474
|
+
};
|
|
5475
|
+
})();
|
|
5476
|
+
(() => {
|
|
5477
|
+
__webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
|
|
5478
|
+
})();
|
|
5479
|
+
(() => {
|
|
5480
|
+
__webpack_require__.r = (exports$2) => {
|
|
5481
|
+
if (typeof Symbol !== "undefined" && Symbol.toStringTag) Object.defineProperty(exports$2, Symbol.toStringTag, { value: "Module" });
|
|
5482
|
+
Object.defineProperty(exports$2, "__esModule", { value: true });
|
|
5483
|
+
};
|
|
5484
|
+
})();
|
|
4978
5485
|
var __webpack_exports__ = {};
|
|
4979
5486
|
__webpack_require__.r(__webpack_exports__);
|
|
4980
5487
|
__webpack_require__.d(__webpack_exports__, {
|
|
@@ -5145,7 +5652,7 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
5145
5652
|
/**
|
|
5146
5653
|
* Number of milliseconds of one day
|
|
5147
5654
|
*/
|
|
5148
|
-
var ONE_DAY =
|
|
5655
|
+
var ONE_DAY = 864e5;
|
|
5149
5656
|
/**
|
|
5150
5657
|
* @see: <http://docs.python.org/library/datetime.html#datetime.MAXYEAR>
|
|
5151
5658
|
*/
|
|
@@ -5380,9 +5887,6 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
5380
5887
|
}
|
|
5381
5888
|
return to.concat(ar || Array.prototype.slice.call(from));
|
|
5382
5889
|
}
|
|
5383
|
-
function __await(v) {
|
|
5384
|
-
return this instanceof __await ? (this.v = v, this) : new __await(v);
|
|
5385
|
-
}
|
|
5386
5890
|
const callbackiterresult = function(_super) {
|
|
5387
5891
|
__extends(CallbackIterResult, _super);
|
|
5388
5892
|
function CallbackIterResult(method, args, iterator) {
|
|
@@ -5596,9 +6100,10 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
5596
6100
|
ToText.prototype.WEEKLY = function() {
|
|
5597
6101
|
var gettext = this.gettext;
|
|
5598
6102
|
if (this.options.interval !== 1) this.add(this.options.interval.toString()).add(this.plural(this.options.interval) ? gettext("weeks") : gettext("week"));
|
|
5599
|
-
if (this.byweekday && this.byweekday.isWeekdays)
|
|
5600
|
-
|
|
5601
|
-
|
|
6103
|
+
if (this.byweekday && this.byweekday.isWeekdays) {
|
|
6104
|
+
if (this.options.interval === 1) this.add(this.plural(this.options.interval) ? gettext("weekdays") : gettext("weekday"));
|
|
6105
|
+
else this.add(gettext("on")).add(gettext("weekdays"));
|
|
6106
|
+
} else if (this.byweekday && this.byweekday.isEveryDay) this.add(this.plural(this.options.interval) ? gettext("days") : gettext("day"));
|
|
5602
6107
|
else {
|
|
5603
6108
|
if (this.options.interval === 1) this.add(gettext("week"));
|
|
5604
6109
|
if (this.origOptions.bymonth) {
|
|
@@ -5709,8 +6214,10 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
5709
6214
|
var delimJoin = function(array, delimiter, finalDelimiter) {
|
|
5710
6215
|
var list = "";
|
|
5711
6216
|
for (var i = 0; i < array.length; i++) {
|
|
5712
|
-
if (i !== 0)
|
|
5713
|
-
|
|
6217
|
+
if (i !== 0) {
|
|
6218
|
+
if (i === array.length - 1) list += " " + finalDelimiter + " ";
|
|
6219
|
+
else list += delimiter + " ";
|
|
6220
|
+
}
|
|
5714
6221
|
list += array[i];
|
|
5715
6222
|
}
|
|
5716
6223
|
return list;
|
|
@@ -5920,16 +6427,17 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
5920
6427
|
var nth = decodeNTH();
|
|
5921
6428
|
var wkd = decodeWKD();
|
|
5922
6429
|
var m = decodeM();
|
|
5923
|
-
if (nth)
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
6430
|
+
if (nth) {
|
|
6431
|
+
if (wkd) {
|
|
6432
|
+
ttr.nextSymbol();
|
|
6433
|
+
if (!options.byweekday) options.byweekday = [];
|
|
6434
|
+
options.byweekday.push(RRule[wkd].nth(nth));
|
|
6435
|
+
} else {
|
|
6436
|
+
if (!options.bymonthday) options.bymonthday = [];
|
|
6437
|
+
options.bymonthday.push(nth);
|
|
6438
|
+
ttr.accept("day(s)");
|
|
6439
|
+
}
|
|
6440
|
+
} else if (wkd) {
|
|
5933
6441
|
ttr.nextSymbol();
|
|
5934
6442
|
if (!options.byweekday) options.byweekday = [];
|
|
5935
6443
|
options.byweekday.push(RRule[wkd]);
|
|
@@ -6335,9 +6843,7 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
6335
6843
|
case RRule.MONTHLY:
|
|
6336
6844
|
opts.bymonthday = opts.dtstart.getUTCDate();
|
|
6337
6845
|
break;
|
|
6338
|
-
case RRule.WEEKLY:
|
|
6339
|
-
opts.byweekday = [getWeekday(opts.dtstart)];
|
|
6340
|
-
break;
|
|
6846
|
+
case RRule.WEEKLY: opts.byweekday = [getWeekday(opts.dtstart)];
|
|
6341
6847
|
}
|
|
6342
6848
|
if (isPresent(opts.bymonth) && !isArray(opts.bymonth)) opts.bymonth = [opts.bymonth];
|
|
6343
6849
|
if (isPresent(opts.byyearday) && !isArray(opts.byyearday) && isNumber(opts.byyearday)) opts.byyearday = [opts.byyearday];
|
|
@@ -6369,14 +6875,15 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
6369
6875
|
} else if (isWeekdayStr(opts.byweekday)) {
|
|
6370
6876
|
opts.byweekday = [Weekday.fromStr(opts.byweekday).weekday];
|
|
6371
6877
|
opts.bynweekday = null;
|
|
6372
|
-
} else if (opts.byweekday instanceof Weekday)
|
|
6373
|
-
opts.byweekday
|
|
6374
|
-
|
|
6878
|
+
} else if (opts.byweekday instanceof Weekday) {
|
|
6879
|
+
if (!opts.byweekday.n || opts.freq > RRule.MONTHLY) {
|
|
6880
|
+
opts.byweekday = [opts.byweekday.weekday];
|
|
6881
|
+
opts.bynweekday = null;
|
|
6882
|
+
} else {
|
|
6883
|
+
opts.bynweekday = [[opts.byweekday.weekday, opts.byweekday.n]];
|
|
6884
|
+
opts.byweekday = null;
|
|
6885
|
+
}
|
|
6375
6886
|
} else {
|
|
6376
|
-
opts.bynweekday = [[opts.byweekday.weekday, opts.byweekday.n]];
|
|
6377
|
-
opts.byweekday = null;
|
|
6378
|
-
}
|
|
6379
|
-
else {
|
|
6380
6887
|
var byweekday = [];
|
|
6381
6888
|
var bynweekday = [];
|
|
6382
6889
|
for (var i = 0; i < opts.byweekday.length; i++) {
|
|
@@ -6795,12 +7302,13 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
6795
7302
|
nwdaymask: []
|
|
6796
7303
|
};
|
|
6797
7304
|
var ranges = [];
|
|
6798
|
-
if (options.freq === RRule.YEARLY)
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
7305
|
+
if (options.freq === RRule.YEARLY) {
|
|
7306
|
+
if (empty(options.bymonth)) ranges = [[0, yearlen]];
|
|
7307
|
+
else for (var j = 0; j < options.bymonth.length; j++) {
|
|
7308
|
+
month = options.bymonth[j];
|
|
7309
|
+
ranges.push(mrange.slice(month - 1, month + 1));
|
|
7310
|
+
}
|
|
7311
|
+
} else if (options.freq === RRule.MONTHLY) ranges = [mrange.slice(month - 1, month + 1)];
|
|
6804
7312
|
if (empty(ranges)) return result;
|
|
6805
7313
|
result.nwdaymask = repeat(0, yearlen);
|
|
6806
7314
|
for (var j = 0; j < ranges.length; j++) {
|
|
@@ -6839,7 +7347,7 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
6839
7347
|
var month = Math.floor((h + l - 7 * m + 114) / 31);
|
|
6840
7348
|
var day = (h + l - 7 * m + 114) % 31 + 1;
|
|
6841
7349
|
var date = Date.UTC(y, month - 1, day + offset);
|
|
6842
|
-
return [Math.ceil((date - Date.UTC(y, 0, 1)) /
|
|
7350
|
+
return [Math.ceil((date - Date.UTC(y, 0, 1)) / 864e5)];
|
|
6843
7351
|
}
|
|
6844
7352
|
const iterinfo = function() {
|
|
6845
7353
|
function Iterinfo(options) {
|
|
@@ -7708,10 +8216,8 @@ var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
7708
8216
|
return __webpack_exports__;
|
|
7709
8217
|
})();
|
|
7710
8218
|
});
|
|
7711
|
-
})))();
|
|
8219
|
+
})))(), 1);
|
|
7712
8220
|
const PACKED_DATE_MULTIPLIER = 128;
|
|
7713
|
-
const PACKED_DATE_DAY_MASK = 31;
|
|
7714
|
-
const PACKED_DATE_MONTH_MASK = 15;
|
|
7715
8221
|
const PACKED_DATE_MONTH_SHIFT = 5;
|
|
7716
8222
|
const PACKED_DATE_YEAR_SHIFT = 9;
|
|
7717
8223
|
const THINGS_REMINDER_HOUR_SCALE = 67108864;
|
|
@@ -7732,8 +8238,8 @@ const decodeThingsPackedDateParts = (value) => {
|
|
|
7732
8238
|
if (integer <= 0 || integer % PACKED_DATE_MULTIPLIER !== 0) return null;
|
|
7733
8239
|
const packed = integer / PACKED_DATE_MULTIPLIER;
|
|
7734
8240
|
const parts = {
|
|
7735
|
-
day: packed %
|
|
7736
|
-
month: Math.trunc(packed / 2 ** PACKED_DATE_MONTH_SHIFT) %
|
|
8241
|
+
day: packed % 32,
|
|
8242
|
+
month: Math.trunc(packed / 2 ** PACKED_DATE_MONTH_SHIFT) % 16,
|
|
7737
8243
|
year: Math.trunc(packed / 2 ** PACKED_DATE_YEAR_SHIFT)
|
|
7738
8244
|
};
|
|
7739
8245
|
return isValidPackedDate(parts) ? parts : null;
|
|
@@ -7769,7 +8275,8 @@ const combineThingsReminderAt = (options) => {
|
|
|
7769
8275
|
};
|
|
7770
8276
|
//#endregion
|
|
7771
8277
|
//#region ../../packages/import-core/src/things/recurrence.ts
|
|
7772
|
-
const
|
|
8278
|
+
const { RRule, rrulestr } = typeof import_rrule.RRule === "function" ? import_rrule : import_rrule.default;
|
|
8279
|
+
const THINGS_FREQUENCY_TO_RRULE = /* @__PURE__ */ new Map([
|
|
7773
8280
|
[4, "YEARLY"],
|
|
7774
8281
|
[8, "MONTHLY"],
|
|
7775
8282
|
[16, "DAILY"],
|
|
@@ -7784,6 +8291,11 @@ const THINGS_WEEKDAY_TO_RRULE = [
|
|
|
7784
8291
|
"FR",
|
|
7785
8292
|
"SA"
|
|
7786
8293
|
];
|
|
8294
|
+
/**
|
|
8295
|
+
* Things writes `ed` as Unix seconds at UTC midnight, and uses 4001-01-01 as
|
|
8296
|
+
* "never ends". Anything at or past it is treated as no end date.
|
|
8297
|
+
*/
|
|
8298
|
+
const THINGS_NEVER_ENDS_SECONDS = 64092211200;
|
|
7787
8299
|
const parseNumericPlistField = (xml, key) => {
|
|
7788
8300
|
const match = new RegExp(`<key>${key}</key>\\s*<(integer|real)>([^<]+)</\\1>`, "u").exec(xml);
|
|
7789
8301
|
if (!match?.[2]) return null;
|
|
@@ -7809,8 +8321,8 @@ const parseOffsets = (xml) => {
|
|
|
7809
8321
|
};
|
|
7810
8322
|
const parseRRule = (rule) => {
|
|
7811
8323
|
try {
|
|
7812
|
-
const parsed =
|
|
7813
|
-
return parsed instanceof
|
|
8324
|
+
const parsed = rrulestr(rule);
|
|
8325
|
+
return parsed instanceof RRule ? parsed : null;
|
|
7814
8326
|
} catch {
|
|
7815
8327
|
return null;
|
|
7816
8328
|
}
|
|
@@ -7819,7 +8331,8 @@ const buildByDayTokens = (definition) => {
|
|
|
7819
8331
|
const tokens = definition.offsets.map((offset) => typeof offset.wd === "number" ? THINGS_WEEKDAY_TO_RRULE[offset.wd] ?? null : null).filter((value) => value !== null);
|
|
7820
8332
|
if (tokens.length > 0) return tokens;
|
|
7821
8333
|
if (definition.ia === null) return [];
|
|
7822
|
-
|
|
8334
|
+
const weekday = (/* @__PURE__ */ new Date(definition.ia * 1e3)).getUTCDay();
|
|
8335
|
+
return [THINGS_WEEKDAY_TO_RRULE[weekday] ?? "MO"];
|
|
7823
8336
|
};
|
|
7824
8337
|
const buildMonthDays = (definition) => {
|
|
7825
8338
|
const monthDays = definition.offsets.map((offset) => typeof offset.dy === "number" ? Math.trunc(offset.dy) + 1 : null).filter((value) => value !== null && value >= 1);
|
|
@@ -7833,6 +8346,10 @@ const buildAnchor = (definition, task, timeZone) => {
|
|
|
7833
8346
|
if (!timeZone) return `DTSTART:${anchor}`;
|
|
7834
8347
|
return `DTSTART;TZID=${timeZone}:${anchor}`;
|
|
7835
8348
|
};
|
|
8349
|
+
/**
|
|
8350
|
+
* Builds the rule without any end. The end is appended separately because a
|
|
8351
|
+
* count has to be resolved against this exact rule first.
|
|
8352
|
+
*/
|
|
7836
8353
|
const buildRepeatRule = (definition, task, timeZone) => {
|
|
7837
8354
|
const frequency = definition.fu === null ? null : THINGS_FREQUENCY_TO_RRULE.get(definition.fu) ?? null;
|
|
7838
8355
|
if (!frequency) return null;
|
|
@@ -7884,21 +8401,77 @@ const parseThingsRecurrenceDefinition = (xml) => {
|
|
|
7884
8401
|
if (definition.ia === null && definition.sr === null && definition.ed === null && definition.rc === null && definition.fu === null && definition.fa === null && definition.tp === null && definition.ts === null && definition.offsets.length === 0) return null;
|
|
7885
8402
|
return definition;
|
|
7886
8403
|
};
|
|
8404
|
+
const resolveThingsRepeatEnd = (definition) => {
|
|
8405
|
+
const untilDate = definition.ed !== null && definition.ed < THINGS_NEVER_ENDS_SECONDS ? decodeThingsUnixCalendarDate(definition.ed, null) : null;
|
|
8406
|
+
const count = definition.rc !== null && definition.rc > 0 ? Math.trunc(definition.rc) : null;
|
|
8407
|
+
return untilDate === null && count === null ? null : {
|
|
8408
|
+
count,
|
|
8409
|
+
untilDate
|
|
8410
|
+
};
|
|
8411
|
+
};
|
|
8412
|
+
/**
|
|
8413
|
+
* Date-only epoch of the last occurrence a count allows, counted from the
|
|
8414
|
+
* rule's DTSTART, or null when the rule has no DTSTART to count from.
|
|
8415
|
+
*
|
|
8416
|
+
* COUNT itself is never written: iOS only honours COUNT <= 0, and rrule counts
|
|
8417
|
+
* from DTSTART while the rule anchor is Things' `ia`. Resolving the count here
|
|
8418
|
+
* against that same anchor and storing the result as UNTIL gives every
|
|
8419
|
+
* platform one date to agree on. `instanceCreationCount` is not used: it
|
|
8420
|
+
* counts instances Things has already made from that same anchor, so it does
|
|
8421
|
+
* not change which occurrence is last. It would matter only if `ia` had moved
|
|
8422
|
+
* since the count began, and nothing in the database records that.
|
|
8423
|
+
*/
|
|
8424
|
+
const resolveLastOccurrenceDate = (repeatRule, count) => {
|
|
8425
|
+
const parsed = parseRRule(`${repeatRule};COUNT=${count}`);
|
|
8426
|
+
if (!parsed?.origOptions.dtstart) return null;
|
|
8427
|
+
const last = parsed.all().at(-1);
|
|
8428
|
+
return last ? normalizeOccurrenceEpoch(last, getRuleTimeZone(parsed)) : null;
|
|
8429
|
+
};
|
|
8430
|
+
/**
|
|
8431
|
+
* The last day a fixed repeat may land on, from Things' end date and count
|
|
8432
|
+
* together (whichever comes first), or null when neither can be resolved.
|
|
8433
|
+
*/
|
|
8434
|
+
const resolveFixedUntilDate = (repeatRule, end) => {
|
|
8435
|
+
const lastByCount = end.count === null ? null : resolveLastOccurrenceDate(repeatRule, end.count);
|
|
8436
|
+
if (end.count !== null && lastByCount === null) return null;
|
|
8437
|
+
const candidates = [end.untilDate, lastByCount].filter((value) => value !== null);
|
|
8438
|
+
return candidates.length > 0 ? Math.min(...candidates) : null;
|
|
8439
|
+
};
|
|
8440
|
+
/**
|
|
8441
|
+
* UNTIL as the end of that calendar day. rrule compares it against the rule's
|
|
8442
|
+
* wall-clock occurrences, so this admits the last day in the rule's own zone,
|
|
8443
|
+
* and iOS reads only the date digits, so both stop on the same day.
|
|
8444
|
+
*
|
|
8445
|
+
* With a TZID the time is local, written without `Z`, because that is how
|
|
8446
|
+
* rrule serializes it and so how manage-api's `canonicalizeRepeatRule` stores
|
|
8447
|
+
* it. Writing `Z` there would have the server rewrite the rule on insert.
|
|
8448
|
+
*/
|
|
8449
|
+
const formatUntil = (untilDate, timeZone) => {
|
|
8450
|
+
const day = formatDateOnlyEpoch(untilDate);
|
|
8451
|
+
return day ? `UNTIL=${day.replaceAll("-", "")}T235959${timeZone ? "" : "Z"}` : null;
|
|
8452
|
+
};
|
|
7887
8453
|
const isThingsRepeatTemplateTask = (task) => task.recurrenceRuleRaw !== null && task.repeatTemplateSourceId === null;
|
|
7888
8454
|
const isThingsRepeatInstanceTask = (task) => task.repeatTemplateSourceId !== null;
|
|
7889
|
-
const resolveThingsRepeatTemplateFields = (task, timeZone) => {
|
|
8455
|
+
const resolveThingsRepeatTemplateFields = (task, timeZone, today) => {
|
|
7890
8456
|
if (!isThingsRepeatTemplateTask(task)) return null;
|
|
7891
8457
|
const recurrence = parseThingsRecurrenceDefinition(task.recurrenceRuleRaw);
|
|
7892
8458
|
if (!recurrence) return null;
|
|
7893
8459
|
const repeatTimeZone = resolveTimeZoneOrNull(timeZone);
|
|
7894
|
-
const
|
|
7895
|
-
if (!
|
|
7896
|
-
const currentInstanceDate = decodeThingsPackedCalendarDate(task.nextInstanceStartDate, repeatTimeZone) ?? decodeThingsPackedCalendarDate(task.startDate, repeatTimeZone) ?? decodeThingsUnixCalendarDate(recurrence.ia, repeatTimeZone) ?? decodeThingsUnixSeconds(recurrence.ia);
|
|
8460
|
+
const openRule = buildRepeatRule(recurrence, task, repeatTimeZone);
|
|
8461
|
+
if (!openRule) return null;
|
|
7897
8462
|
const repeatType = recurrence.tp === 1 ? "after_completion" : "fixed";
|
|
8463
|
+
const end = resolveThingsRepeatEnd(recurrence);
|
|
8464
|
+
const untilDate = end && repeatType === "fixed" ? resolveFixedUntilDate(openRule, end) : null;
|
|
8465
|
+
const until = untilDate === null ? null : formatUntil(untilDate, repeatTimeZone);
|
|
8466
|
+
const repeatRule = until ? `${openRule};${until}` : openRule;
|
|
8467
|
+
const currentInstanceDate = decodeThingsPackedCalendarDate(task.nextInstanceStartDate, repeatTimeZone) ?? decodeThingsPackedCalendarDate(task.startDate, repeatTimeZone) ?? decodeThingsUnixCalendarDate(recurrence.ia, repeatTimeZone) ?? decodeThingsUnixSeconds(recurrence.ia);
|
|
8468
|
+
const ended = untilDate !== null && (untilDate < today || currentInstanceDate !== null && currentInstanceDate > untilDate);
|
|
7898
8469
|
return {
|
|
7899
8470
|
currentInstanceDate,
|
|
8471
|
+
droppedEnd: until ? null : end,
|
|
8472
|
+
ended,
|
|
7900
8473
|
recurrence,
|
|
7901
|
-
repeatNextAt: repeatType === "fixed" ? calculateNextOccurrence(repeatRule, currentInstanceDate) : null,
|
|
8474
|
+
repeatNextAt: repeatType === "fixed" && !ended ? calculateNextOccurrence(repeatRule, currentInstanceDate) : null,
|
|
7902
8475
|
repeatRule,
|
|
7903
8476
|
repeatStatus: task.instanceCreationPaused || recurrence.ts === 1 ? "paused" : "active",
|
|
7904
8477
|
repeatType
|
|
@@ -8004,11 +8577,15 @@ const buildReminderAt = (task, startDate) => combineThingsReminderAt({
|
|
|
8004
8577
|
reminderTime: task.reminderTime,
|
|
8005
8578
|
startDate
|
|
8006
8579
|
});
|
|
8007
|
-
|
|
8580
|
+
/**
|
|
8581
|
+
* `fallback` is the import time. A trashed task with no dates of its own must
|
|
8582
|
+
* still get an `archivedAt`, or it lands outside Trash.
|
|
8583
|
+
*/
|
|
8584
|
+
const pickArchivedAt = (task, fallback) => {
|
|
8008
8585
|
const updatedAt = decodeThingsUnixSeconds(task.userModificationDate);
|
|
8009
8586
|
const completedAt = decodeThingsUnixSeconds(task.stopDate);
|
|
8010
8587
|
const createdAt = decodeThingsUnixSeconds(task.creationDate);
|
|
8011
|
-
if (task.trashed || task.status === 2) return updatedAt ?? completedAt ?? createdAt;
|
|
8588
|
+
if (task.trashed || task.status === 2) return updatedAt ?? completedAt ?? createdAt ?? fallback;
|
|
8012
8589
|
return null;
|
|
8013
8590
|
};
|
|
8014
8591
|
const pickCompletedAt = (task) => {
|
|
@@ -8016,14 +8593,14 @@ const pickCompletedAt = (task) => {
|
|
|
8016
8593
|
return decodeThingsUnixSeconds(task.stopDate) ?? decodeThingsUnixSeconds(task.userModificationDate) ?? decodeThingsUnixSeconds(task.creationDate);
|
|
8017
8594
|
};
|
|
8018
8595
|
const sortValue = (primary, fallback) => typeof primary === "number" ? primary : fallback;
|
|
8019
|
-
const textDecoder = new TextDecoder();
|
|
8596
|
+
const textDecoder$1 = new TextDecoder();
|
|
8020
8597
|
const encodeBase64 = (value) => {
|
|
8021
8598
|
let binary = "";
|
|
8022
8599
|
for (const byte of value) binary += String.fromCodePoint(byte);
|
|
8023
8600
|
return btoa(binary);
|
|
8024
8601
|
};
|
|
8025
8602
|
const sanitizeBinary = (value) => {
|
|
8026
|
-
const asText = textDecoder.decode(value);
|
|
8603
|
+
const asText = textDecoder$1.decode(value);
|
|
8027
8604
|
if (!asText.includes("�") && READABLE_TEXT_PATTERN.test(asText)) {
|
|
8028
8605
|
const trimmed = asText.trim();
|
|
8029
8606
|
if (trimmed.length > 0) return trimmed;
|
|
@@ -8161,7 +8738,7 @@ const buildTagTitleMap = (tags) => {
|
|
|
8161
8738
|
const buildChecklistMutation = (options) => {
|
|
8162
8739
|
const sourceId = options.sourceId ?? options.item.uuid;
|
|
8163
8740
|
const title = normalizeTitle(options.item.title, `Checklist item ${options.item.uuid.slice(0, 8)}`);
|
|
8164
|
-
const safeTitle = title.length > MAX_CHECKLIST_TITLE_LENGTH ? `${title.slice(0,
|
|
8741
|
+
const safeTitle = title.length > MAX_CHECKLIST_TITLE_LENGTH ? `${title.slice(0, 497)}...` : title;
|
|
8165
8742
|
return {
|
|
8166
8743
|
modelId: createDeterministicImportId({
|
|
8167
8744
|
modelName: "TaskChecklistItem",
|
|
@@ -8183,19 +8760,25 @@ const buildChecklistMutation = (options) => {
|
|
|
8183
8760
|
};
|
|
8184
8761
|
const buildTeamActions = (ctx, areas) => {
|
|
8185
8762
|
const teams = [];
|
|
8763
|
+
const teamMemberships = [];
|
|
8764
|
+
const membershipIds = /* @__PURE__ */ new Set();
|
|
8186
8765
|
for (const area of [...areas].toSorted((a, b) => {
|
|
8187
8766
|
const indexComparison = compareNullableNumber(a.sortIndex, b.sortIndex);
|
|
8188
8767
|
if (indexComparison !== 0) return indexComparison;
|
|
8189
8768
|
return a.uuid.localeCompare(b.uuid);
|
|
8190
8769
|
})) {
|
|
8191
|
-
const
|
|
8770
|
+
const teamName = normalizeTitle(area.title, `Area ${area.uuid.slice(0, 8)}`);
|
|
8771
|
+
const teamLookupKey = normalizeThingsTeamLookupKey(teamName);
|
|
8772
|
+
const existingTeamId = ctx.existingTeamIdByName.get(teamLookupKey);
|
|
8773
|
+
const teamId = existingTeamId ?? createDeterministicImportId({
|
|
8192
8774
|
modelName: "Team",
|
|
8193
8775
|
sourceId: area.uuid,
|
|
8194
8776
|
workspaceId: ctx.workspaceId
|
|
8195
8777
|
});
|
|
8778
|
+
ctx.existingTeamIdByName.set(teamLookupKey, teamId);
|
|
8196
8779
|
ctx.areaTeamIdBySourceId.set(area.uuid, teamId);
|
|
8197
8780
|
const areaTagTitles = (ctx.areaTagMap.get(area.uuid) ?? []).map((link) => ctx.tagTitleById.get(link.tagId) ?? null).filter((title) => typeof title === "string");
|
|
8198
|
-
teams.push({
|
|
8781
|
+
if (!existingTeamId) teams.push({
|
|
8199
8782
|
modelId: teamId,
|
|
8200
8783
|
modelName: "Team",
|
|
8201
8784
|
payload: {
|
|
@@ -8204,14 +8787,36 @@ const buildTeamActions = (ctx, areas) => {
|
|
|
8204
8787
|
description: appendMetadataBlock(null, ctx.metadataMode, buildAreaMetadata(area, areaTagTitles)),
|
|
8205
8788
|
icon: null,
|
|
8206
8789
|
key: ctx.teamKeys.get(area.uuid) ?? "AREA",
|
|
8207
|
-
name:
|
|
8790
|
+
name: teamName,
|
|
8208
8791
|
updatedAt: ctx.createdAtFallback,
|
|
8209
8792
|
workspaceId: ctx.workspaceId
|
|
8210
8793
|
},
|
|
8211
8794
|
sourceId: area.uuid
|
|
8212
8795
|
});
|
|
8796
|
+
const membershipId = createTeamMembershipId({
|
|
8797
|
+
teamId,
|
|
8798
|
+
userId: ctx.creatorId,
|
|
8799
|
+
workspaceId: ctx.workspaceId
|
|
8800
|
+
});
|
|
8801
|
+
if (!membershipIds.has(membershipId)) {
|
|
8802
|
+
membershipIds.add(membershipId);
|
|
8803
|
+
teamMemberships.push({
|
|
8804
|
+
modelId: membershipId,
|
|
8805
|
+
modelName: "TeamMembership",
|
|
8806
|
+
payload: {
|
|
8807
|
+
createdAt: ctx.createdAtFallback,
|
|
8808
|
+
teamId,
|
|
8809
|
+
userId: ctx.creatorId,
|
|
8810
|
+
workspaceId: ctx.workspaceId
|
|
8811
|
+
},
|
|
8812
|
+
sourceId: `${area.uuid}:${ctx.creatorId}`
|
|
8813
|
+
});
|
|
8814
|
+
}
|
|
8213
8815
|
}
|
|
8214
|
-
return
|
|
8816
|
+
return {
|
|
8817
|
+
teamMemberships,
|
|
8818
|
+
teams
|
|
8819
|
+
};
|
|
8215
8820
|
};
|
|
8216
8821
|
const buildLabelActions = (ctx, tags) => {
|
|
8217
8822
|
const labels = [];
|
|
@@ -8244,6 +8849,7 @@ const buildLabelActions = (ctx, tags) => {
|
|
|
8244
8849
|
};
|
|
8245
8850
|
const buildProjectActions = (ctx, sourceProjects) => {
|
|
8246
8851
|
const projects = [];
|
|
8852
|
+
const projectMemberships = [];
|
|
8247
8853
|
const teamProjects = [];
|
|
8248
8854
|
for (const project of [...sourceProjects].toSorted((a, b) => {
|
|
8249
8855
|
const indexComparison = compareNullableNumber(a.sortIndex, b.sortIndex);
|
|
@@ -8262,7 +8868,7 @@ const buildProjectActions = (ctx, sourceProjects) => {
|
|
|
8262
8868
|
modelId: projectId,
|
|
8263
8869
|
modelName: "Project",
|
|
8264
8870
|
payload: {
|
|
8265
|
-
archivedAt: pickArchivedAt(project),
|
|
8871
|
+
archivedAt: pickArchivedAt(project, ctx.createdAtFallback),
|
|
8266
8872
|
color: null,
|
|
8267
8873
|
completedAt: pickCompletedAt(project),
|
|
8268
8874
|
createdAt: decodeThingsUnixSeconds(project.creationDate) ?? ctx.createdAtFallback,
|
|
@@ -8279,6 +8885,21 @@ const buildProjectActions = (ctx, sourceProjects) => {
|
|
|
8279
8885
|
},
|
|
8280
8886
|
sourceId: project.uuid
|
|
8281
8887
|
});
|
|
8888
|
+
projectMemberships.push({
|
|
8889
|
+
modelId: createProjectMembershipId({
|
|
8890
|
+
projectId,
|
|
8891
|
+
userId: ctx.creatorId,
|
|
8892
|
+
workspaceId: ctx.workspaceId
|
|
8893
|
+
}),
|
|
8894
|
+
modelName: "ProjectMembership",
|
|
8895
|
+
payload: {
|
|
8896
|
+
createdAt: decodeThingsUnixSeconds(project.creationDate) ?? ctx.createdAtFallback,
|
|
8897
|
+
projectId,
|
|
8898
|
+
userId: ctx.creatorId,
|
|
8899
|
+
workspaceId: ctx.workspaceId
|
|
8900
|
+
},
|
|
8901
|
+
sourceId: `${project.uuid}:${ctx.creatorId}`
|
|
8902
|
+
});
|
|
8282
8903
|
if (project.areaId !== null) {
|
|
8283
8904
|
const teamId = ctx.areaTeamIdBySourceId.get(project.areaId);
|
|
8284
8905
|
if (teamId) teamProjects.push({
|
|
@@ -8299,6 +8920,7 @@ const buildProjectActions = (ctx, sourceProjects) => {
|
|
|
8299
8920
|
}
|
|
8300
8921
|
}
|
|
8301
8922
|
return {
|
|
8923
|
+
projectMemberships,
|
|
8302
8924
|
projects,
|
|
8303
8925
|
teamProjects
|
|
8304
8926
|
};
|
|
@@ -8324,7 +8946,7 @@ const buildHeadingActions = (ctx, sourceHeadings) => {
|
|
|
8324
8946
|
modelId: headingId,
|
|
8325
8947
|
modelName: "ProjectHeading",
|
|
8326
8948
|
payload: {
|
|
8327
|
-
archivedAt: pickArchivedAt(heading),
|
|
8949
|
+
archivedAt: pickArchivedAt(heading, ctx.createdAtFallback),
|
|
8328
8950
|
createdAt: decodeThingsUnixSeconds(heading.creationDate) ?? ctx.createdAtFallback,
|
|
8329
8951
|
projectId: parentProjectId,
|
|
8330
8952
|
sortOrder: sortValue(heading.sortIndex, headings.length),
|
|
@@ -8395,7 +9017,7 @@ const pushTaskMutation = (ctx, tasks, taskLabels, checklistItems, mutationOption
|
|
|
8395
9017
|
modelId: mutationOptions.taskId,
|
|
8396
9018
|
modelName: "Task",
|
|
8397
9019
|
payload: {
|
|
8398
|
-
archivedAt: pickArchivedAt(mutationOptions.task),
|
|
9020
|
+
archivedAt: pickArchivedAt(mutationOptions.task, ctx.createdAtFallback),
|
|
8399
9021
|
assigneeId: null,
|
|
8400
9022
|
completedAt: pickCompletedAt(mutationOptions.task),
|
|
8401
9023
|
createdAt,
|
|
@@ -8435,8 +9057,22 @@ const pushTaskMutation = (ctx, tasks, taskLabels, checklistItems, mutationOption
|
|
|
8435
9057
|
});
|
|
8436
9058
|
return createdAt;
|
|
8437
9059
|
};
|
|
9060
|
+
/**
|
|
9061
|
+
* The repeat is imported without its end, so it keeps going. Name the task in
|
|
9062
|
+
* the preview rather than let the user find out when it comes back.
|
|
9063
|
+
*/
|
|
9064
|
+
const buildRepeatEndIssue = (task, end, repeatType) => {
|
|
9065
|
+
const title = normalizeTitle(task.title, `Task ${task.uuid.slice(0, 8)}`);
|
|
9066
|
+
const until = formatDateOnlyEpoch(end.untilDate);
|
|
9067
|
+
const thingsEnd = until ? `stops repeating on ${until} in Things` : `repeats ${end.count} ${end.count === 1 ? "time" : "times"} in Things`;
|
|
9068
|
+
const outcome = repeatType === "after_completion" ? "In Done Bear it repeats after each completion with no end, so stop it by hand when you're done." : "That end won't carry over, so it keeps repeating in Done Bear.";
|
|
9069
|
+
return createIssue(ImportIssueCode.RECURRENCE_APPROXIMATED, `"${title}" ${thingsEnd}. ${outcome}`, {
|
|
9070
|
+
field: "repeatRule",
|
|
9071
|
+
sourceRecordId: task.uuid
|
|
9072
|
+
});
|
|
9073
|
+
};
|
|
8438
9074
|
const planRepeatTemplateTask = (ctx, task, taskId, openRepeatInstanceTemplateIds, tasks, taskLabels, checklistItems) => {
|
|
8439
|
-
const repeatTemplate = resolveThingsRepeatTemplateFields(task, ctx.timeZone);
|
|
9075
|
+
const repeatTemplate = resolveThingsRepeatTemplateFields(task, ctx.timeZone, ctx.todayDateOnly);
|
|
8440
9076
|
if (!repeatTemplate) {
|
|
8441
9077
|
pushTaskMutation(ctx, tasks, taskLabels, checklistItems, {
|
|
8442
9078
|
sourceId: task.uuid,
|
|
@@ -8445,6 +9081,8 @@ const planRepeatTemplateTask = (ctx, task, taskId, openRepeatInstanceTemplateIds
|
|
|
8445
9081
|
});
|
|
8446
9082
|
return;
|
|
8447
9083
|
}
|
|
9084
|
+
const archived = pickArchivedAt(task, ctx.createdAtFallback) !== null;
|
|
9085
|
+
if (repeatTemplate.droppedEnd && !archived) ctx.issues.push(buildRepeatEndIssue(task, repeatTemplate.droppedEnd, repeatTemplate.repeatType));
|
|
8448
9086
|
pushTaskMutation(ctx, tasks, taskLabels, checklistItems, {
|
|
8449
9087
|
repeat: {
|
|
8450
9088
|
repeatInstanceDate: null,
|
|
@@ -8458,7 +9096,7 @@ const planRepeatTemplateTask = (ctx, task, taskId, openRepeatInstanceTemplateIds
|
|
|
8458
9096
|
task,
|
|
8459
9097
|
taskId
|
|
8460
9098
|
});
|
|
8461
|
-
if (pickCompletedAt(task) !== null ||
|
|
9099
|
+
if (pickCompletedAt(task) !== null || archived || repeatTemplate.ended || openRepeatInstanceTemplateIds.has(task.uuid)) return;
|
|
8462
9100
|
const currentInstanceTaskId = createDeterministicImportId({
|
|
8463
9101
|
modelName: "Task",
|
|
8464
9102
|
sourceId: `${task.uuid}:current-instance`,
|
|
@@ -8497,7 +9135,7 @@ const buildTaskActions = (ctx, sourceTasks) => {
|
|
|
8497
9135
|
if (todayComparison !== 0) return todayComparison;
|
|
8498
9136
|
return a.uuid.localeCompare(b.uuid);
|
|
8499
9137
|
});
|
|
8500
|
-
const openRepeatInstanceTemplateIds = new Set(sourceTasks.filter(isThingsRepeatInstanceTask).filter((task) => pickCompletedAt(task) === null && pickArchivedAt(task) === null).map((task) => task.repeatTemplateSourceId).filter((value) => typeof value === "string"));
|
|
9138
|
+
const openRepeatInstanceTemplateIds = new Set(sourceTasks.filter(isThingsRepeatInstanceTask).filter((task) => pickCompletedAt(task) === null && pickArchivedAt(task, ctx.createdAtFallback) === null).map((task) => task.repeatTemplateSourceId).filter((value) => typeof value === "string"));
|
|
8501
9139
|
for (const task of sortedSourceTasks) ctx.taskIdBySourceId.set(task.uuid, createDeterministicImportId({
|
|
8502
9140
|
modelName: "Task",
|
|
8503
9141
|
sourceId: task.uuid,
|
|
@@ -8540,6 +9178,45 @@ const buildTaskActions = (ctx, sourceTasks) => {
|
|
|
8540
9178
|
tasks
|
|
8541
9179
|
};
|
|
8542
9180
|
};
|
|
9181
|
+
/**
|
|
9182
|
+
* Still to do in Things: not completed (status 3), not canceled (status 2),
|
|
9183
|
+
* not in the Trash. Canceled and trashed items would arrive in Done Bear's
|
|
9184
|
+
* Trash anyway, and someone asking for open items does not want them.
|
|
9185
|
+
*/
|
|
9186
|
+
const isThingsItemOpen = (task) => !task.trashed && task.status !== 2 && task.status !== 3;
|
|
9187
|
+
/**
|
|
9188
|
+
* Decides, once and here, what `openOnly` keeps:
|
|
9189
|
+
*
|
|
9190
|
+
* - To-dos: only open ones. That includes repeat templates (the rule lives on
|
|
9191
|
+
* them) and open repeat instances; completed instances are the Logbook
|
|
9192
|
+
* history of a repeat and are left behind. An open instance whose template
|
|
9193
|
+
* is left behind keeps its date but loses the link, since the template is
|
|
9194
|
+
* not written.
|
|
9195
|
+
* - Projects: an open project always comes; a finished one comes only when an
|
|
9196
|
+
* open to-do still lives in it (directly or under one of its headings), and
|
|
9197
|
+
* keeps its finished state. Its headings follow it.
|
|
9198
|
+
* - Checklist items follow their to-do, ticked ones included: they are part of
|
|
9199
|
+
* an open to-do's state and do not count as tasks.
|
|
9200
|
+
* - Areas and tags are not filtered.
|
|
9201
|
+
*/
|
|
9202
|
+
const selectOpenOnly = (sourceProjects, sourceHeadings, sourceTasks) => {
|
|
9203
|
+
const tasks = sourceTasks.filter(isThingsItemOpen);
|
|
9204
|
+
const projectIdByHeadingId = new Map(sourceHeadings.map((heading) => [heading.uuid, heading.projectId]));
|
|
9205
|
+
const projectIdsWithOpenTasks = /* @__PURE__ */ new Set();
|
|
9206
|
+
for (const task of tasks) {
|
|
9207
|
+
const projectId = task.projectId ?? (task.headingId === null ? null : projectIdByHeadingId.get(task.headingId) ?? null);
|
|
9208
|
+
if (projectId !== null) projectIdsWithOpenTasks.add(projectId);
|
|
9209
|
+
}
|
|
9210
|
+
const projects = sourceProjects.filter((project) => isThingsItemOpen(project) || projectIdsWithOpenTasks.has(project.uuid));
|
|
9211
|
+
return {
|
|
9212
|
+
leftBehind: {
|
|
9213
|
+
projects: sourceProjects.length - projects.length,
|
|
9214
|
+
tasks: sourceTasks.length - tasks.length
|
|
9215
|
+
},
|
|
9216
|
+
projects,
|
|
9217
|
+
tasks
|
|
9218
|
+
};
|
|
9219
|
+
};
|
|
8543
9220
|
const buildThingsImportPlan = (options) => {
|
|
8544
9221
|
const { snapshot, workspaceId, existingLabelsByTitle } = options;
|
|
8545
9222
|
const ctx = {
|
|
@@ -8551,7 +9228,9 @@ const buildThingsImportPlan = (options) => {
|
|
|
8551
9228
|
createdAtFallback: Date.now(),
|
|
8552
9229
|
creatorId: options.creatorId,
|
|
8553
9230
|
existingLabelIdByTitle: existingLabelsByTitle ?? /* @__PURE__ */ new Map(),
|
|
9231
|
+
existingTeamIdByName: options.existingTeamsByName ?? /* @__PURE__ */ new Map(),
|
|
8554
9232
|
headingIdBySourceId: /* @__PURE__ */ new Map(),
|
|
9233
|
+
issues: [],
|
|
8555
9234
|
metadataMode: options.metadataMode,
|
|
8556
9235
|
projectIdBySourceId: /* @__PURE__ */ new Map(),
|
|
8557
9236
|
projectKeyCandidates: buildUniqueKeys(snapshot.tasks.filter((task) => classifyTaskKind(task) === "project"), {
|
|
@@ -8570,19 +9249,34 @@ const buildThingsImportPlan = (options) => {
|
|
|
8570
9249
|
todayDateOnly: localDateToDateOnlyEpoch(/* @__PURE__ */ new Date()),
|
|
8571
9250
|
workspaceId
|
|
8572
9251
|
};
|
|
8573
|
-
const teams = options.skipTeams ?
|
|
9252
|
+
const { teamMemberships, teams } = options.skipTeams ? {
|
|
9253
|
+
teamMemberships: [],
|
|
9254
|
+
teams: []
|
|
9255
|
+
} : buildTeamActions(ctx, snapshot.areas);
|
|
8574
9256
|
const labels = buildLabelActions(ctx, snapshot.tags);
|
|
8575
9257
|
const sourceProjects = snapshot.tasks.filter((task) => classifyTaskKind(task) === "project");
|
|
8576
9258
|
const sourceHeadings = snapshot.tasks.filter((task) => classifyTaskKind(task) === "heading");
|
|
8577
9259
|
const sourceTasks = snapshot.tasks.filter((task) => classifyTaskKind(task) === "task");
|
|
8578
|
-
const
|
|
9260
|
+
const selected = options.openOnly ? selectOpenOnly(sourceProjects, sourceHeadings, sourceTasks) : {
|
|
9261
|
+
leftBehind: {
|
|
9262
|
+
projects: 0,
|
|
9263
|
+
tasks: 0
|
|
9264
|
+
},
|
|
9265
|
+
projects: sourceProjects,
|
|
9266
|
+
tasks: sourceTasks
|
|
9267
|
+
};
|
|
9268
|
+
const { projectMemberships, projects, teamProjects } = buildProjectActions(ctx, selected.projects);
|
|
8579
9269
|
const headings = buildHeadingActions(ctx, sourceHeadings);
|
|
8580
|
-
const { tasks, taskLabels, checklistItems } = buildTaskActions(ctx,
|
|
9270
|
+
const { tasks, taskLabels, checklistItems } = buildTaskActions(ctx, selected.tasks);
|
|
8581
9271
|
const phases = [
|
|
8582
9272
|
{
|
|
8583
9273
|
items: teams,
|
|
8584
9274
|
name: "teams"
|
|
8585
9275
|
},
|
|
9276
|
+
{
|
|
9277
|
+
items: teamMemberships,
|
|
9278
|
+
name: "teamMemberships"
|
|
9279
|
+
},
|
|
8586
9280
|
{
|
|
8587
9281
|
items: labels,
|
|
8588
9282
|
name: "labels"
|
|
@@ -8591,6 +9285,10 @@ const buildThingsImportPlan = (options) => {
|
|
|
8591
9285
|
items: projects,
|
|
8592
9286
|
name: "projects"
|
|
8593
9287
|
},
|
|
9288
|
+
{
|
|
9289
|
+
items: projectMemberships,
|
|
9290
|
+
name: "projectMemberships"
|
|
9291
|
+
},
|
|
8594
9292
|
{
|
|
8595
9293
|
items: teamProjects,
|
|
8596
9294
|
name: "teamProjects"
|
|
@@ -8614,6 +9312,7 @@ const buildThingsImportPlan = (options) => {
|
|
|
8614
9312
|
];
|
|
8615
9313
|
return {
|
|
8616
9314
|
counts: {
|
|
9315
|
+
leftBehind: selected.leftBehind,
|
|
8617
9316
|
planned: Object.fromEntries(phases.map((phase) => [phase.name, phase.items.length])),
|
|
8618
9317
|
source: {
|
|
8619
9318
|
areaTagLinks: snapshot.areaTags.length,
|
|
@@ -8627,11 +9326,161 @@ const buildThingsImportPlan = (options) => {
|
|
|
8627
9326
|
}
|
|
8628
9327
|
},
|
|
8629
9328
|
databasePath: snapshot.databasePath,
|
|
9329
|
+
issues: ctx.issues,
|
|
8630
9330
|
phases,
|
|
8631
9331
|
taskColumns: snapshot.taskColumns
|
|
8632
9332
|
};
|
|
8633
9333
|
};
|
|
8634
9334
|
//#endregion
|
|
9335
|
+
//#region ../../packages/import-core/src/things/snapshot.ts
|
|
9336
|
+
const textDecoder = new TextDecoder();
|
|
9337
|
+
const THINGS_REQUIRED_TABLE = "TMTask";
|
|
9338
|
+
const THINGS_SQLITE_FILE = "main.sqlite";
|
|
9339
|
+
const LEGACY_THINGS_SQLITE_FILE = "Things.sqlite3";
|
|
9340
|
+
const toThingsStringValue = (value) => {
|
|
9341
|
+
if (typeof value === "string") {
|
|
9342
|
+
const trimmed = value.trim();
|
|
9343
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
9344
|
+
}
|
|
9345
|
+
if (value instanceof Uint8Array) {
|
|
9346
|
+
const text = textDecoder.decode(value).trim();
|
|
9347
|
+
return text.length > 0 ? text : null;
|
|
9348
|
+
}
|
|
9349
|
+
return null;
|
|
9350
|
+
};
|
|
9351
|
+
const toThingsNumberValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
9352
|
+
const toThingsBooleanValue = (value) => {
|
|
9353
|
+
const numeric = toThingsNumberValue(value);
|
|
9354
|
+
return numeric === null ? null : numeric !== 0;
|
|
9355
|
+
};
|
|
9356
|
+
const normalizeThingsTaskRecord = (row) => {
|
|
9357
|
+
const uuid = toThingsStringValue(row.uuid);
|
|
9358
|
+
if (!uuid) return null;
|
|
9359
|
+
return {
|
|
9360
|
+
afterCompletionReferenceDate: toThingsNumberValue(row.rt1_afterCompletionReferenceDate ?? row.afterCompletionReferenceDate),
|
|
9361
|
+
areaId: toThingsStringValue(row.area),
|
|
9362
|
+
contactId: toThingsStringValue(row.contact ?? row.delegate),
|
|
9363
|
+
creationDate: toThingsNumberValue(row.creationDate),
|
|
9364
|
+
deadline: toThingsNumberValue(row.deadline ?? row.dueDate),
|
|
9365
|
+
deadlineOffset: toThingsNumberValue(row.t2_deadlineOffset ?? row.dueDateOffset),
|
|
9366
|
+
deadlineSuppressionDate: toThingsNumberValue(row.deadlineSuppressionDate ?? row.dueDateSuppressionDate),
|
|
9367
|
+
headingId: toThingsStringValue(row.heading ?? row.actionGroup),
|
|
9368
|
+
instanceCreationCount: toThingsNumberValue(row.rt1_instanceCreationCount ?? row.instanceCreationCount),
|
|
9369
|
+
instanceCreationPaused: toThingsBooleanValue(row.rt1_instanceCreationPaused ?? row.instanceCreationPaused) ?? false,
|
|
9370
|
+
instanceCreationStartDate: toThingsNumberValue(row.rt1_instanceCreationStartDate ?? row.instanceCreationStartDate),
|
|
9371
|
+
lastReminderInteractionDate: toThingsNumberValue(row.lastReminderInteractionDate ?? row.lastAlarmInteractionDate),
|
|
9372
|
+
nextInstanceStartDate: toThingsNumberValue(row.rt1_nextInstanceStartDate ?? row.nextInstanceStartDate),
|
|
9373
|
+
notes: toThingsStringValue(row.notes),
|
|
9374
|
+
projectId: toThingsStringValue(row.project),
|
|
9375
|
+
raw: row,
|
|
9376
|
+
recurrenceRuleRaw: toThingsStringValue(row.rt1_recurrenceRule ?? row.recurrenceRule),
|
|
9377
|
+
reminderTime: toThingsNumberValue(row.reminderTime),
|
|
9378
|
+
repeatTemplateSourceId: toThingsStringValue(row.rt1_repeatingTemplate ?? row.repeatingTemplate),
|
|
9379
|
+
repeaterMigrationDate: toThingsNumberValue(row.repeaterMigrationDate),
|
|
9380
|
+
repeaterRaw: toThingsStringValue(row.repeater),
|
|
9381
|
+
sortIndex: toThingsNumberValue(row.index),
|
|
9382
|
+
start: toThingsNumberValue(row.start),
|
|
9383
|
+
startBucket: toThingsNumberValue(row.startBucket),
|
|
9384
|
+
startDate: toThingsNumberValue(row.startDate),
|
|
9385
|
+
status: toThingsNumberValue(row.status),
|
|
9386
|
+
stopDate: toThingsNumberValue(row.stopDate),
|
|
9387
|
+
title: toThingsStringValue(row.title),
|
|
9388
|
+
todayIndex: toThingsNumberValue(row.todayIndex),
|
|
9389
|
+
todayIndexReferenceDate: toThingsNumberValue(row.todayIndexReferenceDate),
|
|
9390
|
+
trashed: toThingsBooleanValue(row.trashed) ?? false,
|
|
9391
|
+
type: toThingsNumberValue(row.type),
|
|
9392
|
+
userModificationDate: toThingsNumberValue(row.userModificationDate),
|
|
9393
|
+
uuid
|
|
9394
|
+
};
|
|
9395
|
+
};
|
|
9396
|
+
const normalizeThingsAreaRecord = (row) => {
|
|
9397
|
+
const uuid = toThingsStringValue(row.uuid);
|
|
9398
|
+
if (!uuid) return null;
|
|
9399
|
+
return {
|
|
9400
|
+
raw: row,
|
|
9401
|
+
sortIndex: toThingsNumberValue(row.index),
|
|
9402
|
+
title: toThingsStringValue(row.title),
|
|
9403
|
+
uuid,
|
|
9404
|
+
visible: toThingsBooleanValue(row.visible)
|
|
9405
|
+
};
|
|
9406
|
+
};
|
|
9407
|
+
const normalizeThingsTagRecord = (row) => {
|
|
9408
|
+
const uuid = toThingsStringValue(row.uuid);
|
|
9409
|
+
if (!uuid) return null;
|
|
9410
|
+
return {
|
|
9411
|
+
parentId: toThingsStringValue(row.parent),
|
|
9412
|
+
raw: row,
|
|
9413
|
+
shortcut: toThingsStringValue(row.shortcut),
|
|
9414
|
+
sortIndex: toThingsNumberValue(row.index),
|
|
9415
|
+
title: toThingsStringValue(row.title),
|
|
9416
|
+
usedDate: toThingsNumberValue(row.usedDate),
|
|
9417
|
+
uuid
|
|
9418
|
+
};
|
|
9419
|
+
};
|
|
9420
|
+
const normalizeThingsChecklistItemRecord = (row) => {
|
|
9421
|
+
const uuid = toThingsStringValue(row.uuid);
|
|
9422
|
+
if (!uuid) return null;
|
|
9423
|
+
return {
|
|
9424
|
+
creationDate: toThingsNumberValue(row.creationDate),
|
|
9425
|
+
raw: row,
|
|
9426
|
+
sortIndex: toThingsNumberValue(row.index),
|
|
9427
|
+
status: toThingsNumberValue(row.status),
|
|
9428
|
+
stopDate: toThingsNumberValue(row.stopDate),
|
|
9429
|
+
taskId: toThingsStringValue(row.task),
|
|
9430
|
+
title: toThingsStringValue(row.title),
|
|
9431
|
+
userModificationDate: toThingsNumberValue(row.userModificationDate),
|
|
9432
|
+
uuid
|
|
9433
|
+
};
|
|
9434
|
+
};
|
|
9435
|
+
const normalizeThingsTaskTagRecord = (row) => {
|
|
9436
|
+
const taskId = toThingsStringValue(row.tasks);
|
|
9437
|
+
const tagId = toThingsStringValue(row.tags);
|
|
9438
|
+
return taskId && tagId ? {
|
|
9439
|
+
tagId,
|
|
9440
|
+
taskId
|
|
9441
|
+
} : null;
|
|
9442
|
+
};
|
|
9443
|
+
const normalizeThingsAreaTagRecord = (row) => {
|
|
9444
|
+
const areaId = toThingsStringValue(row.areas);
|
|
9445
|
+
const tagId = toThingsStringValue(row.tags);
|
|
9446
|
+
return areaId && tagId ? {
|
|
9447
|
+
areaId,
|
|
9448
|
+
tagId
|
|
9449
|
+
} : null;
|
|
9450
|
+
};
|
|
9451
|
+
const normalizeThingsContactRecord = (row) => {
|
|
9452
|
+
const uuid = toThingsStringValue(row.uuid);
|
|
9453
|
+
if (!uuid) return null;
|
|
9454
|
+
return {
|
|
9455
|
+
appleAddressBookId: toThingsStringValue(row.appleAddressBookId),
|
|
9456
|
+
displayName: toThingsStringValue(row.displayName),
|
|
9457
|
+
emails: toThingsStringValue(row.emails),
|
|
9458
|
+
firstName: toThingsStringValue(row.firstName),
|
|
9459
|
+
lastName: toThingsStringValue(row.lastName),
|
|
9460
|
+
raw: row,
|
|
9461
|
+
uuid
|
|
9462
|
+
};
|
|
9463
|
+
};
|
|
9464
|
+
const mapPresent = (rows, normalize) => rows.map(normalize).filter((record) => record !== null);
|
|
9465
|
+
/**
|
|
9466
|
+
* Turns raw Things tables into the snapshot `buildThingsImportPlan` consumes.
|
|
9467
|
+
* Callers supply the SQL; this is the one place row → record mapping lives.
|
|
9468
|
+
*/
|
|
9469
|
+
const buildThingsDatabaseSnapshot = (reader, databasePath) => {
|
|
9470
|
+
if (!reader.hasTable("TMTask")) throw new Error("This does not look like a Things 3 database. Select the main.sqlite file that contains the TMTask table.");
|
|
9471
|
+
return {
|
|
9472
|
+
areaTags: reader.hasTable("TMAreaTag") ? mapPresent(reader.readRows("TMAreaTag"), normalizeThingsAreaTagRecord) : [],
|
|
9473
|
+
areas: reader.hasTable("TMArea") ? mapPresent(reader.readRows("TMArea"), normalizeThingsAreaRecord) : [],
|
|
9474
|
+
checklistItems: reader.hasTable("TMChecklistItem") ? mapPresent(reader.readRows("TMChecklistItem"), normalizeThingsChecklistItemRecord) : [],
|
|
9475
|
+
contacts: reader.hasTable("TMContact") ? mapPresent(reader.readRows("TMContact"), normalizeThingsContactRecord) : [],
|
|
9476
|
+
databasePath,
|
|
9477
|
+
tags: reader.hasTable("TMTag") ? mapPresent(reader.readRows("TMTag"), normalizeThingsTagRecord) : [],
|
|
9478
|
+
taskColumns: reader.getTableColumns(THINGS_REQUIRED_TABLE),
|
|
9479
|
+
taskTags: reader.hasTable("TMTaskTag") ? mapPresent(reader.readRows("TMTaskTag"), normalizeThingsTaskTagRecord) : [],
|
|
9480
|
+
tasks: mapPresent(reader.readRows(THINGS_REQUIRED_TABLE), normalizeThingsTaskRecord)
|
|
9481
|
+
};
|
|
9482
|
+
};
|
|
9483
|
+
//#endregion
|
|
8635
9484
|
//#region ../../packages/sync-ids/src/meet-done-bear.ts
|
|
8636
9485
|
/**
|
|
8637
9486
|
* Ids for the "Meet Done Bear" sample project every new workspace is seeded
|
|
@@ -8685,8 +9534,6 @@ const listMeetDoneBearArchiveTargets = (options) => {
|
|
|
8685
9534
|
//#region src/import/things-sqlite.ts
|
|
8686
9535
|
const THINGS_CONTAINER_PATH = join(homedir(), "Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac");
|
|
8687
9536
|
const THINGS_BUNDLE_NAME = "Things Database.thingsdatabase";
|
|
8688
|
-
const THINGS_SQLITE_FILE = "main.sqlite";
|
|
8689
|
-
const LEGACY_THINGS_SQLITE_FILE = "Things.sqlite3";
|
|
8690
9537
|
const URL_SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/iu;
|
|
8691
9538
|
const isUrlLike = (value) => URL_SCHEME_PATTERN.test(value);
|
|
8692
9539
|
const resolveInputPath = (databaseUrl) => {
|
|
@@ -8699,7 +9546,7 @@ const resolveInputPath = (databaseUrl) => {
|
|
|
8699
9546
|
const expandThingsDatabasePath = (inputPath) => {
|
|
8700
9547
|
const resolvedPath = resolve(inputPath);
|
|
8701
9548
|
const lastSegment = basename(resolvedPath);
|
|
8702
|
-
if (lastSegment ===
|
|
9549
|
+
if (lastSegment === "main.sqlite" || lastSegment === "Things.sqlite3") return resolvedPath;
|
|
8703
9550
|
if (lastSegment === THINGS_BUNDLE_NAME) return join(resolvedPath, THINGS_SQLITE_FILE);
|
|
8704
9551
|
if (lastSegment.startsWith("ThingsData-")) return join(resolvedPath, THINGS_BUNDLE_NAME, THINGS_SQLITE_FILE);
|
|
8705
9552
|
return resolvedPath;
|
|
@@ -8744,130 +9591,6 @@ const getTableColumns = (database, tableName) => {
|
|
|
8744
9591
|
if (!hasTable(database, tableName)) return [];
|
|
8745
9592
|
return database.prepare(`PRAGMA table_info(${tableName})`).all().map((row) => typeof row.name === "string" ? row.name : null).filter((name) => name !== null);
|
|
8746
9593
|
};
|
|
8747
|
-
const toStringValue = (value) => {
|
|
8748
|
-
if (typeof value === "string") {
|
|
8749
|
-
const trimmed = value.trim();
|
|
8750
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
8751
|
-
}
|
|
8752
|
-
if (Buffer.isBuffer(value)) {
|
|
8753
|
-
const text = value.toString("utf-8").trim();
|
|
8754
|
-
return text.length > 0 ? text : null;
|
|
8755
|
-
}
|
|
8756
|
-
return null;
|
|
8757
|
-
};
|
|
8758
|
-
const toNumberValue = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
8759
|
-
const toBooleanValue = (value) => {
|
|
8760
|
-
const numeric = toNumberValue(value);
|
|
8761
|
-
return numeric === null ? null : numeric !== 0;
|
|
8762
|
-
};
|
|
8763
|
-
const normalizeTaskRecord = (row) => {
|
|
8764
|
-
const uuid = toStringValue(row.uuid);
|
|
8765
|
-
if (!uuid) return null;
|
|
8766
|
-
return {
|
|
8767
|
-
afterCompletionReferenceDate: toNumberValue(row.rt1_afterCompletionReferenceDate ?? row.afterCompletionReferenceDate),
|
|
8768
|
-
areaId: toStringValue(row.area),
|
|
8769
|
-
contactId: toStringValue(row.contact ?? row.delegate),
|
|
8770
|
-
creationDate: toNumberValue(row.creationDate),
|
|
8771
|
-
deadline: toNumberValue(row.deadline ?? row.dueDate),
|
|
8772
|
-
deadlineOffset: toNumberValue(row.t2_deadlineOffset ?? row.dueDateOffset),
|
|
8773
|
-
deadlineSuppressionDate: toNumberValue(row.deadlineSuppressionDate ?? row.dueDateSuppressionDate),
|
|
8774
|
-
headingId: toStringValue(row.heading ?? row.actionGroup),
|
|
8775
|
-
instanceCreationCount: toNumberValue(row.rt1_instanceCreationCount ?? row.instanceCreationCount),
|
|
8776
|
-
instanceCreationPaused: toBooleanValue(row.rt1_instanceCreationPaused ?? row.instanceCreationPaused) ?? false,
|
|
8777
|
-
instanceCreationStartDate: toNumberValue(row.rt1_instanceCreationStartDate ?? row.instanceCreationStartDate),
|
|
8778
|
-
lastReminderInteractionDate: toNumberValue(row.lastReminderInteractionDate ?? row.lastAlarmInteractionDate),
|
|
8779
|
-
nextInstanceStartDate: toNumberValue(row.rt1_nextInstanceStartDate ?? row.nextInstanceStartDate),
|
|
8780
|
-
notes: toStringValue(row.notes),
|
|
8781
|
-
projectId: toStringValue(row.project),
|
|
8782
|
-
raw: row,
|
|
8783
|
-
recurrenceRuleRaw: toStringValue(row.rt1_recurrenceRule ?? row.recurrenceRule),
|
|
8784
|
-
reminderTime: toNumberValue(row.reminderTime),
|
|
8785
|
-
repeatTemplateSourceId: toStringValue(row.rt1_repeatingTemplate ?? row.repeatingTemplate),
|
|
8786
|
-
repeaterMigrationDate: toNumberValue(row.repeaterMigrationDate),
|
|
8787
|
-
repeaterRaw: toStringValue(row.repeater),
|
|
8788
|
-
sortIndex: toNumberValue(row.index),
|
|
8789
|
-
start: toNumberValue(row.start),
|
|
8790
|
-
startBucket: toNumberValue(row.startBucket),
|
|
8791
|
-
startDate: toNumberValue(row.startDate),
|
|
8792
|
-
status: toNumberValue(row.status),
|
|
8793
|
-
stopDate: toNumberValue(row.stopDate),
|
|
8794
|
-
title: toStringValue(row.title),
|
|
8795
|
-
todayIndex: toNumberValue(row.todayIndex),
|
|
8796
|
-
todayIndexReferenceDate: toNumberValue(row.todayIndexReferenceDate),
|
|
8797
|
-
trashed: toBooleanValue(row.trashed) ?? false,
|
|
8798
|
-
type: toNumberValue(row.type),
|
|
8799
|
-
userModificationDate: toNumberValue(row.userModificationDate),
|
|
8800
|
-
uuid
|
|
8801
|
-
};
|
|
8802
|
-
};
|
|
8803
|
-
const normalizeAreaRecord = (row) => {
|
|
8804
|
-
const uuid = toStringValue(row.uuid);
|
|
8805
|
-
if (!uuid) return null;
|
|
8806
|
-
return {
|
|
8807
|
-
raw: row,
|
|
8808
|
-
sortIndex: toNumberValue(row.index),
|
|
8809
|
-
title: toStringValue(row.title),
|
|
8810
|
-
uuid,
|
|
8811
|
-
visible: toBooleanValue(row.visible)
|
|
8812
|
-
};
|
|
8813
|
-
};
|
|
8814
|
-
const normalizeTagRecord = (row) => {
|
|
8815
|
-
const uuid = toStringValue(row.uuid);
|
|
8816
|
-
if (!uuid) return null;
|
|
8817
|
-
return {
|
|
8818
|
-
parentId: toStringValue(row.parent),
|
|
8819
|
-
raw: row,
|
|
8820
|
-
shortcut: toStringValue(row.shortcut),
|
|
8821
|
-
sortIndex: toNumberValue(row.index),
|
|
8822
|
-
title: toStringValue(row.title),
|
|
8823
|
-
usedDate: toNumberValue(row.usedDate),
|
|
8824
|
-
uuid
|
|
8825
|
-
};
|
|
8826
|
-
};
|
|
8827
|
-
const normalizeChecklistItemRecord = (row) => {
|
|
8828
|
-
const uuid = toStringValue(row.uuid);
|
|
8829
|
-
if (!uuid) return null;
|
|
8830
|
-
return {
|
|
8831
|
-
creationDate: toNumberValue(row.creationDate),
|
|
8832
|
-
raw: row,
|
|
8833
|
-
sortIndex: toNumberValue(row.index),
|
|
8834
|
-
status: toNumberValue(row.status),
|
|
8835
|
-
stopDate: toNumberValue(row.stopDate),
|
|
8836
|
-
taskId: toStringValue(row.task),
|
|
8837
|
-
title: toStringValue(row.title),
|
|
8838
|
-
userModificationDate: toNumberValue(row.userModificationDate),
|
|
8839
|
-
uuid
|
|
8840
|
-
};
|
|
8841
|
-
};
|
|
8842
|
-
const normalizeTaskTagRecord = (row) => {
|
|
8843
|
-
const taskId = toStringValue(row.tasks);
|
|
8844
|
-
const tagId = toStringValue(row.tags);
|
|
8845
|
-
return taskId && tagId ? {
|
|
8846
|
-
tagId,
|
|
8847
|
-
taskId
|
|
8848
|
-
} : null;
|
|
8849
|
-
};
|
|
8850
|
-
const normalizeAreaTagRecord = (row) => {
|
|
8851
|
-
const areaId = toStringValue(row.areas);
|
|
8852
|
-
const tagId = toStringValue(row.tags);
|
|
8853
|
-
return areaId && tagId ? {
|
|
8854
|
-
areaId,
|
|
8855
|
-
tagId
|
|
8856
|
-
} : null;
|
|
8857
|
-
};
|
|
8858
|
-
const normalizeContactRecord = (row) => {
|
|
8859
|
-
const uuid = toStringValue(row.uuid);
|
|
8860
|
-
if (!uuid) return null;
|
|
8861
|
-
return {
|
|
8862
|
-
appleAddressBookId: toStringValue(row.appleAddressBookId),
|
|
8863
|
-
displayName: toStringValue(row.displayName),
|
|
8864
|
-
emails: toStringValue(row.emails),
|
|
8865
|
-
firstName: toStringValue(row.firstName),
|
|
8866
|
-
lastName: toStringValue(row.lastName),
|
|
8867
|
-
raw: row,
|
|
8868
|
-
uuid
|
|
8869
|
-
};
|
|
8870
|
-
};
|
|
8871
9594
|
const resolveThingsDatabasePath = (databaseUrl) => expandThingsDatabasePath(resolveInputPath(databaseUrl));
|
|
8872
9595
|
const resolveDefaultThingsDatabasePath = () => {
|
|
8873
9596
|
const resolved = findLatestThingsDatabasePath();
|
|
@@ -8882,17 +9605,11 @@ const loadThingsDatabase = (databaseUrl) => {
|
|
|
8882
9605
|
readonly: true
|
|
8883
9606
|
});
|
|
8884
9607
|
try {
|
|
8885
|
-
return {
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
databasePath,
|
|
8891
|
-
tags: hasTable(database, "TMTag") ? readRows(database, "TMTag").map(normalizeTagRecord).filter((record) => record !== null) : [],
|
|
8892
|
-
taskColumns: getTableColumns(database, "TMTask"),
|
|
8893
|
-
taskTags: hasTable(database, "TMTaskTag") ? readRows(database, "TMTaskTag").map(normalizeTaskTagRecord).filter((record) => record !== null) : [],
|
|
8894
|
-
tasks: readRows(database, "TMTask").map(normalizeTaskRecord).filter((record) => record !== null)
|
|
8895
|
-
};
|
|
9608
|
+
return buildThingsDatabaseSnapshot({
|
|
9609
|
+
getTableColumns: (tableName) => getTableColumns(database, tableName),
|
|
9610
|
+
hasTable: (tableName) => hasTable(database, tableName),
|
|
9611
|
+
readRows: (tableName) => readRows(database, tableName)
|
|
9612
|
+
}, databasePath);
|
|
8896
9613
|
} finally {
|
|
8897
9614
|
database.close();
|
|
8898
9615
|
}
|
|
@@ -8900,7 +9617,15 @@ const loadThingsDatabase = (databaseUrl) => {
|
|
|
8900
9617
|
//#endregion
|
|
8901
9618
|
//#region src/import/things-runner.ts
|
|
8902
9619
|
const WORKSPACE_SCOPED_MODELS = SYNC_CONTENT_MODELS;
|
|
8903
|
-
const INSERT_ONLY_MODELS = new Set([
|
|
9620
|
+
const INSERT_ONLY_MODELS = /* @__PURE__ */ new Set([
|
|
9621
|
+
"TaskLabel",
|
|
9622
|
+
"TeamProject",
|
|
9623
|
+
"TeamMembership",
|
|
9624
|
+
"ProjectMembership"
|
|
9625
|
+
]);
|
|
9626
|
+
/** Membership joins are not in SYNC_CONTENT_MODELS (identity surface), but
|
|
9627
|
+
* Things import writes the importer's own rows and needs them for skip-on-rerun. */
|
|
9628
|
+
const THINGS_MEMBERSHIP_MODELS = ["TeamMembership", "ProjectMembership"];
|
|
8904
9629
|
const chunk = (items, size) => {
|
|
8905
9630
|
const result = [];
|
|
8906
9631
|
for (let index = 0; index < items.length; index += size) result.push(items.slice(index, index + size));
|
|
@@ -8944,9 +9669,9 @@ const loadWorkspaceScopedRowsByModel = async (context, options) => {
|
|
|
8944
9669
|
};
|
|
8945
9670
|
const loadExistingImportState = async (options) => {
|
|
8946
9671
|
let modelNames;
|
|
8947
|
-
if (options.dryRun) modelNames = ["Label"];
|
|
8948
|
-
else if (options.skipTeams) modelNames = WORKSPACE_SCOPED_MODELS.filter((m) => m !== "Team" && m !== "TeamProject");
|
|
8949
|
-
else modelNames = WORKSPACE_SCOPED_MODELS;
|
|
9672
|
+
if (options.dryRun) modelNames = options.skipTeams ? ["Label"] : ["Label", "Team"];
|
|
9673
|
+
else if (options.skipTeams) modelNames = [...WORKSPACE_SCOPED_MODELS.filter((m) => m !== "Team" && m !== "TeamProject"), "ProjectMembership"];
|
|
9674
|
+
else modelNames = [...WORKSPACE_SCOPED_MODELS, ...THINGS_MEMBERSHIP_MODELS];
|
|
8950
9675
|
const rowsByModel = await loadWorkspaceScopedRowsByModel(options.context, {
|
|
8951
9676
|
apiBaseUrl: options.commandContext.apiBaseUrl,
|
|
8952
9677
|
modelNames,
|
|
@@ -8955,6 +9680,7 @@ const loadExistingImportState = async (options) => {
|
|
|
8955
9680
|
});
|
|
8956
9681
|
return {
|
|
8957
9682
|
existingLabelIdsByTitle: buildExistingLabelIdsByTitle(rowsByModel),
|
|
9683
|
+
existingTeamIdsByName: buildExistingTeamIdsByName(rowsByModel),
|
|
8958
9684
|
rowsByModel
|
|
8959
9685
|
};
|
|
8960
9686
|
};
|
|
@@ -8972,9 +9698,23 @@ const buildExistingLabelIdsByTitle = (rowsByModel) => {
|
|
|
8972
9698
|
}
|
|
8973
9699
|
return labelsByTitle;
|
|
8974
9700
|
};
|
|
9701
|
+
const buildExistingTeamIdsByName = (rowsByModel) => {
|
|
9702
|
+
const rows = rowsByModel.get("Team") ?? [];
|
|
9703
|
+
const teamsByName = /* @__PURE__ */ new Map();
|
|
9704
|
+
for (const row of rows) {
|
|
9705
|
+
if (typeof row !== "object" || row === null) continue;
|
|
9706
|
+
const record = row;
|
|
9707
|
+
if (record.__class !== "Team") continue;
|
|
9708
|
+
const id = typeof record.id === "string" ? record.id : null;
|
|
9709
|
+
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
9710
|
+
if (!id || name.length === 0) continue;
|
|
9711
|
+
teamsByName.set(normalizeThingsTeamLookupKey(name), id);
|
|
9712
|
+
}
|
|
9713
|
+
return teamsByName;
|
|
9714
|
+
};
|
|
8975
9715
|
const buildExistingIds = (rowsByModel) => {
|
|
8976
|
-
const
|
|
8977
|
-
return new Map(
|
|
9716
|
+
const modelNames = [...WORKSPACE_SCOPED_MODELS, ...THINGS_MEMBERSHIP_MODELS];
|
|
9717
|
+
return new Map(modelNames.map((modelName) => [modelName, collectRowIds(rowsByModel.get(modelName) ?? [], modelName)]));
|
|
8978
9718
|
};
|
|
8979
9719
|
const buildTransactions = (phaseItems, options) => {
|
|
8980
9720
|
const transactions = [];
|
|
@@ -8992,6 +9732,7 @@ const buildTransactions = (phaseItems, options) => {
|
|
|
8992
9732
|
if (action === "INSERT") {
|
|
8993
9733
|
inserted += 1;
|
|
8994
9734
|
existingIds.add(item.modelId);
|
|
9735
|
+
options.existingIds.set(item.modelName, existingIds);
|
|
8995
9736
|
} else updated += 1;
|
|
8996
9737
|
transactions.push({
|
|
8997
9738
|
action,
|
|
@@ -9009,6 +9750,24 @@ const buildTransactions = (phaseItems, options) => {
|
|
|
9009
9750
|
updated
|
|
9010
9751
|
};
|
|
9011
9752
|
};
|
|
9753
|
+
const PLAN_LIMIT_PATTERN = /plan includes up to|upgrade to add more/iu;
|
|
9754
|
+
const LIMITED_MODELS = ["Task", "Team"];
|
|
9755
|
+
/**
|
|
9756
|
+
* Turns a batch the server partly refused for the plan limit into one clear
|
|
9757
|
+
* stop: how many tasks and teams landed before it, and what to do next. Any
|
|
9758
|
+
* other refusal is passed through unchanged.
|
|
9759
|
+
*/
|
|
9760
|
+
const explainPlanLimitStop = (error, context) => {
|
|
9761
|
+
if (!(error instanceof SyncMutationRejectedError) || !error.results.some((result) => !result.success && PLAN_LIMIT_PATTERN.test(result.error ?? ""))) return error;
|
|
9762
|
+
const landed = new Map(context.landed);
|
|
9763
|
+
const accepted = new Set(error.results.filter((result) => result.success).map((r) => r.clientTxId));
|
|
9764
|
+
for (const tx of context.batch) if (tx.action === "INSERT" && accepted.has(tx.clientTxId)) landed.set(tx.modelName, (landed.get(tx.modelName) ?? 0) + 1);
|
|
9765
|
+
const parts = LIMITED_MODELS.filter((model) => (context.planned.get(model) ?? 0) > 0).map((model) => {
|
|
9766
|
+
const noun = model === "Task" ? "tasks" : "teams";
|
|
9767
|
+
return `${landed.get(model) ?? 0} of ${context.planned.get(model) ?? 0} ${noun}`;
|
|
9768
|
+
});
|
|
9769
|
+
return new CliError(`Import stopped: the workspace reached its plan limit. Imported ${parts.join(" and ")} before it stopped.`, EXIT_CODES.ERROR, "Upgrade the workspace, or run again with --open-only (and --skip-teams if the team limit was hit). A re-run skips what already landed.");
|
|
9770
|
+
};
|
|
9012
9771
|
const executeImportPlan = async (options, plan, rowsByModel) => {
|
|
9013
9772
|
const execution = {
|
|
9014
9773
|
batches: 0,
|
|
@@ -9017,25 +9776,37 @@ const executeImportPlan = async (options, plan, rowsByModel) => {
|
|
|
9017
9776
|
updated: 0
|
|
9018
9777
|
};
|
|
9019
9778
|
const existingIds = buildExistingIds(rowsByModel);
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9779
|
+
const prepared = plan.phases.map((phase) => buildTransactions(phase.items, {
|
|
9780
|
+
clientId: options.commandContext.localContext.clientId,
|
|
9781
|
+
existingIds
|
|
9782
|
+
}));
|
|
9783
|
+
const planned = /* @__PURE__ */ new Map();
|
|
9784
|
+
for (const tx of prepared.flatMap((phase) => phase.transactions)) if (tx.action === "INSERT") planned.set(tx.modelName, (planned.get(tx.modelName) ?? 0) + 1);
|
|
9785
|
+
const landed = /* @__PURE__ */ new Map();
|
|
9786
|
+
for (const phasePrepared of prepared) {
|
|
9787
|
+
execution.inserted += phasePrepared.inserted;
|
|
9788
|
+
execution.updated += phasePrepared.updated;
|
|
9789
|
+
execution.skipped += phasePrepared.skipped;
|
|
9790
|
+
const transactionBatches = chunk(phasePrepared.transactions, options.batchSize);
|
|
9029
9791
|
for (const transactionBatch of transactionBatches) {
|
|
9030
9792
|
if (transactionBatch.length === 0) continue;
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
9793
|
+
try {
|
|
9794
|
+
await mutateModel(options.context, {
|
|
9795
|
+
baseUrl: options.commandContext.apiBaseUrl,
|
|
9796
|
+
request: {
|
|
9797
|
+
batchId: randomUUID(),
|
|
9798
|
+
transactions: transactionBatch
|
|
9799
|
+
},
|
|
9800
|
+
token: options.commandContext.token
|
|
9801
|
+
});
|
|
9802
|
+
} catch (error) {
|
|
9803
|
+
throw explainPlanLimitStop(error, {
|
|
9804
|
+
batch: transactionBatch,
|
|
9805
|
+
landed,
|
|
9806
|
+
planned
|
|
9807
|
+
});
|
|
9808
|
+
}
|
|
9809
|
+
for (const tx of transactionBatch) if (tx.action === "INSERT") landed.set(tx.modelName, (landed.get(tx.modelName) ?? 0) + 1);
|
|
9039
9810
|
execution.batches += 1;
|
|
9040
9811
|
}
|
|
9041
9812
|
}
|
|
@@ -9077,7 +9848,9 @@ const runThingsImport = async (options) => {
|
|
|
9077
9848
|
const plan = buildThingsImportPlan({
|
|
9078
9849
|
creatorId: options.creatorId,
|
|
9079
9850
|
existingLabelsByTitle: existingState.existingLabelIdsByTitle,
|
|
9851
|
+
existingTeamsByName: existingState.existingTeamIdsByName,
|
|
9080
9852
|
metadataMode: options.metadataMode,
|
|
9853
|
+
openOnly: options.openOnly,
|
|
9081
9854
|
skipTeams: options.skipTeams,
|
|
9082
9855
|
snapshot,
|
|
9083
9856
|
timeZone: resolveLocalTimeZone(),
|
|
@@ -9093,6 +9866,7 @@ const runThingsImport = async (options) => {
|
|
|
9093
9866
|
skipped: 0,
|
|
9094
9867
|
updated: 0
|
|
9095
9868
|
},
|
|
9869
|
+
issues: plan.issues,
|
|
9096
9870
|
taskColumns: plan.taskColumns,
|
|
9097
9871
|
workspace: options.workspace
|
|
9098
9872
|
};
|
|
@@ -9103,6 +9877,7 @@ const runThingsImport = async (options) => {
|
|
|
9103
9877
|
databasePath: plan.databasePath,
|
|
9104
9878
|
dryRun: false,
|
|
9105
9879
|
execution,
|
|
9880
|
+
issues: plan.issues,
|
|
9106
9881
|
taskColumns: plan.taskColumns,
|
|
9107
9882
|
workspace: options.workspace
|
|
9108
9883
|
};
|
|
@@ -9132,6 +9907,7 @@ const runThingsImportCommand = async (context, options, workspaceOverride) => {
|
|
|
9132
9907
|
databaseUrl: options.databaseUrl,
|
|
9133
9908
|
dryRun: context.dryRun,
|
|
9134
9909
|
metadataMode,
|
|
9910
|
+
openOnly: options.openOnly,
|
|
9135
9911
|
skipTeams: options.skipTeams,
|
|
9136
9912
|
workspace
|
|
9137
9913
|
});
|
|
@@ -9143,9 +9919,10 @@ const runThingsImportCommand = async (context, options, workspaceOverride) => {
|
|
|
9143
9919
|
const summary = summarizeThingsImportResult(result);
|
|
9144
9920
|
writeSuccess(context, summary.successMessage);
|
|
9145
9921
|
for (const line of summary.infoLines) writeInfo(context, line);
|
|
9922
|
+
for (const line of summary.warningLines) writeWarning(context, line);
|
|
9146
9923
|
};
|
|
9147
9924
|
const registerImportCommands = (program, workspaceOverride) => {
|
|
9148
|
-
program.command("import").description("Import external data into Done Bear").command("things").description("Import a Things 3 SQLite export").option("--database-url <database-url>", "Path, file:// URL, sqlite:// URL, .thingsdatabase bundle, or ThingsData-* directory").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--skip-teams", "Skip importing Things Areas as Teams and TeamProject links", false).option("--metadata-mode <mode>", "How to preserve Things-only metadata: note|none", "none").option("--batch-size <count>", "Maximum sync mutations per batch", (value) => parsePositiveInteger(value, "Batch size"), 100).addHelpText("after", `
|
|
9925
|
+
program.command("import").description("Import external data into Done Bear").command("things").description("Import a Things 3 SQLite export").option("--database-url <database-url>", "Path, file:// URL, sqlite:// URL, .thingsdatabase bundle, or ThingsData-* directory").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--skip-teams", "Skip importing Things Areas as Teams and TeamProject links", false).option("--open-only", "Import only open to-dos; leave completed, canceled and trashed ones behind", false).option("--metadata-mode <mode>", "How to preserve Things-only metadata: note|none", "none").option("--batch-size <count>", "Maximum sync mutations per batch", (value) => parsePositiveInteger(value, "Batch size"), 100).addHelpText("after", `
|
|
9149
9926
|
Examples:
|
|
9150
9927
|
donebear import things --dry-run --json
|
|
9151
9928
|
donebear import things --database-url ~/Backups/Things\\ Database.thingsdatabase --json
|
|
@@ -9153,6 +9930,7 @@ Examples:
|
|
|
9153
9930
|
|
|
9154
9931
|
Notes:
|
|
9155
9932
|
--database-url accepts a copied Things 3 export bundle, a direct main.sqlite path, or a ThingsData-* directory.
|
|
9933
|
+
--open-only leaves the Logbook behind. Completed to-dos count toward the Free plan's task limit.
|
|
9156
9934
|
--metadata-mode note preserves Things-only fields in task/project/team descriptions using a structured appendix.
|
|
9157
9935
|
`).action(async (options, command) => {
|
|
9158
9936
|
const context = contextFromCommand(command);
|
|
@@ -9225,11 +10003,15 @@ const runLabelList = async (context, options) => {
|
|
|
9225
10003
|
}
|
|
9226
10004
|
writeInfo(context, `${scopeLabel(scope)} (${sorted.length} label(s))`);
|
|
9227
10005
|
for (const labelRecord of sorted) writeInfo(context, `${labelRecord.id.slice(0, 8)} ${labelRecord.title}`);
|
|
9228
|
-
if (context.copy)
|
|
10006
|
+
if (context.copy) {
|
|
10007
|
+
const text = sorted.map((l) => `${l.id.slice(0, 8)} ${l.title}`).join("\n");
|
|
10008
|
+
copyToClipboard(text);
|
|
10009
|
+
}
|
|
9229
10010
|
};
|
|
9230
10011
|
const runLabelShow = async (context, labelRef, options) => {
|
|
10012
|
+
const commandContext = await resolveCommandContext(context);
|
|
9231
10013
|
const label = await resolveLabel(context, {
|
|
9232
|
-
commandContext
|
|
10014
|
+
commandContext,
|
|
9233
10015
|
labelRef,
|
|
9234
10016
|
workspaceRef: normalizeWorkspaceRef(options.workspace)
|
|
9235
10017
|
});
|
|
@@ -9305,6 +10087,30 @@ const runLabelCreate = async (context, title, options) => {
|
|
|
9305
10087
|
}
|
|
9306
10088
|
writeSuccess(context, `Created label: ${title.trim()}`);
|
|
9307
10089
|
};
|
|
10090
|
+
/**
|
|
10091
|
+
* A TaskLabel row is keyed on task, label and workspace together, so a DELETE
|
|
10092
|
+
* needs `workspaceId` as much as an INSERT does: without it the server cannot
|
|
10093
|
+
* address the row and rejects the mutation.
|
|
10094
|
+
*/
|
|
10095
|
+
const buildTaskLabelTransaction = (options) => {
|
|
10096
|
+
const { action, clientId, labelId, taskId, workspaceId } = options;
|
|
10097
|
+
const key = {
|
|
10098
|
+
labelId,
|
|
10099
|
+
taskId,
|
|
10100
|
+
workspaceId
|
|
10101
|
+
};
|
|
10102
|
+
return {
|
|
10103
|
+
action,
|
|
10104
|
+
clientId,
|
|
10105
|
+
clientTxId: randomUUID(),
|
|
10106
|
+
modelId: `${taskId}:${labelId}`,
|
|
10107
|
+
modelName: "TaskLabel",
|
|
10108
|
+
payload: action === "INSERT" ? {
|
|
10109
|
+
...key,
|
|
10110
|
+
createdAt: Date.now()
|
|
10111
|
+
} : key
|
|
10112
|
+
};
|
|
10113
|
+
};
|
|
9308
10114
|
const runLabelAdd = async (context, taskRef, labelRef, options) => {
|
|
9309
10115
|
const commandContext = await resolveCommandContext(context);
|
|
9310
10116
|
const workspaceRef = normalizeWorkspaceRef(options.workspace);
|
|
@@ -9322,19 +10128,13 @@ const runLabelAdd = async (context, taskRef, labelRef, options) => {
|
|
|
9322
10128
|
baseUrl: commandContext.apiBaseUrl,
|
|
9323
10129
|
request: {
|
|
9324
10130
|
batchId: randomUUID(),
|
|
9325
|
-
transactions: [{
|
|
10131
|
+
transactions: [buildTaskLabelTransaction({
|
|
9326
10132
|
action: "INSERT",
|
|
9327
10133
|
clientId: commandContext.localContext.clientId,
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
createdAt: Date.now(),
|
|
9333
|
-
labelId: label.id,
|
|
9334
|
-
taskId: task.id,
|
|
9335
|
-
workspaceId: task.workspaceId
|
|
9336
|
-
}
|
|
9337
|
-
}]
|
|
10134
|
+
labelId: label.id,
|
|
10135
|
+
taskId: task.id,
|
|
10136
|
+
workspaceId: task.workspaceId
|
|
10137
|
+
})]
|
|
9338
10138
|
},
|
|
9339
10139
|
token: commandContext.token
|
|
9340
10140
|
});
|
|
@@ -9366,17 +10166,13 @@ const runLabelRemove = async (context, taskRef, labelRef, options) => {
|
|
|
9366
10166
|
baseUrl: commandContext.apiBaseUrl,
|
|
9367
10167
|
request: {
|
|
9368
10168
|
batchId: randomUUID(),
|
|
9369
|
-
transactions: [{
|
|
10169
|
+
transactions: [buildTaskLabelTransaction({
|
|
9370
10170
|
action: "DELETE",
|
|
9371
10171
|
clientId: commandContext.localContext.clientId,
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
labelId: label.id,
|
|
9377
|
-
taskId: task.id
|
|
9378
|
-
}
|
|
9379
|
-
}]
|
|
10172
|
+
labelId: label.id,
|
|
10173
|
+
taskId: task.id,
|
|
10174
|
+
workspaceId: task.workspaceId
|
|
10175
|
+
})]
|
|
9380
10176
|
},
|
|
9381
10177
|
token: commandContext.token
|
|
9382
10178
|
});
|
|
@@ -9498,7 +10294,7 @@ const runProjectList = async (context, options) => {
|
|
|
9498
10294
|
token: commandContext.token,
|
|
9499
10295
|
workspaceRef: normalizeWorkspaceRef(options.workspace)
|
|
9500
10296
|
});
|
|
9501
|
-
const
|
|
10297
|
+
const allProjects = scope.mode === "all" ? await loadProjectsByWorkspaceIds(context, {
|
|
9502
10298
|
baseUrl: commandContext.apiBaseUrl,
|
|
9503
10299
|
token: commandContext.token,
|
|
9504
10300
|
workspaceIds: scope.workspaces.map((w) => w.id)
|
|
@@ -9506,7 +10302,8 @@ const runProjectList = async (context, options) => {
|
|
|
9506
10302
|
baseUrl: commandContext.apiBaseUrl,
|
|
9507
10303
|
token: commandContext.token,
|
|
9508
10304
|
workspaceId: scope.workspace.id
|
|
9509
|
-
})
|
|
10305
|
+
});
|
|
10306
|
+
const filtered = filterProjects(sortProjectsByUpdatedDesc(allProjects), requestedStatus);
|
|
9510
10307
|
if (context.json) {
|
|
9511
10308
|
writeOutput(context, {
|
|
9512
10309
|
...scopeJsonFields(scope),
|
|
@@ -9545,11 +10342,15 @@ const runProjectList = async (context, options) => {
|
|
|
9545
10342
|
const suffix = target ? ` due ${target}` : "";
|
|
9546
10343
|
writeInfo(context, `${project.id.slice(0, 8)} [${project.key}] ${project.name} (${status})${suffix}`);
|
|
9547
10344
|
}
|
|
9548
|
-
if (context.copy)
|
|
10345
|
+
if (context.copy) {
|
|
10346
|
+
const text = filtered.map((p) => `${p.id.slice(0, 8)} [${p.key}] ${p.name} (${getProjectStatus(p)})`).join("\n");
|
|
10347
|
+
copyToClipboard(text);
|
|
10348
|
+
}
|
|
9549
10349
|
};
|
|
9550
10350
|
const runProjectShow = async (context, projectRef, options) => {
|
|
10351
|
+
const commandContext = await resolveCommandContext(context);
|
|
9551
10352
|
const { project, workspace } = await resolveProjectReference(context, {
|
|
9552
|
-
commandContext
|
|
10353
|
+
commandContext,
|
|
9553
10354
|
projectRef,
|
|
9554
10355
|
workspaceRef: normalizeWorkspaceRef(options.workspace)
|
|
9555
10356
|
});
|
|
@@ -9581,7 +10382,7 @@ const resolveProjectName = async (context, rawName) => {
|
|
|
9581
10382
|
const trimmed = rawName.trim();
|
|
9582
10383
|
if (trimmed.length > 0) return trimmed;
|
|
9583
10384
|
}
|
|
9584
|
-
const { promptForValue } = await import("./prompt-
|
|
10385
|
+
const { promptForValue } = await import("./prompt-ujp1V6r9.mjs");
|
|
9585
10386
|
const prompted = await promptForValue(context, "Project name");
|
|
9586
10387
|
if (prompted) return prompted;
|
|
9587
10388
|
throw new CliError("Project name is required.", EXIT_CODES.VALIDATION, "Provide a name: donebear project add \"My project\"");
|
|
@@ -9826,10 +10627,7 @@ const runProjectMembershipChange = async (context, projectRef, options) => {
|
|
|
9826
10627
|
case "already_member":
|
|
9827
10628
|
writeInfo(context, `Already in project: ${project.name}`);
|
|
9828
10629
|
break;
|
|
9829
|
-
case "not_a_member":
|
|
9830
|
-
writeInfo(context, `Not in project: ${project.name}`);
|
|
9831
|
-
break;
|
|
9832
|
-
default: break;
|
|
10630
|
+
case "not_a_member": writeInfo(context, `Not in project: ${project.name}`);
|
|
9833
10631
|
}
|
|
9834
10632
|
};
|
|
9835
10633
|
const registerProjectCommands = (program, workspaceOverride) => {
|
|
@@ -9964,7 +10762,7 @@ const runSearch = async (context, query, options, workspaceOverride) => {
|
|
|
9964
10762
|
token: commandContext.token,
|
|
9965
10763
|
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
9966
10764
|
});
|
|
9967
|
-
const
|
|
10765
|
+
const tasks = scope.mode === "all" ? await loadTasksByWorkspaceIds(context, {
|
|
9968
10766
|
baseUrl: commandContext.apiBaseUrl,
|
|
9969
10767
|
token: commandContext.token,
|
|
9970
10768
|
workspaceIds: scope.workspaces.map((w) => w.id)
|
|
@@ -9972,7 +10770,8 @@ const runSearch = async (context, query, options, workspaceOverride) => {
|
|
|
9972
10770
|
baseUrl: commandContext.apiBaseUrl,
|
|
9973
10771
|
token: commandContext.token,
|
|
9974
10772
|
workspaceId: scope.workspace.id
|
|
9975
|
-
})
|
|
10773
|
+
});
|
|
10774
|
+
const filtered = filterTasks(tasks, {
|
|
9976
10775
|
search: trimmedQuery,
|
|
9977
10776
|
state: requestedState,
|
|
9978
10777
|
view: null
|
|
@@ -10017,7 +10816,7 @@ Examples:
|
|
|
10017
10816
|
//#endregion
|
|
10018
10817
|
//#region src/commands/spec.ts
|
|
10019
10818
|
const OVERVIEW = {
|
|
10020
|
-
authentication: "Set DONEBEAR_TOKEN env var,
|
|
10819
|
+
authentication: "Set DONEBEAR_TOKEN env var, run: donebear auth login, or create an account with donebear auth signup --email <human-email>",
|
|
10021
10820
|
commands: {
|
|
10022
10821
|
"api-key": {
|
|
10023
10822
|
description: "Manage personal API keys",
|
|
@@ -10033,8 +10832,9 @@ const OVERVIEW = {
|
|
|
10033
10832
|
quick_example: "donebear auth status --json",
|
|
10034
10833
|
subcommands: [
|
|
10035
10834
|
"login",
|
|
10036
|
-
"
|
|
10037
|
-
"
|
|
10835
|
+
"logout",
|
|
10836
|
+
"signup",
|
|
10837
|
+
"status"
|
|
10038
10838
|
]
|
|
10039
10839
|
},
|
|
10040
10840
|
comment: {
|
|
@@ -10264,6 +11064,44 @@ const COMMAND_SPECS = {
|
|
|
10264
11064
|
},
|
|
10265
11065
|
usage: "donebear auth login [options]"
|
|
10266
11066
|
},
|
|
11067
|
+
"auth signup": {
|
|
11068
|
+
command: "auth signup",
|
|
11069
|
+
examples: ["donebear auth signup --email person@example.com", "donebear auth signup --email person@example.com --json --no-interactive"],
|
|
11070
|
+
json_output: {
|
|
11071
|
+
apiKey: "{ id, key, prefix, scopes }",
|
|
11072
|
+
email: "string",
|
|
11073
|
+
instructions: "{ agent, human }",
|
|
11074
|
+
mcp: "{ url, headers, cursor, claude }",
|
|
11075
|
+
ok: "boolean",
|
|
11076
|
+
sessionStored: "boolean",
|
|
11077
|
+
status: "complete",
|
|
11078
|
+
user: "{ id, email }",
|
|
11079
|
+
verifyUrl: "string",
|
|
11080
|
+
workspace: "{ id, name, urlKey }"
|
|
11081
|
+
},
|
|
11082
|
+
non_interactive: "Requires --email. Do not invent an address. --json and --no-interactive print the verify link on stderr, poll until the human verifies, then emit one complete JSON object on stdout. If the email is already registered, run donebear auth login instead.",
|
|
11083
|
+
notes: "Creates a real human workspace after email verification, then mints a db_ API key with read+write (not admin). WorkOS agent_auth and RFC 8628 device code are not offered.",
|
|
11084
|
+
options: {
|
|
11085
|
+
"--email": {
|
|
11086
|
+
description: "Human email to verify. Required. Do not invent an address.",
|
|
11087
|
+
type: "string"
|
|
11088
|
+
},
|
|
11089
|
+
"--name": {
|
|
11090
|
+
description: "Optional display name",
|
|
11091
|
+
type: "string"
|
|
11092
|
+
},
|
|
11093
|
+
"--no-interactive": {
|
|
11094
|
+
description: "Never prompt for the email code; poll the verify link instead",
|
|
11095
|
+
type: "boolean"
|
|
11096
|
+
},
|
|
11097
|
+
"--timeout": {
|
|
11098
|
+
default: 600,
|
|
11099
|
+
description: "Seconds to wait for email verification",
|
|
11100
|
+
type: "number"
|
|
11101
|
+
}
|
|
11102
|
+
},
|
|
11103
|
+
usage: "donebear auth signup [options]"
|
|
11104
|
+
},
|
|
10267
11105
|
"auth status": {
|
|
10268
11106
|
command: "auth status",
|
|
10269
11107
|
examples: ["donebear auth status --json"],
|
|
@@ -10536,6 +11374,10 @@ const COMMAND_SPECS = {
|
|
|
10536
11374
|
enum: ["note", "none"],
|
|
10537
11375
|
type: "string"
|
|
10538
11376
|
},
|
|
11377
|
+
"--open-only": {
|
|
11378
|
+
description: "Import only open to-dos; completed, canceled and trashed ones are left behind",
|
|
11379
|
+
type: "boolean"
|
|
11380
|
+
},
|
|
10539
11381
|
"--skip-teams": {
|
|
10540
11382
|
description: "Skip importing Things Areas as Teams and TeamProject links",
|
|
10541
11383
|
type: "boolean"
|
|
@@ -11120,6 +11962,44 @@ const COMMAND_SPECS = {
|
|
|
11120
11962
|
options: {},
|
|
11121
11963
|
usage: "donebear whoami"
|
|
11122
11964
|
},
|
|
11965
|
+
"workspace invite": {
|
|
11966
|
+
arguments: { workspace: {
|
|
11967
|
+
description: "Workspace id, urlKey, or name. Defaults to the current workspace.",
|
|
11968
|
+
required: false,
|
|
11969
|
+
type: "string"
|
|
11970
|
+
} },
|
|
11971
|
+
command: "workspace invite",
|
|
11972
|
+
examples: [
|
|
11973
|
+
"donebear workspace invite --email user@example.com --role member",
|
|
11974
|
+
"donebear workspace invite --share --copy",
|
|
11975
|
+
"donebear workspace invite --share --project <project-id>"
|
|
11976
|
+
],
|
|
11977
|
+
json_output: {
|
|
11978
|
+
invitation: "WorkspaceInvitationRecord",
|
|
11979
|
+
workspace: "WorkspaceRecord"
|
|
11980
|
+
},
|
|
11981
|
+
notes: "Email invites are single-use. --share mints a reusable /join?code= link. --project requires --share.",
|
|
11982
|
+
options: {
|
|
11983
|
+
"--email": {
|
|
11984
|
+
description: "Email address to invite (single-use)",
|
|
11985
|
+
type: "string"
|
|
11986
|
+
},
|
|
11987
|
+
"--project": {
|
|
11988
|
+
description: "Scope a share link so joiners also become members of this project",
|
|
11989
|
+
type: "string"
|
|
11990
|
+
},
|
|
11991
|
+
"--role": {
|
|
11992
|
+
default: "member",
|
|
11993
|
+
description: "Role for an email invite: member|admin",
|
|
11994
|
+
type: "string"
|
|
11995
|
+
},
|
|
11996
|
+
"--share": {
|
|
11997
|
+
description: "Create a reusable join link instead of an email invite",
|
|
11998
|
+
type: "boolean"
|
|
11999
|
+
}
|
|
12000
|
+
},
|
|
12001
|
+
usage: "donebear workspace invite [workspace] [options]"
|
|
12002
|
+
},
|
|
11123
12003
|
"workspace list": {
|
|
11124
12004
|
command: "workspace list",
|
|
11125
12005
|
examples: ["donebear workspace list --json"],
|
|
@@ -11162,10 +12042,11 @@ const TOP_LEVEL_SPECS = {
|
|
|
11162
12042
|
auth: {
|
|
11163
12043
|
command: "auth",
|
|
11164
12044
|
description: "Authentication",
|
|
11165
|
-
non_interactive: "Set DONEBEAR_TOKEN=<token> to skip interactive login.",
|
|
12045
|
+
non_interactive: "Set DONEBEAR_TOKEN=<token> to skip interactive login. New accounts: donebear auth signup --email <human-email> --json --no-interactive.",
|
|
11166
12046
|
subcommands: {
|
|
11167
12047
|
login: "OAuth login (Google or GitHub)",
|
|
11168
12048
|
logout: "Sign out and remove stored session",
|
|
12049
|
+
signup: "Create an account from a human email (agent signup)",
|
|
11169
12050
|
status: "Show auth status"
|
|
11170
12051
|
},
|
|
11171
12052
|
tip: "donebear spec auth login: get full spec for auth login"
|
|
@@ -11252,7 +12133,7 @@ const TOP_LEVEL_SPECS = {
|
|
|
11252
12133
|
create: "Create a new workspace",
|
|
11253
12134
|
current: "Show the active workspace",
|
|
11254
12135
|
invitations: "List pending invitations",
|
|
11255
|
-
invite: "Invite a
|
|
12136
|
+
invite: "Invite by email, or mint a reusable --share join link",
|
|
11256
12137
|
join: "Join a workspace by invite",
|
|
11257
12138
|
list: "List all workspaces",
|
|
11258
12139
|
members: "List workspace members",
|
|
@@ -11300,6 +12181,7 @@ Examples:
|
|
|
11300
12181
|
donebear spec project list Detailed spec for project list
|
|
11301
12182
|
donebear spec import things Detailed spec for the Things importer
|
|
11302
12183
|
donebear spec search Detailed spec for search
|
|
12184
|
+
donebear spec auth signup Agent signup from a human email
|
|
11303
12185
|
donebear spec auth login Auth login options and non-interactive notes
|
|
11304
12186
|
`).action(async (command, subcommand, _options, cmd) => {
|
|
11305
12187
|
const context = contextFromCommand(cmd);
|
|
@@ -11424,7 +12306,7 @@ const runToday = async (context, options, workspaceOverride) => {
|
|
|
11424
12306
|
token: commandContext.token,
|
|
11425
12307
|
workspaceRef: workspaceOverride ?? normalizeWorkspaceRef(options.workspace)
|
|
11426
12308
|
});
|
|
11427
|
-
const
|
|
12309
|
+
const tasks = scope.mode === "all" ? await loadTasksByWorkspaceIds(context, {
|
|
11428
12310
|
baseUrl: commandContext.apiBaseUrl,
|
|
11429
12311
|
token: commandContext.token,
|
|
11430
12312
|
workspaceIds: scope.workspaces.map((w) => w.id)
|
|
@@ -11432,7 +12314,8 @@ const runToday = async (context, options, workspaceOverride) => {
|
|
|
11432
12314
|
baseUrl: commandContext.apiBaseUrl,
|
|
11433
12315
|
token: commandContext.token,
|
|
11434
12316
|
workspaceId: scope.workspace.id
|
|
11435
|
-
})
|
|
12317
|
+
});
|
|
12318
|
+
const todayTasks = filterToday(tasks);
|
|
11436
12319
|
const limited = todayTasks.slice(0, options.limit);
|
|
11437
12320
|
if (context.json) {
|
|
11438
12321
|
writeOutput(context, {
|
|
@@ -11557,7 +12440,10 @@ const runWorkspaceList = async (context) => {
|
|
|
11557
12440
|
return;
|
|
11558
12441
|
}
|
|
11559
12442
|
writeInfo(context, "Workspaces:");
|
|
11560
|
-
for (const workspace of workspaces)
|
|
12443
|
+
for (const workspace of workspaces) {
|
|
12444
|
+
const marker = workspace.id === commandContext.localContext.workspaceId ? "*" : " ";
|
|
12445
|
+
writeInfo(context, `${marker} ${workspace.id} ${workspace.name}${workspace.urlKey ? ` (${workspace.urlKey})` : ""} role=${workspace.role}`);
|
|
12446
|
+
}
|
|
11561
12447
|
};
|
|
11562
12448
|
const runWorkspaceCurrent = async (context) => {
|
|
11563
12449
|
const commandContext = await resolveCommandContext(context);
|
|
@@ -11584,10 +12470,11 @@ const runWorkspaceCurrent = async (context) => {
|
|
|
11584
12470
|
};
|
|
11585
12471
|
const runWorkspaceUse = async (context, reference) => {
|
|
11586
12472
|
const commandContext = await resolveCommandContext(context);
|
|
11587
|
-
const
|
|
12473
|
+
const workspaces = await listWorkspaces(context, {
|
|
11588
12474
|
baseUrl: commandContext.apiBaseUrl,
|
|
11589
12475
|
token: commandContext.token
|
|
11590
|
-
})
|
|
12476
|
+
});
|
|
12477
|
+
const selected = findWorkspace(reference, workspaces);
|
|
11591
12478
|
if (!selected) throw new Error(`Workspace "${reference}" not found. Run \`donebear workspace list\`.`);
|
|
11592
12479
|
await setCurrentWorkspace(selected.id);
|
|
11593
12480
|
if (context.json) {
|
|
@@ -11758,10 +12645,14 @@ const runWorkspaceInvite = async (context, workspaceRefArg, workspaceOverride, o
|
|
|
11758
12645
|
token: commandContext.token,
|
|
11759
12646
|
workspaceRef: workspaceOverride ?? workspaceRefArg ?? null
|
|
11760
12647
|
});
|
|
12648
|
+
if (options.share && options.email) throw new CliError("Use --share or --email, not both.", EXIT_CODES.VALIDATION);
|
|
12649
|
+
if (options.project && !options.share) throw new CliError("--project requires --share.", EXIT_CODES.VALIDATION);
|
|
11761
12650
|
const invitation = await createWorkspaceInvitation(context, {
|
|
11762
12651
|
baseUrl: commandContext.apiBaseUrl,
|
|
11763
12652
|
email: options.email,
|
|
11764
|
-
|
|
12653
|
+
kind: options.share ? "share" : void 0,
|
|
12654
|
+
projectId: options.project,
|
|
12655
|
+
role: options.share ? "member" : options.role ?? "member",
|
|
11765
12656
|
token: commandContext.token,
|
|
11766
12657
|
workspaceId: workspace.id
|
|
11767
12658
|
});
|
|
@@ -11777,7 +12668,7 @@ const runWorkspaceInvite = async (context, workspaceRefArg, workspaceOverride, o
|
|
|
11777
12668
|
writeInfo(context, `role: ${invitation.role}`);
|
|
11778
12669
|
writeInfo(context, `expires: ${invitation.expiresAt}`);
|
|
11779
12670
|
if (invitation.email) writeInfo(context, `email: ${invitation.email}`);
|
|
11780
|
-
const link = `${
|
|
12671
|
+
const link = `${DEFAULT_APP_URL}/join?code=${encodeURIComponent(invitation.code)}`;
|
|
11781
12672
|
writeInfo(context, `link: ${link}`);
|
|
11782
12673
|
if (context.copy) {
|
|
11783
12674
|
if (copyToClipboard(link)) writeInfo(context, "Link copied to clipboard.");
|
|
@@ -11793,6 +12684,8 @@ Examples:
|
|
|
11793
12684
|
donebear workspace members
|
|
11794
12685
|
donebear workspace invitations
|
|
11795
12686
|
donebear workspace invite --email user@example.com --role member
|
|
12687
|
+
donebear workspace invite --share --copy
|
|
12688
|
+
donebear workspace invite --share --project <project-id>
|
|
11796
12689
|
`).action(async (_options, command) => {
|
|
11797
12690
|
const context = contextFromCommand(command);
|
|
11798
12691
|
await runWithErrorHandling(context, async () => {
|
|
@@ -11847,7 +12740,7 @@ Examples:
|
|
|
11847
12740
|
await runWorkspaceInvitations(context, workspaceRef, workspaceOverride);
|
|
11848
12741
|
});
|
|
11849
12742
|
});
|
|
11850
|
-
workspace.command("invite").description("Create an invitation for the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").option("--email <email>", "Email address to invite").option("--role <role>", "Role for the invitee: member|admin", "member").action(async (workspaceRef, options, command) => {
|
|
12743
|
+
workspace.command("invite").description("Create an invitation for the current or specified workspace").argument("[workspace]", "Workspace id, slug, or exact name").option("--email <email>", "Email address to invite").option("--share", "Create a reusable join link").option("--project <id>", "Scope a share link: joiners join the whole workspace as a member, get this project, and use a seat").option("--role <role>", "Role for the invitee: member|admin", "member").action(async (workspaceRef, options, command) => {
|
|
11851
12744
|
const context = contextFromCommand(command);
|
|
11852
12745
|
await runWithErrorHandling(context, async () => {
|
|
11853
12746
|
await runWorkspaceInvite(context, workspaceRef, workspaceOverride, options);
|
|
@@ -11875,8 +12768,9 @@ With auth:
|
|
|
11875
12768
|
donebear import things --dry-run --json
|
|
11876
12769
|
|
|
11877
12770
|
Auth:
|
|
11878
|
-
donebear auth
|
|
11879
|
-
|
|
12771
|
+
donebear auth signup --email you@… Create an account (human verifies email)
|
|
12772
|
+
donebear auth login OAuth browser login
|
|
12773
|
+
DONEBEAR_TOKEN=<token> Non-interactive / CI: set this env var
|
|
11880
12774
|
|
|
11881
12775
|
Workspace targeting:
|
|
11882
12776
|
donebear workspace=myorg task list
|
|
@@ -11935,7 +12829,8 @@ const main = async () => {
|
|
|
11935
12829
|
const hasUserArgs = processedArgv.length <= 2;
|
|
11936
12830
|
const isTTY = process.stdin.isTTY === true;
|
|
11937
12831
|
if (hasUserArgs && isTTY) {
|
|
11938
|
-
|
|
12832
|
+
const ttyContext = contextFromArgv(processedArgv);
|
|
12833
|
+
await runInteractiveMode(ttyContext, program);
|
|
11939
12834
|
return;
|
|
11940
12835
|
}
|
|
11941
12836
|
await program.parseAsync(processedArgv);
|