donebear 0.4.0 → 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 +27 -17
- package/dist/cli.mjs +761 -302
- 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;
|
|
@@ -7534,37 +7988,35 @@ const runTaskAdd = async (context, title, options, workspaceOverride) => {
|
|
|
7534
7988
|
const taskId = idempotencyId("model") ?? randomUUID();
|
|
7535
7989
|
const now = Date.now();
|
|
7536
7990
|
const view = parseTaskView(options.when);
|
|
7537
|
-
const startFields = buildTaskStartFields(view);
|
|
7538
7991
|
const deadlineEpoch = parseDeadline(options.deadline);
|
|
7992
|
+
const clientId = options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId;
|
|
7539
7993
|
await mutateModel(context, {
|
|
7540
7994
|
baseUrl: commandContext.apiBaseUrl,
|
|
7541
|
-
request: {
|
|
7995
|
+
request: buildMutateEnvelope({
|
|
7542
7996
|
batchId: idempotencyId("batch") ?? randomUUID(),
|
|
7997
|
+
clientId,
|
|
7998
|
+
clientTxIds: [idempotencyId("tx") ?? randomUUID()],
|
|
7543
7999
|
transactions: [{
|
|
7544
8000
|
action: "INSERT",
|
|
7545
|
-
clientId: options.idempotencyKey ? "cli-idem" : commandContext.localContext.clientId,
|
|
7546
|
-
clientTxId: idempotencyId("tx") ?? randomUUID(),
|
|
7547
8001
|
modelId: taskId,
|
|
7548
8002
|
modelName: "Task",
|
|
7549
|
-
payload: {
|
|
7550
|
-
archivedAt: null,
|
|
7551
|
-
completedAt: null,
|
|
7552
|
-
createdAt: now,
|
|
7553
|
-
creatorId: userId,
|
|
8003
|
+
payload: buildTaskInsertFields({
|
|
7554
8004
|
deadlineAt: deadlineEpoch,
|
|
7555
8005
|
description,
|
|
7556
|
-
|
|
8006
|
+
ids: {
|
|
8007
|
+
creatorId: userId,
|
|
8008
|
+
taskId,
|
|
8009
|
+
workspaceId: workspace.id
|
|
8010
|
+
},
|
|
8011
|
+
now,
|
|
7557
8012
|
projectId,
|
|
7558
|
-
sortOrder: 0,
|
|
7559
8013
|
teamId,
|
|
7560
8014
|
title: normalizedTitle,
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
...startFields
|
|
7565
|
-
}
|
|
8015
|
+
todayEpoch: getTodayDateOnlyEpoch(new Date(now)),
|
|
8016
|
+
view
|
|
8017
|
+
})
|
|
7566
8018
|
}]
|
|
7567
|
-
},
|
|
8019
|
+
}),
|
|
7568
8020
|
token: commandContext.token
|
|
7569
8021
|
});
|
|
7570
8022
|
if (context.json) {
|
|
@@ -7998,6 +8450,13 @@ View (--when): inbox | anytime | today | upcoming | someday
|
|
|
7998
8450
|
});
|
|
7999
8451
|
const itemId = randomUUID();
|
|
8000
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;
|
|
8001
8460
|
await mutateModel(context, {
|
|
8002
8461
|
baseUrl: commandContext.apiBaseUrl,
|
|
8003
8462
|
request: {
|
|
@@ -8011,7 +8470,7 @@ View (--when): inbox | anytime | today | upcoming | someday
|
|
|
8011
8470
|
payload: {
|
|
8012
8471
|
completedAt: null,
|
|
8013
8472
|
createdAt: now,
|
|
8014
|
-
sortOrder
|
|
8473
|
+
sortOrder,
|
|
8015
8474
|
taskId: resolvedTask.id,
|
|
8016
8475
|
title: title.trim(),
|
|
8017
8476
|
updatedAt: now,
|
|
@@ -8235,7 +8694,7 @@ const runSearch = async (context, query, options, workspaceOverride) => {
|
|
|
8235
8694
|
writeInfo(context, `No tasks found matching "${trimmedQuery}".`);
|
|
8236
8695
|
return;
|
|
8237
8696
|
}
|
|
8238
|
-
writeInfo(context, `${scopeLabel(scope)}
|
|
8697
|
+
writeInfo(context, `${scopeLabel(scope)}: "${trimmedQuery}" (${limited.length}/${filtered.length})`);
|
|
8239
8698
|
for (const line of taskListLines(limited)) writeInfo(context, line);
|
|
8240
8699
|
if (context.copy) copyToClipboard(taskListLines(limited).join("\n"));
|
|
8241
8700
|
};
|
|
@@ -9177,7 +9636,7 @@ const TOP_LEVEL_SPECS = {
|
|
|
9177
9636
|
list: "List all API keys with scopes and expiration",
|
|
9178
9637
|
revoke: "Revoke an API key by id"
|
|
9179
9638
|
},
|
|
9180
|
-
tip: "donebear spec api-key create
|
|
9639
|
+
tip: "donebear spec api-key create: get full spec for api-key create"
|
|
9181
9640
|
},
|
|
9182
9641
|
auth: {
|
|
9183
9642
|
command: "auth",
|
|
@@ -9188,13 +9647,13 @@ const TOP_LEVEL_SPECS = {
|
|
|
9188
9647
|
logout: "Sign out and remove stored session",
|
|
9189
9648
|
status: "Show auth status"
|
|
9190
9649
|
},
|
|
9191
|
-
tip: "donebear spec auth login
|
|
9650
|
+
tip: "donebear spec auth login: get full spec for auth login"
|
|
9192
9651
|
},
|
|
9193
9652
|
import: {
|
|
9194
9653
|
command: "import",
|
|
9195
9654
|
description: "Import external data into Done Bear",
|
|
9196
9655
|
subcommands: { things: "Import a Things 3 SQLite export into the selected workspace" },
|
|
9197
|
-
tip: "donebear spec import things
|
|
9656
|
+
tip: "donebear spec import things: get full spec for the Things importer"
|
|
9198
9657
|
},
|
|
9199
9658
|
label: {
|
|
9200
9659
|
command: "label",
|
|
@@ -9219,7 +9678,7 @@ const TOP_LEVEL_SPECS = {
|
|
|
9219
9678
|
show: "Get project details by id, key, or name",
|
|
9220
9679
|
unarchive: "Restore an archived project"
|
|
9221
9680
|
},
|
|
9222
|
-
tip: "donebear spec project add
|
|
9681
|
+
tip: "donebear spec project add: get full spec for project add"
|
|
9223
9682
|
},
|
|
9224
9683
|
task: {
|
|
9225
9684
|
command: "task",
|
|
@@ -9240,7 +9699,7 @@ const TOP_LEVEL_SPECS = {
|
|
|
9240
9699
|
show: "Get task details by id",
|
|
9241
9700
|
unarchive: "Restore an archived task"
|
|
9242
9701
|
},
|
|
9243
|
-
tip: "donebear spec task add
|
|
9702
|
+
tip: "donebear spec task add: get full spec for task add"
|
|
9244
9703
|
},
|
|
9245
9704
|
team: {
|
|
9246
9705
|
command: "team",
|
|
@@ -9264,7 +9723,7 @@ const TOP_LEVEL_SPECS = {
|
|
|
9264
9723
|
members: "List workspace members",
|
|
9265
9724
|
use: "Set the default workspace"
|
|
9266
9725
|
},
|
|
9267
|
-
tip: "donebear spec workspace use
|
|
9726
|
+
tip: "donebear spec workspace use: get full spec for workspace use"
|
|
9268
9727
|
}
|
|
9269
9728
|
};
|
|
9270
9729
|
const runSpec = (_context, args) => {
|
|
@@ -9298,7 +9757,7 @@ const runSpec = (_context, args) => {
|
|
|
9298
9757
|
});
|
|
9299
9758
|
};
|
|
9300
9759
|
const registerSpecCommand = (program) => {
|
|
9301
|
-
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", `
|
|
9302
9761
|
Examples:
|
|
9303
9762
|
donebear spec Full CLI overview (~500 tokens)
|
|
9304
9763
|
donebear spec task All task subcommands
|
|
@@ -9455,7 +9914,7 @@ const runToday = async (context, options, workspaceOverride) => {
|
|
|
9455
9914
|
writeInfo(context, "No tasks for today.");
|
|
9456
9915
|
return;
|
|
9457
9916
|
}
|
|
9458
|
-
writeInfo(context, `${scopeLabel(scope)}
|
|
9917
|
+
writeInfo(context, `${scopeLabel(scope)}: Today (${limited.length}/${todayTasks.length})`);
|
|
9459
9918
|
for (const line of taskListLines(limited)) writeInfo(context, line);
|
|
9460
9919
|
if (context.copy) copyToClipboard(taskListLines(limited).join("\n"));
|
|
9461
9920
|
};
|
|
@@ -9880,7 +10339,7 @@ With auth:
|
|
|
9880
10339
|
|
|
9881
10340
|
Auth:
|
|
9882
10341
|
donebear auth login OAuth browser login
|
|
9883
|
-
DONEBEAR_TOKEN=<token> Non-interactive / CI
|
|
10342
|
+
DONEBEAR_TOKEN=<token> Non-interactive / CI: set this env var
|
|
9884
10343
|
|
|
9885
10344
|
Workspace targeting:
|
|
9886
10345
|
donebear workspace=myorg task list
|