donebear 0.3.9 → 0.4.1
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 +9 -9
- package/README.md +28 -18
- package/dist/cli.mjs +815 -345
- package/dist/cli.mjs.map +1 -1
- package/package.json +17 -5
package/dist/cli.mjs
CHANGED
|
@@ -14,6 +14,7 @@ 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 { DEFAULT_RANK_GAP } from "@donebear/rank-logic";
|
|
17
18
|
import { createServer } from "node:http";
|
|
18
19
|
import { fileURLToPath } from "node:url";
|
|
19
20
|
import Database from "better-sqlite3";
|
|
@@ -308,17 +309,17 @@ const refreshAccessToken = (config, refreshToken) => {
|
|
|
308
309
|
};
|
|
309
310
|
//#endregion
|
|
310
311
|
//#region src/storage.ts
|
|
311
|
-
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
312
|
+
const isRecord$1 = (value) => typeof value === "object" && value !== null;
|
|
312
313
|
const isOAuthProvider = (value) => value === "google" || value === "github" || value === "oauth2";
|
|
313
314
|
const parseStoredAuthSession = (value) => {
|
|
314
|
-
if (!isRecord(value)) return null;
|
|
315
|
+
if (!isRecord$1(value)) return null;
|
|
315
316
|
if (typeof value.accessToken !== "string") return null;
|
|
316
317
|
if (value.refreshToken !== null && typeof value.refreshToken !== "string") return null;
|
|
317
318
|
if (value.tokenType !== null && typeof value.tokenType !== "string") return null;
|
|
318
319
|
if (value.expiresAt !== null && typeof value.expiresAt !== "string") return null;
|
|
319
320
|
if (!isOAuthProvider(value.provider)) return null;
|
|
320
321
|
const { user } = value;
|
|
321
|
-
if (user !== null && (!isRecord(user) || typeof user.id !== "string" || user.email !== null && typeof user.email !== "string")) return null;
|
|
322
|
+
if (user !== null && (!isRecord$1(user) || typeof user.id !== "string" || user.email !== null && typeof user.email !== "string")) return null;
|
|
322
323
|
if (typeof value.createdAt !== "string") return null;
|
|
323
324
|
const parsedUser = user === null ? null : {
|
|
324
325
|
email: user.email ?? null,
|
|
@@ -336,7 +337,7 @@ const parseStoredAuthSession = (value) => {
|
|
|
336
337
|
};
|
|
337
338
|
const parseAuthFile = (payload) => {
|
|
338
339
|
const parsed = JSON.parse(payload);
|
|
339
|
-
if (!isRecord(parsed) || parsed.version !== 1) throw new CliError(`Invalid auth cache format in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
|
|
340
|
+
if (!isRecord$1(parsed) || parsed.version !== 1) throw new CliError(`Invalid auth cache format in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
|
|
340
341
|
const session = parseStoredAuthSession(parsed.session);
|
|
341
342
|
if (!session) throw new CliError(`Invalid auth session in ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
|
|
342
343
|
return {
|
|
@@ -348,7 +349,7 @@ const readStoredAuthSession = async () => {
|
|
|
348
349
|
try {
|
|
349
350
|
return parseAuthFile(await readFile(AUTH_FILE_PATH, "utf-8")).session;
|
|
350
351
|
} catch (error) {
|
|
351
|
-
if (isRecord(error) && error.code === "ENOENT") return null;
|
|
352
|
+
if (isRecord$1(error) && error.code === "ENOENT") return null;
|
|
352
353
|
if (error instanceof CliError) throw error;
|
|
353
354
|
throw new CliError(`Failed to read credentials from ${AUTH_FILE_PATH}`, EXIT_CODES.ERROR);
|
|
354
355
|
}
|
|
@@ -486,6 +487,568 @@ const resolveTokenStatus = (token) => {
|
|
|
486
487
|
expiresInSeconds
|
|
487
488
|
};
|
|
488
489
|
};
|
|
490
|
+
/**
|
|
491
|
+
* Curated GraphQL selection for agent-facing task reads.
|
|
492
|
+
*
|
|
493
|
+
* This intentionally remains smaller than the Task sync bootstrap surface.
|
|
494
|
+
*/
|
|
495
|
+
const TASK_FIELDS = {
|
|
496
|
+
bootstrapFields: [
|
|
497
|
+
"id",
|
|
498
|
+
"key",
|
|
499
|
+
"title",
|
|
500
|
+
"description",
|
|
501
|
+
"sortOrder",
|
|
502
|
+
"todaySortOrder",
|
|
503
|
+
"todayIndexReferenceDate",
|
|
504
|
+
"createdAt",
|
|
505
|
+
"updatedAt",
|
|
506
|
+
"completedAt",
|
|
507
|
+
"archivedAt",
|
|
508
|
+
"start",
|
|
509
|
+
"startDate",
|
|
510
|
+
"startBucket",
|
|
511
|
+
"deadlineAt",
|
|
512
|
+
"deadlineSuppressionAt",
|
|
513
|
+
"reminderAt",
|
|
514
|
+
"lastReminderInteractionAt",
|
|
515
|
+
"creatorId",
|
|
516
|
+
"workspaceId",
|
|
517
|
+
"assigneeId",
|
|
518
|
+
"projectId",
|
|
519
|
+
"headingId",
|
|
520
|
+
"teamId",
|
|
521
|
+
"projectSortOrder",
|
|
522
|
+
"repeatRule",
|
|
523
|
+
"repeatType",
|
|
524
|
+
"repeatStatus",
|
|
525
|
+
"repeatTemplateId",
|
|
526
|
+
"repeatInstanceDate",
|
|
527
|
+
"repeatNextAt"
|
|
528
|
+
],
|
|
529
|
+
insertFields: [
|
|
530
|
+
"archivedAt",
|
|
531
|
+
"assigneeId",
|
|
532
|
+
"completedAt",
|
|
533
|
+
"createdAt",
|
|
534
|
+
"creatorId",
|
|
535
|
+
"deadlineAt",
|
|
536
|
+
"deadlineSuppressionAt",
|
|
537
|
+
"description",
|
|
538
|
+
"headingId",
|
|
539
|
+
"key",
|
|
540
|
+
"lastReminderInteractionAt",
|
|
541
|
+
"projectId",
|
|
542
|
+
"projectSortOrder",
|
|
543
|
+
"reminderAt",
|
|
544
|
+
"repeatInstanceDate",
|
|
545
|
+
"repeatNextAt",
|
|
546
|
+
"repeatRule",
|
|
547
|
+
"repeatStatus",
|
|
548
|
+
"repeatTemplateId",
|
|
549
|
+
"repeatType",
|
|
550
|
+
"sortOrder",
|
|
551
|
+
"start",
|
|
552
|
+
"startBucket",
|
|
553
|
+
"startDate",
|
|
554
|
+
"teamId",
|
|
555
|
+
"title",
|
|
556
|
+
"todayIndexReferenceDate",
|
|
557
|
+
"todaySortOrder",
|
|
558
|
+
"updatedAt",
|
|
559
|
+
"workspaceId"
|
|
560
|
+
],
|
|
561
|
+
iosKeys: [
|
|
562
|
+
"id",
|
|
563
|
+
"key",
|
|
564
|
+
"title",
|
|
565
|
+
"description",
|
|
566
|
+
"sortOrder",
|
|
567
|
+
"todaySortOrder",
|
|
568
|
+
"todayIndexReferenceDate",
|
|
569
|
+
"createdAt",
|
|
570
|
+
"updatedAt",
|
|
571
|
+
"completedAt",
|
|
572
|
+
"archivedAt",
|
|
573
|
+
"start",
|
|
574
|
+
"startDate",
|
|
575
|
+
"startBucket",
|
|
576
|
+
"deadlineAt",
|
|
577
|
+
"deadlineSuppressionAt",
|
|
578
|
+
"reminderAt",
|
|
579
|
+
"lastReminderInteractionAt",
|
|
580
|
+
"creatorId",
|
|
581
|
+
"workspaceId",
|
|
582
|
+
"assigneeId",
|
|
583
|
+
"projectId",
|
|
584
|
+
"headingId",
|
|
585
|
+
"teamId",
|
|
586
|
+
"projectSortOrder",
|
|
587
|
+
"repeatRule",
|
|
588
|
+
"repeatType",
|
|
589
|
+
"repeatStatus",
|
|
590
|
+
"repeatTemplateId",
|
|
591
|
+
"repeatInstanceDate",
|
|
592
|
+
"repeatNextAt"
|
|
593
|
+
],
|
|
594
|
+
model: "Task",
|
|
595
|
+
readProjection: [
|
|
596
|
+
"id",
|
|
597
|
+
"title",
|
|
598
|
+
"description",
|
|
599
|
+
"createdAt",
|
|
600
|
+
"updatedAt",
|
|
601
|
+
"completedAt",
|
|
602
|
+
"archivedAt",
|
|
603
|
+
"start",
|
|
604
|
+
"startDate",
|
|
605
|
+
"startBucket",
|
|
606
|
+
"todayIndexReferenceDate",
|
|
607
|
+
"deadlineAt",
|
|
608
|
+
"creatorId",
|
|
609
|
+
"workspaceId",
|
|
610
|
+
"projectId",
|
|
611
|
+
"teamId",
|
|
612
|
+
"assigneeId",
|
|
613
|
+
"headingId",
|
|
614
|
+
"repeatRule",
|
|
615
|
+
"repeatTemplateId"
|
|
616
|
+
],
|
|
617
|
+
updateFields: [
|
|
618
|
+
"title",
|
|
619
|
+
"description",
|
|
620
|
+
"sortOrder",
|
|
621
|
+
"todaySortOrder",
|
|
622
|
+
"todayIndexReferenceDate",
|
|
623
|
+
"createdAt",
|
|
624
|
+
"updatedAt",
|
|
625
|
+
"completedAt",
|
|
626
|
+
"archivedAt",
|
|
627
|
+
"start",
|
|
628
|
+
"startDate",
|
|
629
|
+
"startBucket",
|
|
630
|
+
"deadlineAt",
|
|
631
|
+
"deadlineSuppressionAt",
|
|
632
|
+
"reminderAt",
|
|
633
|
+
"lastReminderInteractionAt",
|
|
634
|
+
"creatorId",
|
|
635
|
+
"assigneeId",
|
|
636
|
+
"projectId",
|
|
637
|
+
"headingId",
|
|
638
|
+
"teamId",
|
|
639
|
+
"projectSortOrder",
|
|
640
|
+
"repeatRule",
|
|
641
|
+
"repeatType",
|
|
642
|
+
"repeatStatus",
|
|
643
|
+
"repeatTemplateId",
|
|
644
|
+
"repeatInstanceDate",
|
|
645
|
+
"repeatNextAt"
|
|
646
|
+
]
|
|
647
|
+
}.readProjection.join("\n");
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region ../../packages/task-logic/src/date-only.ts
|
|
650
|
+
const DATE_ONLY_DAY_MS = 1440 * 60 * 1e3;
|
|
651
|
+
const DATE_ONLY_INPUT_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/u;
|
|
652
|
+
const isFiniteEpoch$1 = (value) => typeof value === "number" && Number.isFinite(value);
|
|
653
|
+
const isCanonicalDateOnlyEpoch = (value) => value % DATE_ONLY_DAY_MS === 0;
|
|
654
|
+
const getUtcDateOnlyParts = (date) => ({
|
|
655
|
+
day: date.getUTCDate(),
|
|
656
|
+
month: date.getUTCMonth() + 1,
|
|
657
|
+
year: date.getUTCFullYear()
|
|
658
|
+
});
|
|
659
|
+
const isValidDateOnlyParts = (parts) => {
|
|
660
|
+
if (!(Number.isInteger(parts.year) && Number.isInteger(parts.month) && Number.isInteger(parts.day))) return false;
|
|
661
|
+
if (parts.year < 1 || parts.month < 1 || parts.month > 12 || parts.day < 1 || parts.day > 31) return false;
|
|
662
|
+
const candidate = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
|
|
663
|
+
return candidate.getUTCFullYear() === parts.year && candidate.getUTCMonth() + 1 === parts.month && candidate.getUTCDate() === parts.day;
|
|
664
|
+
};
|
|
665
|
+
const parseDateOnlyInput = (value) => {
|
|
666
|
+
if (typeof value !== "string") return null;
|
|
667
|
+
const match = DATE_ONLY_INPUT_PATTERN.exec(value.trim());
|
|
668
|
+
if (!match) return null;
|
|
669
|
+
const parts = {
|
|
670
|
+
day: Number(match[3]),
|
|
671
|
+
month: Number(match[2]),
|
|
672
|
+
year: Number(match[1])
|
|
673
|
+
};
|
|
674
|
+
return isValidDateOnlyParts(parts) ? parts : null;
|
|
675
|
+
};
|
|
676
|
+
const dateOnlyPartsToEpoch = (parts) => isValidDateOnlyParts(parts) ? Date.UTC(parts.year, parts.month - 1, parts.day) : null;
|
|
677
|
+
const epochToDateOnlyParts = (epoch) => {
|
|
678
|
+
if (!isFiniteEpoch$1(epoch)) return null;
|
|
679
|
+
const date = new Date(epoch);
|
|
680
|
+
return Number.isNaN(date.getTime()) ? null : getUtcDateOnlyParts(date);
|
|
681
|
+
};
|
|
682
|
+
const formatDateOnlyParts = (parts) => isValidDateOnlyParts(parts) ? `${String(parts.year).padStart(4, "0")}-${String(parts.month).padStart(2, "0")}-${String(parts.day).padStart(2, "0")}` : null;
|
|
683
|
+
const formatDateOnlyEpoch = (epoch) => {
|
|
684
|
+
const parts = epochToDateOnlyParts(epoch);
|
|
685
|
+
return parts ? formatDateOnlyParts(parts) : null;
|
|
686
|
+
};
|
|
687
|
+
const normalizeDateOnlyEpoch = (value) => {
|
|
688
|
+
if (!(isFiniteEpoch$1(value) && isCanonicalDateOnlyEpoch(value))) return null;
|
|
689
|
+
return value;
|
|
690
|
+
};
|
|
691
|
+
const parseDateOnlyInputToEpoch = (value) => {
|
|
692
|
+
const parts = parseDateOnlyInput(value);
|
|
693
|
+
return parts ? dateOnlyPartsToEpoch(parts) : null;
|
|
694
|
+
};
|
|
695
|
+
const localDateToDateOnlyEpoch = (date) => Date.UTC(date.getFullYear(), date.getMonth(), date.getDate());
|
|
696
|
+
const getTodayDateOnlyEpoch = (now = /* @__PURE__ */ new Date()) => localDateToDateOnlyEpoch(now);
|
|
697
|
+
//#endregion
|
|
698
|
+
//#region ../../packages/task-logic/src/task-status.ts
|
|
699
|
+
const normalizeStart = (value) => {
|
|
700
|
+
if (typeof value !== "string") return null;
|
|
701
|
+
const trimmed = value.trim().toLowerCase();
|
|
702
|
+
if (trimmed === "not_started") return "not_started";
|
|
703
|
+
if (trimmed === "started") return "started";
|
|
704
|
+
if (trimmed === "someday") return "someday";
|
|
705
|
+
return null;
|
|
706
|
+
};
|
|
707
|
+
const isNullish = (value) => value === null || value === void 0;
|
|
708
|
+
const isRepeatTemplate = (task) => !isNullish(task.repeatRule) && isNullish(task.repeatTemplateId);
|
|
709
|
+
const getTaskState$1 = (task) => {
|
|
710
|
+
if (!isNullish(task.archivedAt)) return "archived";
|
|
711
|
+
if (!isNullish(task.completedAt)) return "done";
|
|
712
|
+
return "open";
|
|
713
|
+
};
|
|
714
|
+
/**
|
|
715
|
+
* Derive the GTD bucket a task belongs to. `todayEpoch` is a canonical
|
|
716
|
+
* UTC-midnight date-only epoch (compute it via getTodayDateOnlyEpoch).
|
|
717
|
+
*
|
|
718
|
+
* Note: this deliberately does NOT hide repeat templates. Template hiding is a
|
|
719
|
+
* list/retention concern that callers apply via isRepeatTemplate.
|
|
720
|
+
*/
|
|
721
|
+
const getTaskView = (task, todayEpoch) => {
|
|
722
|
+
if (!isNullish(task.archivedAt)) return "trash";
|
|
723
|
+
if (!isNullish(task.completedAt)) return "logbook";
|
|
724
|
+
const start = normalizeStart(task.start);
|
|
725
|
+
const startDate = normalizeDateOnlyEpoch(task.startDate);
|
|
726
|
+
const deadline = normalizeDateOnlyEpoch(task.deadlineAt);
|
|
727
|
+
const isDeadlineDue = deadline !== null && deadline <= todayEpoch;
|
|
728
|
+
const tomorrow = todayEpoch + DATE_ONLY_DAY_MS;
|
|
729
|
+
if (start === null) return null;
|
|
730
|
+
if (start === "not_started") return isDeadlineDue ? "today" : "inbox";
|
|
731
|
+
if (start === "someday") return isDeadlineDue ? "today" : "someday";
|
|
732
|
+
if (startDate === null) return isDeadlineDue ? "today" : "anytime";
|
|
733
|
+
if (startDate < tomorrow || isDeadlineDue) return "today";
|
|
734
|
+
return "upcoming";
|
|
735
|
+
};
|
|
736
|
+
/**
|
|
737
|
+
* Canonical 3-field start shape for a target view. (cli/raycast additionally
|
|
738
|
+
* emit a 4th `todayIndexReferenceDate`; that is a client concern and is
|
|
739
|
+
* intentionally NOT produced here.)
|
|
740
|
+
*/
|
|
741
|
+
const buildTaskStartFields$1 = (view, options = {}) => {
|
|
742
|
+
const today = options.todayEpoch ?? getTodayDateOnlyEpoch(options.now);
|
|
743
|
+
const tomorrow = today + DATE_ONLY_DAY_MS;
|
|
744
|
+
const startDate = normalizeDateOnlyEpoch(options.startDate);
|
|
745
|
+
switch (view) {
|
|
746
|
+
case "inbox": return {
|
|
747
|
+
start: "not_started",
|
|
748
|
+
startBucket: "today",
|
|
749
|
+
startDate: null
|
|
750
|
+
};
|
|
751
|
+
case "anytime": return {
|
|
752
|
+
start: "started",
|
|
753
|
+
startBucket: "today",
|
|
754
|
+
startDate: null
|
|
755
|
+
};
|
|
756
|
+
case "someday": return {
|
|
757
|
+
start: "someday",
|
|
758
|
+
startBucket: "today",
|
|
759
|
+
startDate: null
|
|
760
|
+
};
|
|
761
|
+
case "today": return {
|
|
762
|
+
start: "started",
|
|
763
|
+
startBucket: "today",
|
|
764
|
+
startDate: startDate ?? today
|
|
765
|
+
};
|
|
766
|
+
case "upcoming": return {
|
|
767
|
+
start: "started",
|
|
768
|
+
startBucket: "upcoming",
|
|
769
|
+
startDate: startDate ?? tomorrow
|
|
770
|
+
};
|
|
771
|
+
default: return {
|
|
772
|
+
start: "not_started",
|
|
773
|
+
startBucket: "today",
|
|
774
|
+
startDate: null
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
//#endregion
|
|
779
|
+
//#region ../../packages/manage-client/src/mutations.ts
|
|
780
|
+
const startFieldsForView = (view, todayEpoch, startDate) => ({
|
|
781
|
+
...buildTaskStartFields$1(view, {
|
|
782
|
+
startDate,
|
|
783
|
+
todayEpoch
|
|
784
|
+
}),
|
|
785
|
+
todayIndexReferenceDate: view === "today" ? todayEpoch : null
|
|
786
|
+
});
|
|
787
|
+
const buildTaskInsertFields = (input) => ({
|
|
788
|
+
archivedAt: null,
|
|
789
|
+
completedAt: null,
|
|
790
|
+
createdAt: input.now,
|
|
791
|
+
creatorId: input.ids.creatorId,
|
|
792
|
+
deadlineAt: input.deadlineAt ?? null,
|
|
793
|
+
description: input.description ?? null,
|
|
794
|
+
key: input.ids.taskId.replaceAll("-", "").slice(0, 8),
|
|
795
|
+
projectId: input.projectId ?? null,
|
|
796
|
+
sortOrder: input.sortOrder ?? DEFAULT_RANK_GAP,
|
|
797
|
+
teamId: input.teamId ?? null,
|
|
798
|
+
title: input.title,
|
|
799
|
+
todaySortOrder: input.todaySortOrder ?? DEFAULT_RANK_GAP,
|
|
800
|
+
updatedAt: input.now,
|
|
801
|
+
workspaceId: input.ids.workspaceId,
|
|
802
|
+
...startFieldsForView(input.view, input.todayEpoch, input.startDate)
|
|
803
|
+
});
|
|
804
|
+
const buildMutateEnvelope = ({ batchId, clientId, clientTxIds, transactions }) => {
|
|
805
|
+
if (transactions.length !== clientTxIds.length) throw new Error("A client transaction ID is required for every transaction");
|
|
806
|
+
return {
|
|
807
|
+
batchId,
|
|
808
|
+
transactions: transactions.map((transaction, index) => {
|
|
809
|
+
const clientTxId = clientTxIds[index];
|
|
810
|
+
if (!clientTxId) throw new Error("Client transaction IDs cannot be empty");
|
|
811
|
+
return {
|
|
812
|
+
...transaction,
|
|
813
|
+
clientId,
|
|
814
|
+
clientTxId
|
|
815
|
+
};
|
|
816
|
+
})
|
|
817
|
+
};
|
|
818
|
+
};
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region ../../packages/manage-client/src/sync-mutate.ts
|
|
821
|
+
const normalizeBaseUrl$1 = (value) => value.endsWith("/") ? value.slice(0, -1) : value;
|
|
822
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
823
|
+
const parseResult = (value) => {
|
|
824
|
+
if (!isRecord(value)) return null;
|
|
825
|
+
if (typeof value.clientTxId !== "string" || typeof value.success !== "boolean") return null;
|
|
826
|
+
if (value.error !== void 0 && typeof value.error !== "string") return null;
|
|
827
|
+
if (value.syncId !== void 0 && typeof value.syncId !== "string") return null;
|
|
828
|
+
return {
|
|
829
|
+
clientTxId: value.clientTxId,
|
|
830
|
+
success: value.success,
|
|
831
|
+
...value.error === void 0 ? {} : { error: value.error },
|
|
832
|
+
...value.syncId === void 0 ? {} : { syncId: value.syncId }
|
|
833
|
+
};
|
|
834
|
+
};
|
|
835
|
+
const parseResponse = (value) => {
|
|
836
|
+
if (!isRecord(value) || typeof value.success !== "boolean" || typeof value.lastSyncId !== "string" || !Array.isArray(value.results)) return null;
|
|
837
|
+
const results = value.results.map(parseResult);
|
|
838
|
+
if (results.some((result) => result === null)) return null;
|
|
839
|
+
return {
|
|
840
|
+
lastSyncId: value.lastSyncId,
|
|
841
|
+
results,
|
|
842
|
+
success: value.success
|
|
843
|
+
};
|
|
844
|
+
};
|
|
845
|
+
const defaultErrorMapper$1 = (failure) => {
|
|
846
|
+
if (failure.type === "timeout") return new Error(`Sync mutation timed out after ${failure.timeoutMs}ms`, { cause: failure.cause });
|
|
847
|
+
if (failure.type === "http") return /* @__PURE__ */ new Error(`Sync mutation failed with HTTP ${failure.status}`);
|
|
848
|
+
if (failure.type === "mutation") {
|
|
849
|
+
const result = failure.response.results.find((item) => !item.success);
|
|
850
|
+
return new Error(result?.error ?? "Sync mutation failed");
|
|
851
|
+
}
|
|
852
|
+
if (failure.type === "invalid-response") return new Error("Sync mutation returned an invalid response", { cause: failure.cause });
|
|
853
|
+
return failure.cause instanceof Error ? failure.cause : new Error("Sync mutation request failed", { cause: failure.cause });
|
|
854
|
+
};
|
|
855
|
+
const isTimeoutError$1 = (error) => error instanceof Error && error.name === "TimeoutError";
|
|
856
|
+
const createSyncMutateClient = ({ baseUrl, errorMapper = defaultErrorMapper$1, fetch: fetchImplementation = globalThis.fetch, getToken, timeoutMs }) => {
|
|
857
|
+
if (!fetchImplementation) throw new Error("A fetch implementation is required");
|
|
858
|
+
const mutate = async (request, options = {}) => {
|
|
859
|
+
const timeoutSignal = timeoutMs === void 0 ? void 0 : AbortSignal.timeout(timeoutMs);
|
|
860
|
+
const signal = options.signal && timeoutSignal ? AbortSignal.any([options.signal, timeoutSignal]) : options.signal ?? timeoutSignal;
|
|
861
|
+
let response;
|
|
862
|
+
try {
|
|
863
|
+
response = await fetchImplementation(`${normalizeBaseUrl$1(baseUrl)}/sync/mutate`, {
|
|
864
|
+
body: JSON.stringify(request),
|
|
865
|
+
headers: {
|
|
866
|
+
Accept: "application/json",
|
|
867
|
+
Authorization: `Bearer ${await getToken()}`,
|
|
868
|
+
"Content-Type": "application/json"
|
|
869
|
+
},
|
|
870
|
+
method: "POST",
|
|
871
|
+
signal
|
|
872
|
+
});
|
|
873
|
+
} catch (error) {
|
|
874
|
+
if (timeoutMs !== void 0 && (timeoutSignal?.aborted || isTimeoutError$1(error))) throw errorMapper({
|
|
875
|
+
cause: error,
|
|
876
|
+
timeoutMs,
|
|
877
|
+
type: "timeout"
|
|
878
|
+
});
|
|
879
|
+
throw errorMapper({
|
|
880
|
+
cause: error,
|
|
881
|
+
type: "network"
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
if (!response.ok) throw errorMapper({
|
|
885
|
+
body: await response.text(),
|
|
886
|
+
status: response.status,
|
|
887
|
+
type: "http"
|
|
888
|
+
});
|
|
889
|
+
let raw;
|
|
890
|
+
try {
|
|
891
|
+
raw = await response.json();
|
|
892
|
+
} catch (error) {
|
|
893
|
+
throw errorMapper({
|
|
894
|
+
cause: error,
|
|
895
|
+
type: "invalid-response"
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
const payload = parseResponse(raw);
|
|
899
|
+
if (!payload) throw errorMapper({
|
|
900
|
+
cause: /* @__PURE__ */ new TypeError("Malformed sync mutation response"),
|
|
901
|
+
type: "invalid-response"
|
|
902
|
+
});
|
|
903
|
+
if (!payload.success) throw errorMapper({
|
|
904
|
+
response: payload,
|
|
905
|
+
type: "mutation"
|
|
906
|
+
});
|
|
907
|
+
return payload;
|
|
908
|
+
};
|
|
909
|
+
return { mutate };
|
|
910
|
+
};
|
|
911
|
+
//#endregion
|
|
912
|
+
//#region ../../packages/manage-client/src/index.ts
|
|
913
|
+
const TASKS_QUERY = `
|
|
914
|
+
query DonebearTasks($first: Int!, $after: String, $workspaceId: ID!) {
|
|
915
|
+
tasks(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
|
|
916
|
+
nodes { ${TASK_FIELDS} }
|
|
917
|
+
pageInfo { hasNextPage endCursor }
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
`;
|
|
921
|
+
const MY_WORKSPACES_QUERY = `
|
|
922
|
+
query MyWorkspaces {
|
|
923
|
+
myWorkspaces {
|
|
924
|
+
id name urlKey logoUrl role createdAt updatedAt
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
`;
|
|
928
|
+
const normalizeBaseUrl = (value) => value.endsWith("/") ? value.slice(0, -1) : value;
|
|
929
|
+
const toInstantEpoch = (value) => {
|
|
930
|
+
if (typeof value !== "string") return null;
|
|
931
|
+
const epoch = Date.parse(value);
|
|
932
|
+
return Number.isFinite(epoch) ? epoch : null;
|
|
933
|
+
};
|
|
934
|
+
const toTaskRecord$1 = (node) => ({
|
|
935
|
+
archivedAt: toInstantEpoch(node.archivedAt),
|
|
936
|
+
assigneeId: node.assigneeId,
|
|
937
|
+
completedAt: toInstantEpoch(node.completedAt),
|
|
938
|
+
createdAt: toInstantEpoch(node.createdAt),
|
|
939
|
+
creatorId: node.creatorId,
|
|
940
|
+
deadlineAt: parseDateOnlyInputToEpoch(node.deadlineAt),
|
|
941
|
+
description: node.description,
|
|
942
|
+
headingId: node.headingId,
|
|
943
|
+
id: node.id,
|
|
944
|
+
projectId: node.projectId,
|
|
945
|
+
repeatRule: node.repeatRule,
|
|
946
|
+
repeatTemplateId: node.repeatTemplateId,
|
|
947
|
+
start: node.start,
|
|
948
|
+
startBucket: node.startBucket,
|
|
949
|
+
startDate: parseDateOnlyInputToEpoch(node.startDate),
|
|
950
|
+
teamId: node.teamId,
|
|
951
|
+
title: node.title,
|
|
952
|
+
todayIndexReferenceDate: parseDateOnlyInputToEpoch(node.todayIndexReferenceDate),
|
|
953
|
+
updatedAt: toInstantEpoch(node.updatedAt),
|
|
954
|
+
workspaceId: node.workspaceId
|
|
955
|
+
});
|
|
956
|
+
const defaultErrorMapper = (failure) => {
|
|
957
|
+
if (failure.type === "timeout") return new Error(`GraphQL request timed out after ${failure.timeoutMs}ms`, { cause: failure.cause });
|
|
958
|
+
if (failure.type === "http") return /* @__PURE__ */ new Error(`GraphQL request failed with HTTP ${failure.status}`);
|
|
959
|
+
if (failure.type === "graphql") return new Error(failure.errors[0]?.message ?? "GraphQL request failed");
|
|
960
|
+
if (failure.type === "missing-data") return /* @__PURE__ */ new Error("GraphQL request returned no data");
|
|
961
|
+
return failure.cause instanceof Error ? failure.cause : new Error("GraphQL request failed", { cause: failure.cause });
|
|
962
|
+
};
|
|
963
|
+
const isTimeoutError = (error) => error instanceof Error && error.name === "TimeoutError";
|
|
964
|
+
const createGraphqlClient = ({ baseUrl, errorMapper = defaultErrorMapper, fetch: fetchImplementation = globalThis.fetch, getToken, timeoutMs }) => {
|
|
965
|
+
if (!fetchImplementation) throw new Error("A fetch implementation is required");
|
|
966
|
+
const graphqlRequest = async ({ query, signal: requestSignal, variables = {} }) => {
|
|
967
|
+
const timeoutSignal = timeoutMs === void 0 ? void 0 : AbortSignal.timeout(timeoutMs);
|
|
968
|
+
const signal = requestSignal && timeoutSignal ? AbortSignal.any([requestSignal, timeoutSignal]) : requestSignal ?? timeoutSignal;
|
|
969
|
+
let response;
|
|
970
|
+
try {
|
|
971
|
+
response = await fetchImplementation(`${normalizeBaseUrl(baseUrl)}/graphql`, {
|
|
972
|
+
body: JSON.stringify({
|
|
973
|
+
query,
|
|
974
|
+
variables
|
|
975
|
+
}),
|
|
976
|
+
headers: {
|
|
977
|
+
Authorization: `Bearer ${await getToken()}`,
|
|
978
|
+
"Content-Type": "application/json"
|
|
979
|
+
},
|
|
980
|
+
method: "POST",
|
|
981
|
+
signal
|
|
982
|
+
});
|
|
983
|
+
} catch (error) {
|
|
984
|
+
if (timeoutMs !== void 0 && (timeoutSignal?.aborted || isTimeoutError(error))) throw errorMapper({
|
|
985
|
+
cause: error,
|
|
986
|
+
timeoutMs,
|
|
987
|
+
type: "timeout"
|
|
988
|
+
});
|
|
989
|
+
throw errorMapper({
|
|
990
|
+
cause: error,
|
|
991
|
+
type: "network"
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
if (!response.ok) throw errorMapper({
|
|
995
|
+
body: await response.text(),
|
|
996
|
+
status: response.status,
|
|
997
|
+
type: "http"
|
|
998
|
+
});
|
|
999
|
+
let payload;
|
|
1000
|
+
try {
|
|
1001
|
+
payload = await response.json();
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
throw errorMapper({
|
|
1004
|
+
cause: error,
|
|
1005
|
+
type: "network"
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
if (payload.errors?.length) throw errorMapper({
|
|
1009
|
+
errors: payload.errors,
|
|
1010
|
+
type: "graphql"
|
|
1011
|
+
});
|
|
1012
|
+
if (payload.data === void 0) throw errorMapper({ type: "missing-data" });
|
|
1013
|
+
return payload.data;
|
|
1014
|
+
};
|
|
1015
|
+
const paginate = async ({ nodeKey, pageSize = 100, query, signal, transform, variables = {} }) => {
|
|
1016
|
+
const records = [];
|
|
1017
|
+
let after = null;
|
|
1018
|
+
for (let pageCount = 0; pageCount < 1e3; pageCount += 1) {
|
|
1019
|
+
const connection = (await graphqlRequest({
|
|
1020
|
+
query,
|
|
1021
|
+
signal,
|
|
1022
|
+
variables: {
|
|
1023
|
+
...variables,
|
|
1024
|
+
after,
|
|
1025
|
+
first: pageSize
|
|
1026
|
+
}
|
|
1027
|
+
}))[nodeKey];
|
|
1028
|
+
if (!connection) throw new Error(`GraphQL response is missing connection: ${nodeKey}`);
|
|
1029
|
+
for (const node of connection.nodes) records.push(transform(node));
|
|
1030
|
+
if (!connection.pageInfo.hasNextPage) return records;
|
|
1031
|
+
if (!connection.pageInfo.endCursor) throw new Error("GraphQL page has no end cursor");
|
|
1032
|
+
after = connection.pageInfo.endCursor;
|
|
1033
|
+
}
|
|
1034
|
+
throw new Error("GraphQL pagination exceeded safe page limit");
|
|
1035
|
+
};
|
|
1036
|
+
const listWorkspaces = async () => {
|
|
1037
|
+
return (await graphqlRequest({ query: MY_WORKSPACES_QUERY })).myWorkspaces.map((workspace) => ({ ...workspace }));
|
|
1038
|
+
};
|
|
1039
|
+
const listTasks = (workspaceId) => paginate({
|
|
1040
|
+
nodeKey: "tasks",
|
|
1041
|
+
query: TASKS_QUERY,
|
|
1042
|
+
transform: toTaskRecord$1,
|
|
1043
|
+
variables: { workspaceId }
|
|
1044
|
+
});
|
|
1045
|
+
return {
|
|
1046
|
+
graphqlRequest,
|
|
1047
|
+
listTasks,
|
|
1048
|
+
listWorkspaces,
|
|
1049
|
+
paginate
|
|
1050
|
+
};
|
|
1051
|
+
};
|
|
489
1052
|
//#endregion
|
|
490
1053
|
//#region src/api/http.ts
|
|
491
1054
|
const extractErrorMessage = (value) => {
|
|
@@ -536,62 +1099,15 @@ const requestNdjson = async (context, endpoint, options) => {
|
|
|
536
1099
|
};
|
|
537
1100
|
//#endregion
|
|
538
1101
|
//#region src/date-codecs.ts
|
|
539
|
-
const ISO_DATE_ONLY_REGEX = /^(\d{4})-(\d{2})-(\d{2})$/u;
|
|
540
1102
|
const ISO_INSTANT_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/u;
|
|
541
|
-
const pad = (value, length = 2) => String(value).padStart(length, "0");
|
|
542
1103
|
const isFiniteEpoch = (value) => typeof value === "number" && Number.isFinite(value);
|
|
543
|
-
const isValidDateOnlyParts = (parts) => {
|
|
544
|
-
if (!(Number.isInteger(parts.year) && Number.isInteger(parts.month) && Number.isInteger(parts.day))) return false;
|
|
545
|
-
if (parts.year < 1 || parts.month < 1 || parts.month > 12 || parts.day < 1 || parts.day > 31) return false;
|
|
546
|
-
const candidate = new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
|
|
547
|
-
return candidate.getUTCFullYear() === parts.year && candidate.getUTCMonth() === parts.month - 1 && candidate.getUTCDate() === parts.day;
|
|
548
|
-
};
|
|
549
|
-
const dateOnlyPartsToEpoch = (parts) => {
|
|
550
|
-
if (!isValidDateOnlyParts(parts)) return null;
|
|
551
|
-
return Date.UTC(parts.year, parts.month - 1, parts.day);
|
|
552
|
-
};
|
|
553
|
-
const epochToDateOnlyParts = (epochMs) => {
|
|
554
|
-
if (!isFiniteEpoch(epochMs)) return null;
|
|
555
|
-
const date = new Date(epochMs);
|
|
556
|
-
if (Number.isNaN(date.getTime())) return null;
|
|
557
|
-
return {
|
|
558
|
-
day: date.getUTCDate(),
|
|
559
|
-
month: date.getUTCMonth() + 1,
|
|
560
|
-
year: date.getUTCFullYear()
|
|
561
|
-
};
|
|
562
|
-
};
|
|
563
|
-
const formatDateOnlyParts = (parts) => {
|
|
564
|
-
if (!isValidDateOnlyParts(parts)) return null;
|
|
565
|
-
return `${pad(parts.year, 4)}-${pad(parts.month)}-${pad(parts.day)}`;
|
|
566
|
-
};
|
|
567
|
-
const formatDateOnlyEpoch = (epochMs) => {
|
|
568
|
-
const parts = epochToDateOnlyParts(epochMs);
|
|
569
|
-
return parts ? formatDateOnlyParts(parts) : null;
|
|
570
|
-
};
|
|
571
1104
|
const formatInstantEpoch = (epochMs) => {
|
|
572
1105
|
if (!isFiniteEpoch(epochMs)) return null;
|
|
573
1106
|
const date = new Date(epochMs);
|
|
574
1107
|
if (Number.isNaN(date.getTime())) return null;
|
|
575
1108
|
return date.toISOString();
|
|
576
1109
|
};
|
|
577
|
-
const
|
|
578
|
-
if (typeof value !== "string") return null;
|
|
579
|
-
const trimmed = value.trim();
|
|
580
|
-
const match = ISO_DATE_ONLY_REGEX.exec(trimmed);
|
|
581
|
-
if (!match) return null;
|
|
582
|
-
const year = Number.parseInt(match[1] ?? "", 10);
|
|
583
|
-
const month = Number.parseInt(match[2] ?? "", 10);
|
|
584
|
-
const parts = {
|
|
585
|
-
day: Number.parseInt(match[3] ?? "", 10),
|
|
586
|
-
month,
|
|
587
|
-
year
|
|
588
|
-
};
|
|
589
|
-
return isValidDateOnlyParts(parts) ? parts : null;
|
|
590
|
-
};
|
|
591
|
-
const parseApiDateOnlyToEpoch = (value) => {
|
|
592
|
-
const parts = parseDateOnlyString(value);
|
|
593
|
-
return parts ? dateOnlyPartsToEpoch(parts) : null;
|
|
594
|
-
};
|
|
1110
|
+
const parseApiDateOnlyToEpoch = (value) => parseDateOnlyInputToEpoch(value);
|
|
595
1111
|
const parseApiInstantToEpoch = (value) => {
|
|
596
1112
|
if (typeof value !== "string") return null;
|
|
597
1113
|
const trimmed = value.trim();
|
|
@@ -601,47 +1117,19 @@ const parseApiInstantToEpoch = (value) => {
|
|
|
601
1117
|
};
|
|
602
1118
|
const parseCliDateOnlyToEpoch = (value, fieldLabel) => {
|
|
603
1119
|
if (!value) return null;
|
|
604
|
-
const
|
|
605
|
-
if (
|
|
606
|
-
return
|
|
1120
|
+
const epoch = parseDateOnlyInputToEpoch(value);
|
|
1121
|
+
if (epoch === null) throw new Error(`Invalid ${fieldLabel}: ${value}. Expected YYYY-MM-DD.`);
|
|
1122
|
+
return epoch;
|
|
607
1123
|
};
|
|
608
|
-
const dateOnlyEpochFromLocalDate = (now = /* @__PURE__ */ new Date()) =>
|
|
1124
|
+
const dateOnlyEpochFromLocalDate = (now = /* @__PURE__ */ new Date()) => localDateToDateOnlyEpoch(now);
|
|
609
1125
|
//#endregion
|
|
610
1126
|
//#region src/manage-api.ts
|
|
611
|
-
const TASK_FIELDS = `
|
|
612
|
-
id
|
|
613
|
-
title
|
|
614
|
-
description
|
|
615
|
-
createdAt
|
|
616
|
-
updatedAt
|
|
617
|
-
completedAt
|
|
618
|
-
archivedAt
|
|
619
|
-
start
|
|
620
|
-
startDate
|
|
621
|
-
startBucket
|
|
622
|
-
todayIndexReferenceDate
|
|
623
|
-
deadlineAt
|
|
624
|
-
creatorId
|
|
625
|
-
workspaceId
|
|
626
|
-
projectId
|
|
627
|
-
teamId
|
|
628
|
-
assigneeId
|
|
629
|
-
headingId
|
|
630
|
-
repeatRule
|
|
631
|
-
repeatTemplateId
|
|
632
|
-
`;
|
|
633
|
-
const TASKS_QUERY = `
|
|
634
|
-
query DonebearTasks($first: Int!, $after: String, $workspaceId: ID!) {
|
|
635
|
-
tasks(first: $first, after: $after, filter: { workspaceId: { eq: $workspaceId } }) {
|
|
636
|
-
nodes { ${TASK_FIELDS} }
|
|
637
|
-
pageInfo { hasNextPage endCursor }
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
`;
|
|
641
1127
|
const TASKS_ALL_QUERY = `
|
|
642
1128
|
query DonebearTasksAll($first: Int!, $after: String, $workspaceIds: [ID!]!) {
|
|
643
1129
|
tasks(first: $first, after: $after, filter: { workspaceId: { in: $workspaceIds } }) {
|
|
644
|
-
nodes {
|
|
1130
|
+
nodes {
|
|
1131
|
+
${TASK_FIELDS}
|
|
1132
|
+
}
|
|
645
1133
|
pageInfo { hasNextPage endCursor }
|
|
646
1134
|
}
|
|
647
1135
|
}
|
|
@@ -700,13 +1188,6 @@ const TEAMS_ALL_QUERY = `
|
|
|
700
1188
|
}
|
|
701
1189
|
}
|
|
702
1190
|
`;
|
|
703
|
-
const MY_WORKSPACES_QUERY = `
|
|
704
|
-
query MyWorkspaces {
|
|
705
|
-
myWorkspaces {
|
|
706
|
-
id name urlKey logoUrl role createdAt updatedAt
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
`;
|
|
710
1191
|
const WORKSPACE_CREATE_MUTATION = `
|
|
711
1192
|
mutation WorkspaceCreate($input: WorkspaceCreateInput!) {
|
|
712
1193
|
workspaceCreate(input: $input) {
|
|
@@ -778,8 +1259,9 @@ const API_KEY_REVOKE_MUTATION = `
|
|
|
778
1259
|
}
|
|
779
1260
|
`;
|
|
780
1261
|
const normalizeApiUrl = (url) => url.endsWith("/") ? url.slice(0, -1) : url;
|
|
781
|
-
const
|
|
782
|
-
|
|
1262
|
+
const omitUndefined = (entries) => Object.fromEntries(Object.entries(entries).filter(([, value]) => value !== void 0));
|
|
1263
|
+
const requestGraphql = async (context, baseUrl, options) => {
|
|
1264
|
+
const response = await requestJson(context, `${baseUrl}/graphql`, {
|
|
783
1265
|
body: {
|
|
784
1266
|
query: options.query,
|
|
785
1267
|
variables: options.variables
|
|
@@ -795,14 +1277,13 @@ const requestGraphql = async (context, endpoint, options) => {
|
|
|
795
1277
|
return response.data;
|
|
796
1278
|
};
|
|
797
1279
|
const paginateGraphql = async (context, options) => {
|
|
798
|
-
const endpoint = `${options.baseUrl}/graphql`;
|
|
799
1280
|
const results = [];
|
|
800
1281
|
let after = null;
|
|
801
1282
|
let pageCount = 0;
|
|
802
1283
|
while (true) {
|
|
803
1284
|
pageCount += 1;
|
|
804
1285
|
if (pageCount > 1e3) throw new CliError("GraphQL pagination exceeded safe page limit", EXIT_CODES.ERROR);
|
|
805
|
-
const connection = (await requestGraphql(context,
|
|
1286
|
+
const connection = (await requestGraphql(context, options.baseUrl, {
|
|
806
1287
|
query: options.query,
|
|
807
1288
|
token: options.token,
|
|
808
1289
|
variables: {
|
|
@@ -851,28 +1332,6 @@ const toNullableNumber = (input) => {
|
|
|
851
1332
|
if (input === null || input === void 0) return null;
|
|
852
1333
|
return typeof input === "number" ? input : null;
|
|
853
1334
|
};
|
|
854
|
-
const toTaskRecordFromGraphqlNode = (node) => ({
|
|
855
|
-
archivedAt: parseApiInstantToEpoch(node.archivedAt),
|
|
856
|
-
assigneeId: node.assigneeId,
|
|
857
|
-
completedAt: parseApiInstantToEpoch(node.completedAt),
|
|
858
|
-
createdAt: parseApiInstantToEpoch(node.createdAt),
|
|
859
|
-
creatorId: node.creatorId,
|
|
860
|
-
deadlineAt: parseApiDateOnlyToEpoch(node.deadlineAt),
|
|
861
|
-
description: node.description,
|
|
862
|
-
headingId: node.headingId,
|
|
863
|
-
id: node.id,
|
|
864
|
-
projectId: node.projectId,
|
|
865
|
-
repeatRule: node.repeatRule,
|
|
866
|
-
repeatTemplateId: node.repeatTemplateId,
|
|
867
|
-
start: node.start,
|
|
868
|
-
startBucket: node.startBucket,
|
|
869
|
-
startDate: parseApiDateOnlyToEpoch(node.startDate),
|
|
870
|
-
teamId: node.teamId,
|
|
871
|
-
title: node.title,
|
|
872
|
-
todayIndexReferenceDate: parseApiDateOnlyToEpoch(node.todayIndexReferenceDate),
|
|
873
|
-
updatedAt: parseApiInstantToEpoch(node.updatedAt),
|
|
874
|
-
workspaceId: node.workspaceId
|
|
875
|
-
});
|
|
876
1335
|
const toProjectRecordFromGraphqlNode = (node) => ({
|
|
877
1336
|
archivedAt: parseApiInstantToEpoch(node.archivedAt),
|
|
878
1337
|
completedAt: parseApiInstantToEpoch(node.completedAt),
|
|
@@ -905,15 +1364,6 @@ const toTeamRecordFromGraphqlNode = (node) => ({
|
|
|
905
1364
|
updatedAt: parseApiInstantToEpoch(node.updatedAt),
|
|
906
1365
|
workspaceId: node.workspaceId
|
|
907
1366
|
});
|
|
908
|
-
const toWorkspaceSummaryFromGraphqlNode = (node) => ({
|
|
909
|
-
createdAt: node.createdAt,
|
|
910
|
-
id: node.id,
|
|
911
|
-
logoUrl: node.logoUrl,
|
|
912
|
-
name: node.name,
|
|
913
|
-
role: node.role,
|
|
914
|
-
updatedAt: node.updatedAt,
|
|
915
|
-
urlKey: node.urlKey
|
|
916
|
-
});
|
|
917
1367
|
const toWorkspaceRecordFromGraphqlNode = (node) => ({
|
|
918
1368
|
archivedAt: node.archivedAt,
|
|
919
1369
|
createdAt: node.createdAt,
|
|
@@ -993,7 +1443,7 @@ const VIEWER_QUERY = `
|
|
|
993
1443
|
}
|
|
994
1444
|
`;
|
|
995
1445
|
const loadViewer = async (context, options) => {
|
|
996
|
-
const data = await requestGraphql(context,
|
|
1446
|
+
const data = await requestGraphql(context, options.baseUrl, {
|
|
997
1447
|
query: VIEWER_QUERY,
|
|
998
1448
|
token: options.token,
|
|
999
1449
|
variables: {}
|
|
@@ -1004,22 +1454,19 @@ const loadViewer = async (context, options) => {
|
|
|
1004
1454
|
id: data.viewer.id
|
|
1005
1455
|
};
|
|
1006
1456
|
};
|
|
1007
|
-
const listWorkspaces = async (
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
variables: {}
|
|
1012
|
-
})).myWorkspaces.map(toWorkspaceSummaryFromGraphqlNode);
|
|
1013
|
-
};
|
|
1457
|
+
const listWorkspaces = async (_context, options) => await createGraphqlClient({
|
|
1458
|
+
baseUrl: options.baseUrl,
|
|
1459
|
+
getToken: () => options.token
|
|
1460
|
+
}).listWorkspaces();
|
|
1014
1461
|
const createWorkspace = async (context, options) => {
|
|
1015
|
-
return toWorkspaceRecordFromGraphqlNode((await requestGraphql(context,
|
|
1462
|
+
return toWorkspaceRecordFromGraphqlNode((await requestGraphql(context, options.baseUrl, {
|
|
1016
1463
|
query: WORKSPACE_CREATE_MUTATION,
|
|
1017
1464
|
token: options.token,
|
|
1018
1465
|
variables: { input: options.body }
|
|
1019
1466
|
})).workspaceCreate.workspace);
|
|
1020
1467
|
};
|
|
1021
1468
|
const joinWorkspace = async (context, options) => {
|
|
1022
|
-
const data = await requestGraphql(context,
|
|
1469
|
+
const data = await requestGraphql(context, options.baseUrl, {
|
|
1023
1470
|
query: WORKSPACE_JOIN_MUTATION,
|
|
1024
1471
|
token: options.token,
|
|
1025
1472
|
variables: { code: options.code }
|
|
@@ -1029,20 +1476,16 @@ const joinWorkspace = async (context, options) => {
|
|
|
1029
1476
|
workspace: toWorkspaceRecordFromGraphqlNode(data.workspaceJoin.workspace)
|
|
1030
1477
|
};
|
|
1031
1478
|
};
|
|
1032
|
-
const loadTasksByWorkspace = (
|
|
1479
|
+
const loadTasksByWorkspace = (_context, options) => createGraphqlClient({
|
|
1033
1480
|
baseUrl: options.baseUrl,
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
token: options.token,
|
|
1037
|
-
transform: toTaskRecordFromGraphqlNode,
|
|
1038
|
-
variables: { workspaceId: options.workspaceId }
|
|
1039
|
-
});
|
|
1481
|
+
getToken: () => options.token
|
|
1482
|
+
}).listTasks(options.workspaceId);
|
|
1040
1483
|
const loadTasksByWorkspaceIds = (context, options) => paginateGraphql(context, {
|
|
1041
1484
|
baseUrl: options.baseUrl,
|
|
1042
1485
|
nodeKey: "tasks",
|
|
1043
1486
|
query: TASKS_ALL_QUERY,
|
|
1044
1487
|
token: options.token,
|
|
1045
|
-
transform:
|
|
1488
|
+
transform: toTaskRecord$1,
|
|
1046
1489
|
variables: { workspaceIds: options.workspaceIds }
|
|
1047
1490
|
});
|
|
1048
1491
|
const loadTaskById = async (context, options) => {
|
|
@@ -1124,6 +1567,21 @@ const loadChecklistItemsByTask = async (context, options) => {
|
|
|
1124
1567
|
}
|
|
1125
1568
|
return results;
|
|
1126
1569
|
};
|
|
1570
|
+
const mapSyncMutateFailure = (failure) => {
|
|
1571
|
+
if (failure.type === "http") {
|
|
1572
|
+
const { body: responseBody, status } = failure;
|
|
1573
|
+
let body = responseBody;
|
|
1574
|
+
try {
|
|
1575
|
+
body = responseBody.length > 0 ? JSON.parse(responseBody) : null;
|
|
1576
|
+
} catch {}
|
|
1577
|
+
const exitCode = status === 401 || status === 403 ? EXIT_CODES.AUTH_REQUIRED : EXIT_CODES.ERROR;
|
|
1578
|
+
return new CliError(extractErrorMessage(body), exitCode);
|
|
1579
|
+
}
|
|
1580
|
+
if (failure.type === "mutation") return new CliError(failure.response.results.find((item) => !item.success)?.error ?? "Sync mutation failed", EXIT_CODES.ERROR);
|
|
1581
|
+
if (failure.type === "timeout") return new CliError(`Sync mutation timed out after ${failure.timeoutMs}ms`, EXIT_CODES.ERROR);
|
|
1582
|
+
if (failure.cause instanceof Error) return failure.cause;
|
|
1583
|
+
return new CliError("Sync mutation request failed", EXIT_CODES.ERROR);
|
|
1584
|
+
};
|
|
1127
1585
|
const mutateModel = async (context, options) => {
|
|
1128
1586
|
if (context.dryRun) {
|
|
1129
1587
|
writeWarning(context, "dry run: mutation skipped");
|
|
@@ -1138,16 +1596,11 @@ const mutateModel = async (context, options) => {
|
|
|
1138
1596
|
});
|
|
1139
1597
|
return;
|
|
1140
1598
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
});
|
|
1146
|
-
if (!response.success) {
|
|
1147
|
-
const error = response.results.find((result) => !result.success);
|
|
1148
|
-
if (error?.error) throw new CliError(error.error, EXIT_CODES.ERROR);
|
|
1149
|
-
throw new CliError("Sync mutation failed", EXIT_CODES.ERROR);
|
|
1150
|
-
}
|
|
1599
|
+
await createSyncMutateClient({
|
|
1600
|
+
baseUrl: options.baseUrl,
|
|
1601
|
+
errorMapper: mapSyncMutateFailure,
|
|
1602
|
+
getToken: () => options.token
|
|
1603
|
+
}).mutate(options.request);
|
|
1151
1604
|
};
|
|
1152
1605
|
const listWorkspaceMembers = async (context, options) => await paginateGraphql(context, {
|
|
1153
1606
|
baseUrl: options.baseUrl,
|
|
@@ -1166,36 +1619,37 @@ const listWorkspaceInvitations = async (context, options) => await paginateGraph
|
|
|
1166
1619
|
variables: { workspaceId: options.workspaceId }
|
|
1167
1620
|
});
|
|
1168
1621
|
const createWorkspaceInvitation = async (context, options) => {
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1622
|
+
const input = omitUndefined({
|
|
1623
|
+
email: options.email,
|
|
1624
|
+
role: options.role,
|
|
1625
|
+
workspaceId: options.workspaceId
|
|
1626
|
+
});
|
|
1627
|
+
return toWorkspaceInvitationRecordFromGraphqlNode((await requestGraphql(context, options.baseUrl, {
|
|
1174
1628
|
query: WORKSPACE_INVITATION_CREATE_MUTATION,
|
|
1175
1629
|
token: options.token,
|
|
1176
1630
|
variables: { input }
|
|
1177
1631
|
})).workspaceInvitationCreate.invitation);
|
|
1178
1632
|
};
|
|
1179
1633
|
const listWorkspaceHistory = async (context, options) => {
|
|
1180
|
-
const
|
|
1181
|
-
const variables = {
|
|
1634
|
+
const variables = omitUndefined({
|
|
1182
1635
|
limit: options.limit,
|
|
1636
|
+
model: options.model || void 0,
|
|
1183
1637
|
workspaceId: options.workspaceId
|
|
1184
|
-
};
|
|
1185
|
-
|
|
1186
|
-
return (await requestGraphql(context, endpoint, {
|
|
1638
|
+
});
|
|
1639
|
+
return (await requestGraphql(context, options.baseUrl, {
|
|
1187
1640
|
query: WORKSPACE_HISTORY_QUERY,
|
|
1188
1641
|
token: options.token,
|
|
1189
1642
|
variables
|
|
1190
1643
|
})).workspaceHistory.map(toWorkspaceHistoryEntryFromGraphqlNode);
|
|
1191
1644
|
};
|
|
1192
1645
|
const createApiKey = async (context, options) => {
|
|
1193
|
-
const
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1646
|
+
const variables = omitUndefined({
|
|
1647
|
+
expiresAt: options.expiresAt,
|
|
1648
|
+
label: options.label,
|
|
1649
|
+
scopes: options.scopes,
|
|
1650
|
+
workspaceIds: options.workspaceIds
|
|
1651
|
+
});
|
|
1652
|
+
const data = await requestGraphql(context, options.baseUrl, {
|
|
1199
1653
|
query: API_KEY_CREATE_MUTATION,
|
|
1200
1654
|
token: options.token,
|
|
1201
1655
|
variables
|
|
@@ -1206,14 +1660,14 @@ const createApiKey = async (context, options) => {
|
|
|
1206
1660
|
};
|
|
1207
1661
|
};
|
|
1208
1662
|
const listApiKeys = async (context, options) => {
|
|
1209
|
-
return (await requestGraphql(context,
|
|
1663
|
+
return (await requestGraphql(context, options.baseUrl, {
|
|
1210
1664
|
query: API_KEYS_QUERY,
|
|
1211
1665
|
token: options.token,
|
|
1212
1666
|
variables: {}
|
|
1213
1667
|
})).apiKeys.map(toApiKeyRecordFromGraphqlNode);
|
|
1214
1668
|
};
|
|
1215
1669
|
const revokeApiKey = async (context, options) => {
|
|
1216
|
-
await requestGraphql(context,
|
|
1670
|
+
await requestGraphql(context, options.baseUrl, {
|
|
1217
1671
|
query: API_KEY_REVOKE_MUTATION,
|
|
1218
1672
|
token: options.token,
|
|
1219
1673
|
variables: { id: options.keyId }
|
|
@@ -1420,7 +1874,7 @@ const runCreate = async (context, label, options) => {
|
|
|
1420
1874
|
if (key.workspaceIds && key.workspaceIds.length > 0) writeInfo(context, ` Restricted to workspaces: ${key.workspaceIds.join(", ")}`);
|
|
1421
1875
|
if (key.expiresAt) writeInfo(context, ` Expires: ${new Date(key.expiresAt).toLocaleDateString()}`);
|
|
1422
1876
|
writeInfo(context, "");
|
|
1423
|
-
writeWarning(context, "Save this key now
|
|
1877
|
+
writeWarning(context, "Save this key now; it will not be shown again.");
|
|
1424
1878
|
};
|
|
1425
1879
|
const runList = async (context) => {
|
|
1426
1880
|
const { token } = await requireAuthToken(context);
|
|
@@ -1765,72 +2219,24 @@ Non-interactive / CI use:
|
|
|
1765
2219
|
};
|
|
1766
2220
|
//#endregion
|
|
1767
2221
|
//#region src/task-defaults.ts
|
|
1768
|
-
const DAY_MS = 1440 * 60 * 1e3;
|
|
1769
2222
|
const buildTaskStartFields = (view, now = /* @__PURE__ */ new Date()) => {
|
|
1770
|
-
const today =
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
startBucket: "today",
|
|
1776
|
-
startDate: null,
|
|
1777
|
-
todayIndexReferenceDate: null
|
|
1778
|
-
};
|
|
1779
|
-
case "anytime": return {
|
|
1780
|
-
start: "started",
|
|
1781
|
-
startBucket: "today",
|
|
1782
|
-
startDate: null,
|
|
1783
|
-
todayIndexReferenceDate: null
|
|
1784
|
-
};
|
|
1785
|
-
case "today": return {
|
|
1786
|
-
start: "started",
|
|
1787
|
-
startBucket: "today",
|
|
1788
|
-
startDate: today,
|
|
1789
|
-
todayIndexReferenceDate: today
|
|
1790
|
-
};
|
|
1791
|
-
case "upcoming": return {
|
|
1792
|
-
start: "started",
|
|
1793
|
-
startBucket: "upcoming",
|
|
1794
|
-
startDate: tomorrow,
|
|
1795
|
-
todayIndexReferenceDate: null
|
|
1796
|
-
};
|
|
1797
|
-
case "someday": return {
|
|
1798
|
-
start: "someday",
|
|
1799
|
-
startBucket: "today",
|
|
1800
|
-
startDate: null,
|
|
1801
|
-
todayIndexReferenceDate: null
|
|
1802
|
-
};
|
|
1803
|
-
default: return {
|
|
1804
|
-
start: "not_started",
|
|
1805
|
-
startBucket: "today",
|
|
1806
|
-
startDate: null,
|
|
1807
|
-
todayIndexReferenceDate: null
|
|
1808
|
-
};
|
|
1809
|
-
}
|
|
1810
|
-
};
|
|
1811
|
-
const getTaskState = (task) => {
|
|
1812
|
-
if (task.archivedAt !== null) return "archived";
|
|
1813
|
-
if (task.completedAt !== null) return "done";
|
|
1814
|
-
return "open";
|
|
2223
|
+
const today = getTodayDateOnlyEpoch(now);
|
|
2224
|
+
return {
|
|
2225
|
+
...buildTaskStartFields$1(view, { now }),
|
|
2226
|
+
todayIndexReferenceDate: view === "today" ? today : null
|
|
2227
|
+
};
|
|
1815
2228
|
};
|
|
2229
|
+
const getTaskState = (task) => getTaskState$1(task);
|
|
1816
2230
|
/**
|
|
1817
2231
|
* Determine which view a task belongs to.
|
|
1818
|
-
*
|
|
2232
|
+
*
|
|
2233
|
+
* Repeat templates are hidden from the CLI's list-level views (they only exist
|
|
2234
|
+
* to generate instances); everything else delegates to the canonical
|
|
2235
|
+
* task-logic derivation shared across every client.
|
|
1819
2236
|
*/
|
|
1820
2237
|
const resolveTaskView = (task) => {
|
|
1821
|
-
if (task
|
|
1822
|
-
|
|
1823
|
-
if (task.completedAt !== null) return "logbook";
|
|
1824
|
-
const { start, startDate, deadlineAt } = task;
|
|
1825
|
-
const today = dateOnlyEpochFromLocalDate();
|
|
1826
|
-
const tomorrow = today + DAY_MS;
|
|
1827
|
-
const hasDeadlineToday = deadlineAt !== null && deadlineAt >= today && deadlineAt < tomorrow;
|
|
1828
|
-
if (start === "not_started") return hasDeadlineToday ? "today" : "inbox";
|
|
1829
|
-
if (start === "someday") return hasDeadlineToday ? "today" : "someday";
|
|
1830
|
-
if (start !== "started") return null;
|
|
1831
|
-
if (startDate === null) return hasDeadlineToday ? "today" : "anytime";
|
|
1832
|
-
if (startDate < tomorrow || hasDeadlineToday) return "today";
|
|
1833
|
-
return "upcoming";
|
|
2238
|
+
if (isRepeatTemplate(task)) return null;
|
|
2239
|
+
return getTaskView(task, getTodayDateOnlyEpoch());
|
|
1834
2240
|
};
|
|
1835
2241
|
const formatEpochDate = (epochMs) => formatInstantEpoch(epochMs);
|
|
1836
2242
|
//#endregion
|
|
@@ -2002,7 +2408,7 @@ const formatMarkdownOutput = (context, user, workspace, taskCounts, recentTasks,
|
|
|
2002
2408
|
lines.push("", "## Recent Open Tasks");
|
|
2003
2409
|
for (const t of recentTasks) {
|
|
2004
2410
|
const deadline = t.deadline ? `, due ${t.deadline}` : "";
|
|
2005
|
-
lines.push(`- [${t.id.slice(0, 8)}] ${t.title}
|
|
2411
|
+
lines.push(`- [${t.id.slice(0, 8)}] ${t.title} (${t.view})${deadline}`);
|
|
2006
2412
|
}
|
|
2007
2413
|
}
|
|
2008
2414
|
if (activeProjects.length > 0) {
|
|
@@ -2371,39 +2777,75 @@ const summarizeThingsImportResult = (result) => {
|
|
|
2371
2777
|
//#region src/import/things-labels.ts
|
|
2372
2778
|
const normalizeThingsLabelLookupKey = (value) => value.trim().toLocaleLowerCase();
|
|
2373
2779
|
//#endregion
|
|
2374
|
-
//#region
|
|
2375
|
-
|
|
2780
|
+
//#region ../../packages/sync-ids/node_modules/uuid/dist-node/regex.js
|
|
2781
|
+
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;
|
|
2782
|
+
//#endregion
|
|
2783
|
+
//#region ../../packages/sync-ids/node_modules/uuid/dist-node/validate.js
|
|
2784
|
+
function validate(uuid) {
|
|
2785
|
+
return typeof uuid === "string" && regex_default.test(uuid);
|
|
2786
|
+
}
|
|
2787
|
+
//#endregion
|
|
2788
|
+
//#region ../../packages/sync-ids/node_modules/uuid/dist-node/parse.js
|
|
2789
|
+
function parse(uuid) {
|
|
2790
|
+
if (!validate(uuid)) throw TypeError("Invalid UUID");
|
|
2791
|
+
let v;
|
|
2792
|
+
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);
|
|
2793
|
+
}
|
|
2794
|
+
//#endregion
|
|
2795
|
+
//#region ../../packages/sync-ids/node_modules/uuid/dist-node/stringify.js
|
|
2796
|
+
const byteToHex = [];
|
|
2797
|
+
for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1));
|
|
2798
|
+
function unsafeStringify(arr, offset = 0) {
|
|
2799
|
+
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();
|
|
2800
|
+
}
|
|
2801
|
+
//#endregion
|
|
2802
|
+
//#region ../../packages/sync-ids/node_modules/uuid/dist-node/v35.js
|
|
2803
|
+
function stringToBytes(str) {
|
|
2804
|
+
str = unescape(encodeURIComponent(str));
|
|
2805
|
+
const bytes = new Uint8Array(str.length);
|
|
2806
|
+
for (let i = 0; i < str.length; ++i) bytes[i] = str.charCodeAt(i);
|
|
2807
|
+
return bytes;
|
|
2808
|
+
}
|
|
2809
|
+
const DNS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
|
|
2810
|
+
const URL$1 = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
|
|
2811
|
+
function v35(version, hash, value, namespace, buf, offset) {
|
|
2812
|
+
const valueBytes = typeof value === "string" ? stringToBytes(value) : value;
|
|
2813
|
+
const namespaceBytes = typeof namespace === "string" ? parse(namespace) : namespace;
|
|
2814
|
+
if (typeof namespace === "string") namespace = parse(namespace);
|
|
2815
|
+
if (namespace?.length !== 16) throw TypeError("Namespace must be array-like (16 iterable integer values, 0-255)");
|
|
2816
|
+
let bytes = new Uint8Array(16 + valueBytes.length);
|
|
2817
|
+
bytes.set(namespaceBytes);
|
|
2818
|
+
bytes.set(valueBytes, namespaceBytes.length);
|
|
2819
|
+
bytes = hash(bytes);
|
|
2820
|
+
bytes[6] = bytes[6] & 15 | version;
|
|
2821
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
2822
|
+
if (buf) {
|
|
2823
|
+
offset ??= 0;
|
|
2824
|
+
if (offset < 0 || offset + 16 > buf.length) throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
|
|
2825
|
+
for (let i = 0; i < 16; ++i) buf[offset + i] = bytes[i];
|
|
2826
|
+
return buf;
|
|
2827
|
+
}
|
|
2828
|
+
return unsafeStringify(bytes);
|
|
2829
|
+
}
|
|
2830
|
+
//#endregion
|
|
2831
|
+
//#region ../../packages/sync-ids/node_modules/uuid/dist-node/sha1.js
|
|
2832
|
+
function sha1(bytes) {
|
|
2833
|
+
if (Array.isArray(bytes)) bytes = Buffer.from(bytes);
|
|
2834
|
+
else if (typeof bytes === "string") bytes = Buffer.from(bytes, "utf8");
|
|
2835
|
+
return createHash("sha1").update(bytes).digest();
|
|
2836
|
+
}
|
|
2837
|
+
//#endregion
|
|
2838
|
+
//#region ../../packages/sync-ids/node_modules/uuid/dist-node/v5.js
|
|
2839
|
+
function v5(value, namespace, buf, offset) {
|
|
2840
|
+
return v35(80, sha1, value, namespace, buf, offset);
|
|
2841
|
+
}
|
|
2842
|
+
v5.DNS = DNS;
|
|
2843
|
+
v5.URL = URL$1;
|
|
2844
|
+
//#endregion
|
|
2845
|
+
//#region ../../packages/sync-ids/src/index.ts
|
|
2376
2846
|
const SYNC_COMPOSITE_ID_NAMESPACE = "92a73695-d772-4b43-9fb4-d79f5fbef300";
|
|
2377
|
-
const
|
|
2378
|
-
const
|
|
2379
|
-
const normalized = uuid.replaceAll("-", "");
|
|
2380
|
-
if (normalized.length !== 32) throw new Error(`Invalid UUID namespace: ${uuid}`);
|
|
2381
|
-
return Uint8Array.from(Buffer.from(normalized, "hex"));
|
|
2382
|
-
};
|
|
2383
|
-
const bytesToUuid = (bytes) => {
|
|
2384
|
-
const hex = Buffer.from(bytes).toString("hex");
|
|
2385
|
-
return [
|
|
2386
|
-
hex.slice(0, 8),
|
|
2387
|
-
hex.slice(8, 12),
|
|
2388
|
-
hex.slice(12, 16),
|
|
2389
|
-
hex.slice(16, 20),
|
|
2390
|
-
hex.slice(20, 32)
|
|
2391
|
-
].join("-");
|
|
2392
|
-
};
|
|
2393
|
-
const createUuidV5 = (name, namespace) => {
|
|
2394
|
-
const namespaceBytes = uuidToBytes(namespace);
|
|
2395
|
-
const hash = createHash("sha1").update(namespaceBytes).update(Buffer.from(name, "utf-8")).digest();
|
|
2396
|
-
const bytes = Uint8Array.from(hash.subarray(0, 16));
|
|
2397
|
-
const versionByte = bytes.at(6);
|
|
2398
|
-
const variantByte = bytes.at(8);
|
|
2399
|
-
if (typeof versionByte !== "number" || typeof variantByte !== "number") throw new TypeError("Failed to generate a deterministic UUID.");
|
|
2400
|
-
bytes[6] = versionByte & 15 | 80;
|
|
2401
|
-
bytes[8] = variantByte & 63 | 128;
|
|
2402
|
-
return bytesToUuid(bytes);
|
|
2403
|
-
};
|
|
2404
|
-
const createCompositeSyncId = (model, parts) => createUuidV5(`${model}:${JSON.stringify(parts)}`, SYNC_COMPOSITE_ID_NAMESPACE);
|
|
2405
|
-
const createDeterministicImportId = (params) => createUuidV5(`things:${params.workspaceId}:${params.modelName}:${params.sourceId}`, IMPORT_ID_NAMESPACE);
|
|
2406
|
-
const createIdempotentMutationId = (params) => createUuidV5(`idem:${params.workspaceId}:${params.modelName}:${params.part}:${params.key}`, IDEMPOTENCY_ID_NAMESPACE);
|
|
2847
|
+
const createDeterministicId = (namespace, name) => v5(name, namespace);
|
|
2848
|
+
const createCompositeSyncId = (model, parts) => createDeterministicId(SYNC_COMPOSITE_ID_NAMESPACE, `${model}:${JSON.stringify(parts)}`);
|
|
2407
2849
|
const createTaskLabelId = (params) => createCompositeSyncId("TaskLabel", [
|
|
2408
2850
|
params.workspaceId,
|
|
2409
2851
|
params.taskId,
|
|
@@ -2415,6 +2857,12 @@ const createTeamProjectId = (params) => createCompositeSyncId("TeamProject", [
|
|
|
2415
2857
|
params.projectId
|
|
2416
2858
|
]);
|
|
2417
2859
|
//#endregion
|
|
2860
|
+
//#region src/import/model-id.ts
|
|
2861
|
+
const IMPORT_ID_NAMESPACE = "b1f9f0ac-9f76-54ed-8eb0-7d6d3a0a0a31";
|
|
2862
|
+
const IDEMPOTENCY_ID_NAMESPACE = "4c8af5e2-1d36-5a0b-9c47-2f81b3d6e955";
|
|
2863
|
+
const createDeterministicImportId = (params) => createDeterministicId(IMPORT_ID_NAMESPACE, `things:${params.workspaceId}:${params.modelName}:${params.sourceId}`);
|
|
2864
|
+
const createIdempotentMutationId = (params) => createDeterministicId(IDEMPOTENCY_ID_NAMESPACE, `idem:${params.workspaceId}:${params.modelName}:${params.part}:${params.key}`);
|
|
2865
|
+
//#endregion
|
|
2418
2866
|
//#region src/import/things-values.ts
|
|
2419
2867
|
var import_rrule = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
2420
2868
|
(function webpackUniversalModuleDefinition(root, factory) {
|
|
@@ -5550,21 +5998,27 @@ const pickCompletedAt = (task) => {
|
|
|
5550
5998
|
return decodeThingsUnixSeconds(task.stopDate) ?? decodeThingsUnixSeconds(task.userModificationDate) ?? decodeThingsUnixSeconds(task.creationDate);
|
|
5551
5999
|
};
|
|
5552
6000
|
const sortValue = (primary, fallback) => typeof primary === "number" ? primary : fallback;
|
|
5553
|
-
const
|
|
5554
|
-
|
|
6001
|
+
const textDecoder = new TextDecoder();
|
|
6002
|
+
const encodeBase64 = (value) => {
|
|
6003
|
+
let binary = "";
|
|
6004
|
+
for (const byte of value) binary += String.fromCodePoint(byte);
|
|
6005
|
+
return btoa(binary);
|
|
6006
|
+
};
|
|
6007
|
+
const sanitizeBinary = (value) => {
|
|
6008
|
+
const asText = textDecoder.decode(value);
|
|
5555
6009
|
if (!asText.includes("�") && READABLE_TEXT_PATTERN.test(asText)) {
|
|
5556
6010
|
const trimmed = asText.trim();
|
|
5557
6011
|
if (trimmed.length > 0) return trimmed;
|
|
5558
6012
|
}
|
|
5559
6013
|
return {
|
|
5560
|
-
data:
|
|
6014
|
+
data: encodeBase64(value),
|
|
5561
6015
|
encoding: "base64"
|
|
5562
6016
|
};
|
|
5563
6017
|
};
|
|
5564
6018
|
const sanitizeValue = (value) => {
|
|
5565
6019
|
if (value === void 0 || value === null) return;
|
|
5566
6020
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
5567
|
-
if (
|
|
6021
|
+
if (value instanceof Uint8Array) return sanitizeBinary(value);
|
|
5568
6022
|
if (Array.isArray(value)) {
|
|
5569
6023
|
const items = value.map((entry) => sanitizeValue(entry)).filter((entry) => entry !== void 0);
|
|
5570
6024
|
return items.length > 0 ? items : void 0;
|
|
@@ -7395,8 +7849,8 @@ const filterTasks = (tasks, options) => {
|
|
|
7395
7849
|
if (query.length === 0) return viewFiltered;
|
|
7396
7850
|
return viewFiltered.filter((task) => {
|
|
7397
7851
|
const title = task.title.toLowerCase();
|
|
7398
|
-
const
|
|
7399
|
-
return title.includes(query) ||
|
|
7852
|
+
const description = task.description?.toLowerCase() ?? "";
|
|
7853
|
+
return title.includes(query) || description.includes(query);
|
|
7400
7854
|
});
|
|
7401
7855
|
};
|
|
7402
7856
|
const loadTasksForScope = (context, scope, apiOptions) => {
|
|
@@ -7477,7 +7931,7 @@ const runTaskShow = async (context, taskRef, options, workspaceOverride) => {
|
|
|
7477
7931
|
if (task.projectId) writeInfo(context, `projectId: ${task.projectId}`);
|
|
7478
7932
|
if (task.teamId) writeInfo(context, `teamId: ${task.teamId}`);
|
|
7479
7933
|
if (task.assigneeId) writeInfo(context, `assigneeId: ${task.assigneeId}`);
|
|
7480
|
-
if (task.description) writeInfo(context, `
|
|
7934
|
+
if (task.description) writeInfo(context, `description: ${task.description}`);
|
|
7481
7935
|
};
|
|
7482
7936
|
const resolveTitle = async (context, rawTitle) => {
|
|
7483
7937
|
if (rawTitle) {
|
|
@@ -7488,20 +7942,28 @@ const resolveTitle = async (context, rawTitle) => {
|
|
|
7488
7942
|
if (prompted) return prompted;
|
|
7489
7943
|
throw new CliError("Task title is required.", EXIT_CODES.VALIDATION, "Provide a title as an argument: donebear task add \"My task\"");
|
|
7490
7944
|
};
|
|
7491
|
-
const
|
|
7492
|
-
if (typeof
|
|
7493
|
-
|
|
7494
|
-
|
|
7945
|
+
const resolveDescription = async (context, flagDescription) => {
|
|
7946
|
+
if (typeof flagDescription !== "string") return null;
|
|
7947
|
+
if (flagDescription === "-") {
|
|
7948
|
+
const piped = await readStdin();
|
|
7949
|
+
if (!piped) {
|
|
7950
|
+
writeWarning(context, "No description received on stdin (--description -); task created without a description.");
|
|
7951
|
+
return null;
|
|
7952
|
+
}
|
|
7953
|
+
return piped;
|
|
7495
7954
|
}
|
|
7496
|
-
const
|
|
7497
|
-
if (
|
|
7498
|
-
|
|
7955
|
+
const trimmed = flagDescription.trim();
|
|
7956
|
+
if (trimmed.length === 0) {
|
|
7957
|
+
writeWarning(context, "Empty --description value; task created without a description.");
|
|
7958
|
+
return null;
|
|
7959
|
+
}
|
|
7960
|
+
return trimmed;
|
|
7499
7961
|
};
|
|
7500
7962
|
const runTaskAdd = async (context, title, options, workspaceOverride) => {
|
|
7501
7963
|
const commandContext = await resolveCommandContext(context);
|
|
7502
7964
|
const userId = await requireUserId(context, commandContext);
|
|
7503
7965
|
const normalizedTitle = await resolveTitle(context, title);
|
|
7504
|
-
const
|
|
7966
|
+
const description = await resolveDescription(context, options.description);
|
|
7505
7967
|
const { workspace, autoSelected } = await resolveWorkspace(context, {
|
|
7506
7968
|
apiBaseUrl: commandContext.apiBaseUrl,
|
|
7507
7969
|
storedWorkspaceId: commandContext.localContext.workspaceId,
|
|
@@ -7526,37 +7988,35 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
|
|
|
7526
7988
|
const taskId = idempotencyId("model") ?? randomUUID();
|
|
7527
7989
|
const now = Date.now();
|
|
7528
7990
|
const view = parseTaskView(options.when);
|
|
7529
|
-
const startFields = buildTaskStartFields(view);
|
|
7530
7991
|
const deadlineEpoch = parseDeadline(options.deadline);
|
|
7992
|
+
const clientId = options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId;
|
|
7531
7993
|
await mutateModel(context, {
|
|
7532
7994
|
baseUrl: commandContext.apiBaseUrl,
|
|
7533
|
-
request: {
|
|
7995
|
+
request: buildMutateEnvelope({
|
|
7534
7996
|
batchId: idempotencyId("batch") ?? randomUUID(),
|
|
7997
|
+
clientId,
|
|
7998
|
+
clientTxIds: [idempotencyId("tx") ?? randomUUID()],
|
|
7535
7999
|
transactions: [{
|
|
7536
8000
|
action: "INSERT",
|
|
7537
|
-
clientId: options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId,
|
|
7538
|
-
clientTxId: idempotencyId("tx") ?? randomUUID(),
|
|
7539
8001
|
modelId: taskId,
|
|
7540
8002
|
modelName: "Task",
|
|
7541
|
-
payload: {
|
|
7542
|
-
archivedAt: null,
|
|
7543
|
-
completedAt: null,
|
|
7544
|
-
createdAt: now,
|
|
7545
|
-
creatorId: userId,
|
|
8003
|
+
payload: buildTaskInsertFields({
|
|
7546
8004
|
deadlineAt: deadlineEpoch,
|
|
7547
|
-
description
|
|
7548
|
-
|
|
8005
|
+
description,
|
|
8006
|
+
ids: {
|
|
8007
|
+
creatorId: userId,
|
|
8008
|
+
taskId,
|
|
8009
|
+
workspaceId: workspace.id
|
|
8010
|
+
},
|
|
8011
|
+
now,
|
|
7549
8012
|
projectId,
|
|
7550
|
-
sortOrder: 0,
|
|
7551
8013
|
teamId,
|
|
7552
8014
|
title: normalizedTitle,
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
...startFields
|
|
7557
|
-
}
|
|
8015
|
+
todayEpoch: getTodayDateOnlyEpoch(new Date(now)),
|
|
8016
|
+
view
|
|
8017
|
+
})
|
|
7558
8018
|
}]
|
|
7559
|
-
},
|
|
8019
|
+
}),
|
|
7560
8020
|
token: commandContext.token
|
|
7561
8021
|
});
|
|
7562
8022
|
if (context.json) {
|
|
@@ -7564,6 +8024,7 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
|
|
|
7564
8024
|
autoSelected,
|
|
7565
8025
|
created: true,
|
|
7566
8026
|
deadlineAt: deadlineEpoch,
|
|
8027
|
+
description,
|
|
7567
8028
|
id: taskId,
|
|
7568
8029
|
projectId,
|
|
7569
8030
|
teamId,
|
|
@@ -7577,6 +8038,7 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
|
|
|
7577
8038
|
if (autoSelected) writeInfo(context, `Auto-selected workspace: ${workspace.name}`);
|
|
7578
8039
|
writeSuccess(context, `Added task: ${normalizedTitle}`);
|
|
7579
8040
|
writeInfo(context, `id: ${taskId}`);
|
|
8041
|
+
if (description) writeInfo(context, `description: ${description}`);
|
|
7580
8042
|
};
|
|
7581
8043
|
const runTaskMutation = async (context, taskRef, payload) => {
|
|
7582
8044
|
const commandContext = await resolveCommandContext(context);
|
|
@@ -7644,7 +8106,7 @@ const resolveTeamId = async (context, commandContext, teamRef, workspaceRef) =>
|
|
|
7644
8106
|
}), teamRef, "Team");
|
|
7645
8107
|
};
|
|
7646
8108
|
const validateTaskEditOptions = (options) => {
|
|
7647
|
-
if (options.
|
|
8109
|
+
if (options.clearDescription && typeof options.description === "string") throw new Error("Use either --description or --clear-description, not both.");
|
|
7648
8110
|
if (options.clearDeadline && typeof options.deadline === "string") throw new Error("Use either --deadline or --clear-deadline, not both.");
|
|
7649
8111
|
if (options.clearProject && typeof options.project === "string") throw new Error("Use either --project or --clear-project, not both.");
|
|
7650
8112
|
if (options.clearTeam && typeof options.team === "string") throw new Error("Use either --team or --clear-team, not both.");
|
|
@@ -7655,10 +8117,10 @@ const applyScalarEdits = (updates, options) => {
|
|
|
7655
8117
|
if (title.length === 0) throw new Error("Task title cannot be empty.");
|
|
7656
8118
|
updates.title = title;
|
|
7657
8119
|
}
|
|
7658
|
-
if (options.
|
|
7659
|
-
else if (typeof options.
|
|
7660
|
-
const
|
|
7661
|
-
updates.description =
|
|
8120
|
+
if (options.clearDescription) updates.description = null;
|
|
8121
|
+
else if (typeof options.description === "string") {
|
|
8122
|
+
const description = options.description.trim();
|
|
8123
|
+
updates.description = description.length > 0 ? description : null;
|
|
7662
8124
|
}
|
|
7663
8125
|
if (typeof options.when === "string") Object.assign(updates, buildTaskStartFields(parseTaskView(options.when)));
|
|
7664
8126
|
if (options.clearDeadline) updates.deadlineAt = null;
|
|
@@ -7674,7 +8136,7 @@ const runTaskEdit = async (context, taskRef, options, workspaceOverride) => {
|
|
|
7674
8136
|
else if (typeof options.project === "string") updates.projectId = await resolveProjectId(context, commandContext, options.project, wsRef);
|
|
7675
8137
|
if (options.clearTeam) updates.teamId = null;
|
|
7676
8138
|
else if (typeof options.team === "string") updates.teamId = await resolveTeamId(context, commandContext, options.team, wsRef);
|
|
7677
|
-
if (Object.keys(updates).length === 0) throw new Error("No updates provided. Use --title, --
|
|
8139
|
+
if (Object.keys(updates).length === 0) throw new Error("No updates provided. Use --title, --description, --clear-description, --when, --deadline, --clear-deadline, --project, --clear-project, --team, or --clear-team.");
|
|
7678
8140
|
updates.updatedAt = Date.now();
|
|
7679
8141
|
await runTaskMutation(context, taskRef, {
|
|
7680
8142
|
action: "UPDATE",
|
|
@@ -7781,7 +8243,7 @@ const runTaskRandom = async (context, options, workspaceOverride) => {
|
|
|
7781
8243
|
writeInfo(context, `start: ${task.start}`);
|
|
7782
8244
|
writeInfo(context, `startBucket: ${task.startBucket}`);
|
|
7783
8245
|
writeInfo(context, `deadline: ${formatDateOnlyEpoch(task.deadlineAt) ?? "(none)"}`);
|
|
7784
|
-
if (task.description) writeInfo(context, `
|
|
8246
|
+
if (task.description) writeInfo(context, `description: ${task.description}`);
|
|
7785
8247
|
};
|
|
7786
8248
|
const resolveChecklistItemReference = async (context, commandContext, taskId, itemRef) => {
|
|
7787
8249
|
const trimmed = itemRef.trim();
|
|
@@ -7852,7 +8314,7 @@ Agent tip: run "donebear spec task add" for full option reference
|
|
|
7852
8314
|
}, workspaceOverride);
|
|
7853
8315
|
});
|
|
7854
8316
|
});
|
|
7855
|
-
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
|
|
8317
|
+
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", `
|
|
7856
8318
|
Examples:
|
|
7857
8319
|
donebear task list --json
|
|
7858
8320
|
donebear task list --state done --json
|
|
@@ -7873,7 +8335,7 @@ Views: inbox | anytime | today | upcoming | someday | logbook | trash
|
|
|
7873
8335
|
await runTaskShow(context, taskId, options, workspaceOverride);
|
|
7874
8336
|
});
|
|
7875
8337
|
});
|
|
7876
|
-
task.command("read").description("Print the description
|
|
8338
|
+
task.command("read").description("Print the description of a task").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").action(async (taskId, options, command) => {
|
|
7877
8339
|
const context = contextFromCommand(command);
|
|
7878
8340
|
await runWithErrorHandling(context, async () => {
|
|
7879
8341
|
await runTaskRead(context, taskId, options, workspaceOverride);
|
|
@@ -7897,12 +8359,12 @@ Views: inbox | anytime | today | upcoming | someday | logbook | trash
|
|
|
7897
8359
|
await runTaskRandom(context, options, workspaceOverride);
|
|
7898
8360
|
});
|
|
7899
8361
|
});
|
|
7900
|
-
task.command("add").alias("create").description("Create a task").argument("[title]", "Task title (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-
|
|
8362
|
+
task.command("add").alias("create").description("Create a task").argument("[title]", "Task title (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-d, --description <text>", "Task description (use --description - to read from piped stdin)").option("--when <view>", "inbox|anytime|today|upcoming|someday", "inbox").option("--deadline <date>", "Deadline (YYYY-MM-DD or ISO datetime)").option("--project <key-or-id>", "Project key or id").option("--team <key-or-id>", "Team key or id").option("--idempotency-key <key>", "Stable key making retries safe (repeated calls create only one task)").addHelpText("after", `
|
|
7901
8363
|
Examples:
|
|
7902
8364
|
donebear task add "Fix login bug" --when today --json
|
|
7903
8365
|
donebear task add "Write tests" --deadline 2030-03-15 --project PROJ --json
|
|
7904
|
-
donebear task add "Review PR" --when anytime --
|
|
7905
|
-
echo "Multi-line
|
|
8366
|
+
donebear task add "Review PR" --when anytime --description "Check for XSS" --json
|
|
8367
|
+
echo "Multi-line description" | donebear task add "My task" --description -
|
|
7906
8368
|
donebear task add # prompts for title interactively
|
|
7907
8369
|
|
|
7908
8370
|
Date format: YYYY-MM-DD (e.g. 2030-03-15)
|
|
@@ -7913,7 +8375,7 @@ View (--when): inbox | anytime | today | upcoming | someday
|
|
|
7913
8375
|
await runTaskAdd(context, title, options, workspaceOverride);
|
|
7914
8376
|
});
|
|
7915
8377
|
});
|
|
7916
|
-
task.command("edit").description("Update task fields").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--title <title>", "Replace task title").option("--
|
|
8378
|
+
task.command("edit").description("Update task fields").argument("<id>", "Task id or unique prefix").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("--title <title>", "Replace task title").option("--description <text>", "Replace task description (empty string clears)").option("--clear-description", "Clear task description").option("--when <view>", "Set view bucket (inbox|anytime|today|upcoming|someday)").option("--deadline <date>", "Set deadline (YYYY-MM-DD or ISO datetime)").option("--clear-deadline", "Clear deadline").option("--project <key-or-id>", "Set project (key or id)").option("--clear-project", "Remove project assignment").option("--team <key-or-id>", "Set team (key or id)").option("--clear-team", "Remove team assignment").action(async (taskId, options, command) => {
|
|
7917
8379
|
const context = contextFromCommand(command);
|
|
7918
8380
|
await runWithErrorHandling(context, async () => {
|
|
7919
8381
|
await runTaskEdit(context, taskId, options, workspaceOverride);
|
|
@@ -7988,6 +8450,13 @@ View (--when): inbox | anytime | today | upcoming | someday
|
|
|
7988
8450
|
});
|
|
7989
8451
|
const itemId = randomUUID();
|
|
7990
8452
|
const now = Date.now();
|
|
8453
|
+
const checklistItems = await loadChecklistItemsByTask(context, {
|
|
8454
|
+
baseUrl: commandContext.apiBaseUrl,
|
|
8455
|
+
taskId: resolvedTask.id,
|
|
8456
|
+
token: commandContext.token
|
|
8457
|
+
});
|
|
8458
|
+
const maxSortOrder = Math.max(...checklistItems.map((item) => item.sortOrder));
|
|
8459
|
+
const sortOrder = checklistItems.length === 0 ? DEFAULT_RANK_GAP : maxSortOrder + DEFAULT_RANK_GAP;
|
|
7991
8460
|
await mutateModel(context, {
|
|
7992
8461
|
baseUrl: commandContext.apiBaseUrl,
|
|
7993
8462
|
request: {
|
|
@@ -8001,7 +8470,7 @@ View (--when): inbox | anytime | today | upcoming | someday
|
|
|
8001
8470
|
payload: {
|
|
8002
8471
|
completedAt: null,
|
|
8003
8472
|
createdAt: now,
|
|
8004
|
-
sortOrder
|
|
8473
|
+
sortOrder,
|
|
8005
8474
|
taskId: resolvedTask.id,
|
|
8006
8475
|
title: title.trim(),
|
|
8007
8476
|
updatedAt: now,
|
|
@@ -8225,12 +8694,12 @@ const runSearch = async (context, query, options, workspaceOverride) => {
|
|
|
8225
8694
|
writeInfo(context, `No tasks found matching "${trimmedQuery}".`);
|
|
8226
8695
|
return;
|
|
8227
8696
|
}
|
|
8228
|
-
writeInfo(context, `${scopeLabel(scope)}
|
|
8697
|
+
writeInfo(context, `${scopeLabel(scope)}: "${trimmedQuery}" (${limited.length}/${filtered.length})`);
|
|
8229
8698
|
for (const line of taskListLines(limited)) writeInfo(context, line);
|
|
8230
8699
|
if (context.copy) copyToClipboard(taskListLines(limited).join("\n"));
|
|
8231
8700
|
};
|
|
8232
8701
|
const registerSearchCommand = (program, workspaceOverride) => {
|
|
8233
|
-
program.command("search").description("Search tasks by title or
|
|
8702
|
+
program.command("search").description("Search tasks by title or description").argument("[query]", "Search query (prompted interactively if omitted)").option("-w, --workspace <workspace>", "Workspace id, slug, or exact name").option("-s, --state <state>", "open|done|archived|all", "all").option("-n, --limit <count>", "Maximum number of results", (value) => parsePositiveInteger(value, "Limit"), DEFAULT_SEARCH_LIMIT).addHelpText("after", `
|
|
8234
8703
|
Examples:
|
|
8235
8704
|
donebear search "meeting"
|
|
8236
8705
|
donebear search "launch" --state open
|
|
@@ -8777,13 +9246,14 @@ const COMMAND_SPECS = {
|
|
|
8777
9246
|
examples: [
|
|
8778
9247
|
"donebear task add \"Fix login bug\" --when today --json",
|
|
8779
9248
|
"donebear task add \"Write tests\" --deadline 2026-03-15 --project PROJ --json",
|
|
8780
|
-
"donebear task add \"Review PR\" --when anytime --
|
|
8781
|
-
"echo \"Multi-line
|
|
9249
|
+
"donebear task add \"Review PR\" --when anytime --description \"Triage for bugs\" --json",
|
|
9250
|
+
"echo \"Multi-line description\" | donebear task add \"My task\" --description -",
|
|
8782
9251
|
"donebear task add # prompts for title interactively in TTY"
|
|
8783
9252
|
],
|
|
8784
9253
|
json_output: {
|
|
8785
9254
|
created: "boolean",
|
|
8786
9255
|
deadlineAt: "number | null",
|
|
9256
|
+
description: "string | null",
|
|
8787
9257
|
id: "string (UUID)",
|
|
8788
9258
|
projectId: "string | null",
|
|
8789
9259
|
teamId: "string | null",
|
|
@@ -8792,19 +9262,19 @@ const COMMAND_SPECS = {
|
|
|
8792
9262
|
workspaceId: "string",
|
|
8793
9263
|
workspaceName: "string"
|
|
8794
9264
|
},
|
|
8795
|
-
notes: "Title is prompted interactively when omitted in TTY.
|
|
9265
|
+
notes: "Title is prompted interactively when omitted in TTY. The description is NOT read from stdin unless you pass --description - explicitly; without --description the task is created with no description (and the saved description is echoed back in the output). Task id accepts full UUID or prefix of >= 4 chars in all other task subcommands.",
|
|
8796
9266
|
options: {
|
|
8797
9267
|
"--deadline": {
|
|
8798
9268
|
description: "Due date",
|
|
8799
9269
|
format: "YYYY-MM-DD",
|
|
8800
9270
|
type: "string"
|
|
8801
9271
|
},
|
|
8802
|
-
"--
|
|
8803
|
-
description: "
|
|
9272
|
+
"--description": {
|
|
9273
|
+
description: "Body / description text. Pass --description - to read from piped stdin.",
|
|
8804
9274
|
type: "string"
|
|
8805
9275
|
},
|
|
8806
|
-
"--
|
|
8807
|
-
description: "
|
|
9276
|
+
"--idempotency-key": {
|
|
9277
|
+
description: "Stable key making retries safe (repeated calls create only one task)",
|
|
8808
9278
|
type: "string"
|
|
8809
9279
|
},
|
|
8810
9280
|
"--project": {
|
|
@@ -8847,7 +9317,7 @@ const COMMAND_SPECS = {
|
|
|
8847
9317
|
}
|
|
8848
9318
|
},
|
|
8849
9319
|
command: "task append",
|
|
8850
|
-
description: "Append text to task
|
|
9320
|
+
description: "Append text to a task's description",
|
|
8851
9321
|
examples: ["donebear task append abc1 \"Additional context\" --json"],
|
|
8852
9322
|
json_output: {
|
|
8853
9323
|
id: "string",
|
|
@@ -8908,8 +9378,8 @@ const COMMAND_SPECS = {
|
|
|
8908
9378
|
description: "Clear the due date",
|
|
8909
9379
|
type: "boolean"
|
|
8910
9380
|
},
|
|
8911
|
-
"--clear-
|
|
8912
|
-
description: "Clear
|
|
9381
|
+
"--clear-description": {
|
|
9382
|
+
description: "Clear the description",
|
|
8913
9383
|
type: "boolean"
|
|
8914
9384
|
},
|
|
8915
9385
|
"--clear-project": {
|
|
@@ -8925,8 +9395,8 @@ const COMMAND_SPECS = {
|
|
|
8925
9395
|
format: "YYYY-MM-DD",
|
|
8926
9396
|
type: "string"
|
|
8927
9397
|
},
|
|
8928
|
-
"--
|
|
8929
|
-
description: "Replace
|
|
9398
|
+
"--description": {
|
|
9399
|
+
description: "Replace the task description",
|
|
8930
9400
|
type: "string"
|
|
8931
9401
|
},
|
|
8932
9402
|
"--project": {
|
|
@@ -9024,7 +9494,7 @@ const COMMAND_SPECS = {
|
|
|
9024
9494
|
}
|
|
9025
9495
|
},
|
|
9026
9496
|
command: "task prepend",
|
|
9027
|
-
description: "Prepend text to task
|
|
9497
|
+
description: "Prepend text to a task's description",
|
|
9028
9498
|
examples: ["donebear task prepend abc1 \"URGENT: \" --json"],
|
|
9029
9499
|
json_output: {
|
|
9030
9500
|
id: "string",
|
|
@@ -9040,7 +9510,7 @@ const COMMAND_SPECS = {
|
|
|
9040
9510
|
type: "string"
|
|
9041
9511
|
} },
|
|
9042
9512
|
command: "task read",
|
|
9043
|
-
description: "Print task description
|
|
9513
|
+
description: "Print task description as plain text",
|
|
9044
9514
|
examples: ["donebear task read abc1"],
|
|
9045
9515
|
options: { "--workspace": { type: "string" } },
|
|
9046
9516
|
usage: "donebear task read <id> [options]"
|
|
@@ -9166,7 +9636,7 @@ const TOP_LEVEL_SPECS = {
|
|
|
9166
9636
|
list: "List all API keys with scopes and expiration",
|
|
9167
9637
|
revoke: "Revoke an API key by id"
|
|
9168
9638
|
},
|
|
9169
|
-
tip: "donebear spec api-key create
|
|
9639
|
+
tip: "donebear spec api-key create: get full spec for api-key create"
|
|
9170
9640
|
},
|
|
9171
9641
|
auth: {
|
|
9172
9642
|
command: "auth",
|
|
@@ -9177,13 +9647,13 @@ const TOP_LEVEL_SPECS = {
|
|
|
9177
9647
|
logout: "Sign out and remove stored session",
|
|
9178
9648
|
status: "Show auth status"
|
|
9179
9649
|
},
|
|
9180
|
-
tip: "donebear spec auth login
|
|
9650
|
+
tip: "donebear spec auth login: get full spec for auth login"
|
|
9181
9651
|
},
|
|
9182
9652
|
import: {
|
|
9183
9653
|
command: "import",
|
|
9184
9654
|
description: "Import external data into Done Bear",
|
|
9185
9655
|
subcommands: { things: "Import a Things 3 SQLite export into the selected workspace" },
|
|
9186
|
-
tip: "donebear spec import things
|
|
9656
|
+
tip: "donebear spec import things: get full spec for the Things importer"
|
|
9187
9657
|
},
|
|
9188
9658
|
label: {
|
|
9189
9659
|
command: "label",
|
|
@@ -9208,7 +9678,7 @@ const TOP_LEVEL_SPECS = {
|
|
|
9208
9678
|
show: "Get project details by id, key, or name",
|
|
9209
9679
|
unarchive: "Restore an archived project"
|
|
9210
9680
|
},
|
|
9211
|
-
tip: "donebear spec project add
|
|
9681
|
+
tip: "donebear spec project add: get full spec for project add"
|
|
9212
9682
|
},
|
|
9213
9683
|
task: {
|
|
9214
9684
|
command: "task",
|
|
@@ -9216,20 +9686,20 @@ const TOP_LEVEL_SPECS = {
|
|
|
9216
9686
|
notes: "Task id accepts full UUID or a prefix of >= 4 chars.",
|
|
9217
9687
|
subcommands: {
|
|
9218
9688
|
add: "Create a new task",
|
|
9219
|
-
append: "Append text to task
|
|
9689
|
+
append: "Append text to a task's description",
|
|
9220
9690
|
archive: "Archive a task",
|
|
9221
9691
|
checklist: "Manage task checklist items",
|
|
9222
9692
|
done: "Mark a task as complete",
|
|
9223
9693
|
edit: "Update task fields",
|
|
9224
9694
|
list: "List tasks (--state, --view, --search, --limit)",
|
|
9225
|
-
prepend: "Prepend text to task
|
|
9695
|
+
prepend: "Prepend text to a task's description",
|
|
9226
9696
|
random: "Pick a random task (--state to filter)",
|
|
9227
|
-
read: "Print task
|
|
9697
|
+
read: "Print task description as plain text",
|
|
9228
9698
|
reopen: "Reopen a completed task",
|
|
9229
9699
|
show: "Get task details by id",
|
|
9230
9700
|
unarchive: "Restore an archived task"
|
|
9231
9701
|
},
|
|
9232
|
-
tip: "donebear spec task add
|
|
9702
|
+
tip: "donebear spec task add: get full spec for task add"
|
|
9233
9703
|
},
|
|
9234
9704
|
team: {
|
|
9235
9705
|
command: "team",
|
|
@@ -9253,7 +9723,7 @@ const TOP_LEVEL_SPECS = {
|
|
|
9253
9723
|
members: "List workspace members",
|
|
9254
9724
|
use: "Set the default workspace"
|
|
9255
9725
|
},
|
|
9256
|
-
tip: "donebear spec workspace use
|
|
9726
|
+
tip: "donebear spec workspace use: get full spec for workspace use"
|
|
9257
9727
|
}
|
|
9258
9728
|
};
|
|
9259
9729
|
const runSpec = (_context, args) => {
|
|
@@ -9287,7 +9757,7 @@ const runSpec = (_context, args) => {
|
|
|
9287
9757
|
});
|
|
9288
9758
|
};
|
|
9289
9759
|
const registerSpecCommand = (program) => {
|
|
9290
|
-
program.command("spec").description("Progressive CLI spec for agent discovery
|
|
9760
|
+
program.command("spec").description("Progressive CLI spec for agent discovery (no auth required)").argument("[command]", "Command name (e.g. task, project)").argument("[subcommand]", "Subcommand name (e.g. add, list)").addHelpText("after", `
|
|
9291
9761
|
Examples:
|
|
9292
9762
|
donebear spec Full CLI overview (~500 tokens)
|
|
9293
9763
|
donebear spec task All task subcommands
|
|
@@ -9444,7 +9914,7 @@ const runToday = async (context, options, workspaceOverride) => {
|
|
|
9444
9914
|
writeInfo(context, "No tasks for today.");
|
|
9445
9915
|
return;
|
|
9446
9916
|
}
|
|
9447
|
-
writeInfo(context, `${scopeLabel(scope)}
|
|
9917
|
+
writeInfo(context, `${scopeLabel(scope)}: Today (${limited.length}/${todayTasks.length})`);
|
|
9448
9918
|
for (const line of taskListLines(limited)) writeInfo(context, line);
|
|
9449
9919
|
if (context.copy) copyToClipboard(taskListLines(limited).join("\n"));
|
|
9450
9920
|
};
|
|
@@ -9869,7 +10339,7 @@ With auth:
|
|
|
9869
10339
|
|
|
9870
10340
|
Auth:
|
|
9871
10341
|
donebear auth login OAuth browser login
|
|
9872
|
-
DONEBEAR_TOKEN=<token> Non-interactive / CI
|
|
10342
|
+
DONEBEAR_TOKEN=<token> Non-interactive / CI: set this env var
|
|
9873
10343
|
|
|
9874
10344
|
Workspace targeting:
|
|
9875
10345
|
donebear workspace=myorg task list
|