@khangal.j/fireside-cli 0.0.3 → 0.0.5
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/README.md +4 -2
- package/dist/index.js +431 -39
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ fireside tasks list
|
|
|
22
22
|
fireside tasks list --project personal
|
|
23
23
|
fireside tasks get <task-id>
|
|
24
24
|
fireside tasks get <task-id> --context
|
|
25
|
+
fireside tasks get <task-id> --handoff
|
|
25
26
|
fireside tasks create --project personal --board main --column maybe --title "Ship CLI"
|
|
26
27
|
fireside tasks update <task-id> --title "Ship task CLI"
|
|
27
28
|
fireside tasks delete <task-id>
|
|
@@ -31,13 +32,14 @@ fireside tasks handoff create <task-id> --to @alice --summary "Ready for review"
|
|
|
31
32
|
## Task Commands
|
|
32
33
|
|
|
33
34
|
- `fireside tasks list [--project <project>] [--board <board>] [--column <column>] [--json]`
|
|
34
|
-
- `fireside tasks get <task-id> [--project <project>] [--context] [--json]`
|
|
35
|
+
- `fireside tasks get <task-id> [--project <project>] [--context] [--handoff] [--json]`
|
|
35
36
|
- `fireside tasks create --project <project> --column <column> --title <title> [--board <board>] [--description <text> | --description-file <path>] [--due-date YYYY-MM-DD] [--assignee <member>]... [--json]`
|
|
36
37
|
- `fireside tasks update <task-id> [--project <project>] [--title <title>] [--description <text> | --description-file <path> | --clear-description] [--due-date YYYY-MM-DD | --clear-due-date] [--assignee <member>]... [--clear-assignees] [--json]`
|
|
37
38
|
- `fireside tasks delete <task-id> [--project <project>] [--json]`
|
|
38
39
|
- `fireside tasks handoff create <task-id> --to <member> --summary <summary> [--project <project>] [--next <member>] [--context <markdown> | --context-file <path>] [--json]`
|
|
39
40
|
|
|
40
|
-
Member selectors accept
|
|
41
|
+
Member selectors accept `@username`, email, user id, or `me`.
|
|
42
|
+
CLI text output prefers usernames so the assignable handle stays visible.
|
|
41
43
|
For handoffs, `--to` must be another project member.
|
|
42
44
|
|
|
43
45
|
By default, the CLI talks to:
|
package/dist/index.js
CHANGED
|
@@ -154,6 +154,19 @@ async function createTask(baseUrl, accessToken, projectId, boardId, input) {
|
|
|
154
154
|
}
|
|
155
155
|
);
|
|
156
156
|
}
|
|
157
|
+
async function createTasks(baseUrl, accessToken, projectId, boardId, inputs) {
|
|
158
|
+
return requestJson(
|
|
159
|
+
`${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards/${encodeURIComponent(boardId)}/tasks/bulk`,
|
|
160
|
+
{
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: {
|
|
163
|
+
...getAuthHeaders(accessToken),
|
|
164
|
+
"content-type": "application/json"
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify({ tasks: inputs })
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
}
|
|
157
170
|
async function updateTask(baseUrl, accessToken, projectId, boardId, taskId, input) {
|
|
158
171
|
return requestJson(
|
|
159
172
|
`${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards/${encodeURIComponent(boardId)}/tasks/${encodeURIComponent(taskId)}`,
|
|
@@ -167,6 +180,19 @@ async function updateTask(baseUrl, accessToken, projectId, boardId, taskId, inpu
|
|
|
167
180
|
}
|
|
168
181
|
);
|
|
169
182
|
}
|
|
183
|
+
async function moveTask(baseUrl, accessToken, projectId, boardId, taskId, input) {
|
|
184
|
+
return requestJson(
|
|
185
|
+
`${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards/${encodeURIComponent(boardId)}/tasks/${encodeURIComponent(taskId)}`,
|
|
186
|
+
{
|
|
187
|
+
method: "PATCH",
|
|
188
|
+
headers: {
|
|
189
|
+
...getAuthHeaders(accessToken),
|
|
190
|
+
"content-type": "application/json"
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify(input)
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
}
|
|
170
196
|
async function deleteTask(baseUrl, accessToken, projectId, boardId, taskId) {
|
|
171
197
|
return requestJson(
|
|
172
198
|
`${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards/${encodeURIComponent(boardId)}/tasks/${encodeURIComponent(taskId)}`,
|
|
@@ -197,6 +223,14 @@ async function createTaskHandoff(baseUrl, accessToken, projectId, boardId, taskI
|
|
|
197
223
|
}
|
|
198
224
|
);
|
|
199
225
|
}
|
|
226
|
+
async function listTaskHandoffs(baseUrl, accessToken, projectId, boardId, taskId) {
|
|
227
|
+
return requestJson(
|
|
228
|
+
`${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards/${encodeURIComponent(boardId)}/tasks/${encodeURIComponent(taskId)}/handoffs`,
|
|
229
|
+
{
|
|
230
|
+
headers: getAuthHeaders(accessToken)
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
}
|
|
200
234
|
async function listAssignedTasks(baseUrl, accessToken) {
|
|
201
235
|
return requestJson(`${baseUrl}/api/my-stuff`, {
|
|
202
236
|
headers: getAuthHeaders(accessToken)
|
|
@@ -475,6 +509,9 @@ function formatCode(code) {
|
|
|
475
509
|
function formatUsername(username) {
|
|
476
510
|
return username ? import_picocolors.default.cyan(`@${username}`) : import_picocolors.default.dim("Not set");
|
|
477
511
|
}
|
|
512
|
+
function formatUserHandle(user) {
|
|
513
|
+
return user.username ? import_picocolors.default.cyan(`@${user.username}`) : import_picocolors.default.cyan(user.email);
|
|
514
|
+
}
|
|
478
515
|
function formatHeading(title, id) {
|
|
479
516
|
if (!id) {
|
|
480
517
|
return import_picocolors.default.bold(title);
|
|
@@ -511,12 +548,6 @@ function formatProjectMarker(color) {
|
|
|
511
548
|
const formatter = colorFormatters[color] || ((value) => value);
|
|
512
549
|
return formatter("o");
|
|
513
550
|
}
|
|
514
|
-
function formatNames(names) {
|
|
515
|
-
if (!names.length) {
|
|
516
|
-
return import_picocolors.default.dim("None");
|
|
517
|
-
}
|
|
518
|
-
return names.map((name) => import_picocolors.default.cyan(name)).join(import_picocolors.default.dim(", "));
|
|
519
|
-
}
|
|
520
551
|
function formatUserCodeForDisplay(userCode) {
|
|
521
552
|
return userCode.match(/.{1,4}/g)?.join("-") || userCode;
|
|
522
553
|
}
|
|
@@ -608,7 +639,7 @@ function uniqueStrings(values) {
|
|
|
608
639
|
return [...new Set(values)];
|
|
609
640
|
}
|
|
610
641
|
function formatMember(member) {
|
|
611
|
-
return
|
|
642
|
+
return formatUserHandle(member);
|
|
612
643
|
}
|
|
613
644
|
function formatMemberList(members) {
|
|
614
645
|
if (!members.length) {
|
|
@@ -634,7 +665,7 @@ function flattenTaskEntries(projectBoardsResults) {
|
|
|
634
665
|
)
|
|
635
666
|
).sort(compareTaskEntries);
|
|
636
667
|
}
|
|
637
|
-
function serializeTaskEntry(taskEntry, contextRevision) {
|
|
668
|
+
function serializeTaskEntry(taskEntry, contextRevision, taskHandoff) {
|
|
638
669
|
return {
|
|
639
670
|
board: {
|
|
640
671
|
id: taskEntry.board.id,
|
|
@@ -646,6 +677,7 @@ function serializeTaskEntry(taskEntry, contextRevision) {
|
|
|
646
677
|
title: taskEntry.column.title
|
|
647
678
|
},
|
|
648
679
|
...contextRevision !== void 0 ? { contextRevision } : {},
|
|
680
|
+
...taskHandoff !== void 0 ? { handoff: taskHandoff } : {},
|
|
649
681
|
project: {
|
|
650
682
|
color: taskEntry.project.color,
|
|
651
683
|
id: taskEntry.project.id,
|
|
@@ -690,7 +722,7 @@ function printTaskSummary(taskEntry) {
|
|
|
690
722
|
}
|
|
691
723
|
console.log("");
|
|
692
724
|
}
|
|
693
|
-
function printTaskDetails(taskEntry, contextRevision) {
|
|
725
|
+
function printTaskDetails(taskEntry, contextRevision, taskHandoff) {
|
|
694
726
|
console.log(formatHeading(taskEntry.task.title, taskEntry.task.id));
|
|
695
727
|
printKeyValue(" Project", import_picocolors.default.cyan(taskEntry.project.title));
|
|
696
728
|
printKeyValue(" Board", taskEntry.board.title);
|
|
@@ -700,10 +732,7 @@ function printTaskDetails(taskEntry, contextRevision) {
|
|
|
700
732
|
taskEntry.task.dueDate ? import_picocolors.default.yellow(formatDueDate(taskEntry.task.dueDate)) : import_picocolors.default.dim("None")
|
|
701
733
|
);
|
|
702
734
|
printKeyValue(" Assignees", formatMemberList(taskEntry.task.assignees));
|
|
703
|
-
printKeyValue(
|
|
704
|
-
" Description",
|
|
705
|
-
taskEntry.task.description || import_picocolors.default.dim("None")
|
|
706
|
-
);
|
|
735
|
+
printKeyValue(" Description", taskEntry.task.description || import_picocolors.default.dim("None"));
|
|
707
736
|
if (contextRevision !== void 0) {
|
|
708
737
|
printKeyValue(
|
|
709
738
|
" Task context",
|
|
@@ -714,6 +743,26 @@ function printTaskDetails(taskEntry, contextRevision) {
|
|
|
714
743
|
console.log(contextRevision.contentMarkdown);
|
|
715
744
|
}
|
|
716
745
|
}
|
|
746
|
+
if (taskHandoff !== void 0) {
|
|
747
|
+
printKeyValue(
|
|
748
|
+
" Active handoff",
|
|
749
|
+
taskHandoff ? import_picocolors.default.cyan(taskHandoff.id) : import_picocolors.default.dim("None")
|
|
750
|
+
);
|
|
751
|
+
if (taskHandoff) {
|
|
752
|
+
printKeyValue(" Handoff status", import_picocolors.default.cyan(taskHandoff.status));
|
|
753
|
+
printKeyValue(" Handoff from", formatMember(taskHandoff.fromUser));
|
|
754
|
+
printKeyValue(" Handoff to", formatMember(taskHandoff.toUser));
|
|
755
|
+
printKeyValue(
|
|
756
|
+
" Handoff next assignee",
|
|
757
|
+
formatMember(taskHandoff.nextAssigneeUser)
|
|
758
|
+
);
|
|
759
|
+
printKeyValue(" Handoff summary", taskHandoff.summary);
|
|
760
|
+
if (taskHandoff.contextMarkdown) {
|
|
761
|
+
console.log("");
|
|
762
|
+
console.log(taskHandoff.contextMarkdown);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
717
766
|
}
|
|
718
767
|
function printTaskHandoff(taskEntry, taskHandoff) {
|
|
719
768
|
console.log(formatHeading(taskEntry.task.title, taskEntry.task.id));
|
|
@@ -724,10 +773,7 @@ function printTaskHandoff(taskEntry, taskHandoff) {
|
|
|
724
773
|
printKeyValue(" Status", import_picocolors.default.cyan(taskHandoff.status));
|
|
725
774
|
printKeyValue(" From", formatMember(taskHandoff.fromUser));
|
|
726
775
|
printKeyValue(" To", formatMember(taskHandoff.toUser));
|
|
727
|
-
printKeyValue(
|
|
728
|
-
" Next assignee",
|
|
729
|
-
formatMember(taskHandoff.nextAssigneeUser)
|
|
730
|
-
);
|
|
776
|
+
printKeyValue(" Next assignee", formatMember(taskHandoff.nextAssigneeUser));
|
|
731
777
|
console.log(` ${taskHandoff.summary}`);
|
|
732
778
|
}
|
|
733
779
|
async function readTextFile(filePath, label) {
|
|
@@ -749,6 +795,58 @@ async function resolveTextInput(value, filePath, label) {
|
|
|
749
795
|
}
|
|
750
796
|
return value;
|
|
751
797
|
}
|
|
798
|
+
function coerceAssignees(value, label) {
|
|
799
|
+
if (value === void 0 || value === null) {
|
|
800
|
+
return void 0;
|
|
801
|
+
}
|
|
802
|
+
if (typeof value === "string") {
|
|
803
|
+
return value.trim() ? [value.trim()] : [];
|
|
804
|
+
}
|
|
805
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
|
|
806
|
+
return value;
|
|
807
|
+
}
|
|
808
|
+
throw new Error(
|
|
809
|
+
`${label}: \`assignees\` must be a string or array of strings.`
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
function normalizeBulkTaskRow(value, label) {
|
|
813
|
+
if (typeof value === "string") {
|
|
814
|
+
return { title: value.trim() };
|
|
815
|
+
}
|
|
816
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
817
|
+
return { title: "" };
|
|
818
|
+
}
|
|
819
|
+
const row = value;
|
|
820
|
+
const optionalString = (key) => typeof row[key] === "string" ? row[key] : void 0;
|
|
821
|
+
return {
|
|
822
|
+
title: typeof row.title === "string" ? row.title.trim() : "",
|
|
823
|
+
description: optionalString("description"),
|
|
824
|
+
column: optionalString("column"),
|
|
825
|
+
dueDate: optionalString("dueDate") ?? optionalString("due-date"),
|
|
826
|
+
assignees: coerceAssignees(row.assignees, label)
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function parseBulkTaskRows(content) {
|
|
830
|
+
const trimmed = content.trim();
|
|
831
|
+
if (!trimmed) {
|
|
832
|
+
return [];
|
|
833
|
+
}
|
|
834
|
+
let parsedJson;
|
|
835
|
+
try {
|
|
836
|
+
parsedJson = JSON.parse(trimmed);
|
|
837
|
+
} catch {
|
|
838
|
+
return trimmed.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((line) => ({ title: line }));
|
|
839
|
+
}
|
|
840
|
+
const rows = Array.isArray(parsedJson) ? parsedJson : parsedJson && typeof parsedJson === "object" && "tasks" in parsedJson ? parsedJson.tasks : void 0;
|
|
841
|
+
if (!Array.isArray(rows)) {
|
|
842
|
+
throw new Error(
|
|
843
|
+
'Tasks file must be a JSON array, a `{ "tasks": [...] }` object, or one title per line.'
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
return rows.map(
|
|
847
|
+
(row, index) => normalizeBulkTaskRow(row, `Task ${index + 1}`)
|
|
848
|
+
);
|
|
849
|
+
}
|
|
752
850
|
async function loadCliContext(baseUrl) {
|
|
753
851
|
const state = await requireAuthState();
|
|
754
852
|
const configState = await loadConfigState();
|
|
@@ -783,6 +881,37 @@ async function resolveTaskEntry(baseUrl, accessToken, taskId, projectSelector) {
|
|
|
783
881
|
}
|
|
784
882
|
return taskEntries[0];
|
|
785
883
|
}
|
|
884
|
+
function parseTargetPosition(value) {
|
|
885
|
+
if (value === void 0) {
|
|
886
|
+
return 0;
|
|
887
|
+
}
|
|
888
|
+
const position = Number(value);
|
|
889
|
+
if (!Number.isInteger(position) || position < 0) {
|
|
890
|
+
throw new Error("`--position` must be a non-negative integer.");
|
|
891
|
+
}
|
|
892
|
+
return position;
|
|
893
|
+
}
|
|
894
|
+
async function runTaskMove(baseUrl, accessToken, taskEntry, targetColumn, targetPosition, json) {
|
|
895
|
+
const movedTask = await moveTask(
|
|
896
|
+
baseUrl,
|
|
897
|
+
accessToken,
|
|
898
|
+
taskEntry.project.id,
|
|
899
|
+
taskEntry.board.id,
|
|
900
|
+
taskEntry.task.id,
|
|
901
|
+
{ targetColumnId: targetColumn.id, targetPosition }
|
|
902
|
+
);
|
|
903
|
+
const movedTaskEntry = {
|
|
904
|
+
...taskEntry,
|
|
905
|
+
column: targetColumn,
|
|
906
|
+
task: movedTask
|
|
907
|
+
};
|
|
908
|
+
if (json) {
|
|
909
|
+
console.log(JSON.stringify(serializeTaskEntry(movedTaskEntry), null, 2));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
printSuccess(`Moved task to ${targetColumn.title}.`);
|
|
913
|
+
printTaskSummary(movedTaskEntry);
|
|
914
|
+
}
|
|
786
915
|
function resolveProjectMember(members, selector, currentUserId) {
|
|
787
916
|
const trimmedSelector = selector.trim();
|
|
788
917
|
if (!trimmedSelector.length) {
|
|
@@ -878,10 +1007,7 @@ function printAssignedTasks(tasks, projectFilter) {
|
|
|
878
1007
|
if (task.dueDate) {
|
|
879
1008
|
printKeyValue(" Due", import_picocolors.default.yellow(formatDueDate(task.dueDate)));
|
|
880
1009
|
}
|
|
881
|
-
printKeyValue(
|
|
882
|
-
" Assignees",
|
|
883
|
-
formatNames(task.assignees.map((assignee) => assignee.name))
|
|
884
|
-
);
|
|
1010
|
+
printKeyValue(" Assignees", formatMemberList(task.assignees));
|
|
885
1011
|
if (task.description) {
|
|
886
1012
|
console.log(` ${task.description}`);
|
|
887
1013
|
}
|
|
@@ -922,10 +1048,9 @@ addBaseUrlOption(
|
|
|
922
1048
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
923
1049
|
});
|
|
924
1050
|
const user = await getCurrentUser(baseUrl, accessToken);
|
|
925
|
-
printSuccess(`Signed in as ${import_picocolors.default.bold(user
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
}
|
|
1051
|
+
printSuccess(`Signed in as ${import_picocolors.default.bold(formatUserHandle(user))}.`);
|
|
1052
|
+
printKeyValue("Email", import_picocolors.default.cyan(user.email));
|
|
1053
|
+
printKeyValue("Username", formatUsername(user.username));
|
|
929
1054
|
})
|
|
930
1055
|
);
|
|
931
1056
|
program.command("logout").description("Remove the local CLI session").action(async () => {
|
|
@@ -944,9 +1069,8 @@ addBaseUrlOption(
|
|
|
944
1069
|
const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
|
|
945
1070
|
const user = await getCurrentUser(baseUrl, state.accessToken);
|
|
946
1071
|
printKeyValue("Base URL", formatUrl(baseUrl));
|
|
947
|
-
printSuccess(
|
|
948
|
-
|
|
949
|
-
);
|
|
1072
|
+
printSuccess(`Signed in as ${import_picocolors.default.bold(formatUserHandle(user))}.`);
|
|
1073
|
+
printKeyValue("Email", import_picocolors.default.cyan(user.email));
|
|
950
1074
|
printKeyValue("Username", formatUsername(user.username));
|
|
951
1075
|
})
|
|
952
1076
|
);
|
|
@@ -1029,7 +1153,7 @@ addBaseUrlOption(
|
|
|
1029
1153
|
)
|
|
1030
1154
|
);
|
|
1031
1155
|
addBaseUrlOption(
|
|
1032
|
-
tasksCommand.command("get <taskId>").description("Show a task by id").option("--json", "Print the task as JSON").option("-p, --project <project>", "Limit lookup to a project id or title").option("--context", "Load the latest saved task context").action(
|
|
1156
|
+
tasksCommand.command("get <taskId>").description("Show a task by id").option("--json", "Print the task as JSON").option("-p, --project <project>", "Limit lookup to a project id or title").option("--context", "Load the latest saved task context").option("--handoff", "Load the active handoff context").action(
|
|
1033
1157
|
async (taskId, options) => {
|
|
1034
1158
|
const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
|
|
1035
1159
|
const taskEntry = await resolveTaskEntry(
|
|
@@ -1045,24 +1169,31 @@ addBaseUrlOption(
|
|
|
1045
1169
|
taskEntry.board.id,
|
|
1046
1170
|
taskId
|
|
1047
1171
|
) : void 0;
|
|
1172
|
+
const taskHandoff = options.handoff ? (await listTaskHandoffs(
|
|
1173
|
+
baseUrl,
|
|
1174
|
+
accessToken,
|
|
1175
|
+
taskEntry.project.id,
|
|
1176
|
+
taskEntry.board.id,
|
|
1177
|
+
taskId
|
|
1178
|
+
)).find((handoff) => handoff.status === "active") || null : void 0;
|
|
1048
1179
|
if (options.json) {
|
|
1049
1180
|
console.log(
|
|
1050
1181
|
JSON.stringify(
|
|
1051
|
-
serializeTaskEntry(taskEntry, contextRevision),
|
|
1182
|
+
serializeTaskEntry(taskEntry, contextRevision, taskHandoff),
|
|
1052
1183
|
null,
|
|
1053
1184
|
2
|
|
1054
1185
|
)
|
|
1055
1186
|
);
|
|
1056
1187
|
return;
|
|
1057
1188
|
}
|
|
1058
|
-
printTaskDetails(taskEntry, contextRevision);
|
|
1189
|
+
printTaskDetails(taskEntry, contextRevision, taskHandoff);
|
|
1059
1190
|
}
|
|
1060
1191
|
)
|
|
1061
1192
|
);
|
|
1062
1193
|
addBaseUrlOption(
|
|
1063
1194
|
tasksCommand.command("create").description("Create a task").requiredOption("-p, --project <project>", "Project id or title").requiredOption("-t, --title <title>", "Task title").requiredOption("-c, --column <column>", "Column id or title").option("--board <board>", "Board id or title").option("--description <description>", "Task description").option("--description-file <path>", "Read task description from a file").option("--due-date <date>", "Due date in YYYY-MM-DD format").option(
|
|
1064
1195
|
"-a, --assignee <member>",
|
|
1065
|
-
"Assign a member by
|
|
1196
|
+
"Assign a member by @username, email, id, or me",
|
|
1066
1197
|
collectOptionValue,
|
|
1067
1198
|
[]
|
|
1068
1199
|
).option("--json", "Print the created task as JSON").action(
|
|
@@ -1073,7 +1204,10 @@ addBaseUrlOption(
|
|
|
1073
1204
|
accessToken,
|
|
1074
1205
|
options.project
|
|
1075
1206
|
);
|
|
1076
|
-
const board = resolveBoard(
|
|
1207
|
+
const board = resolveBoard(
|
|
1208
|
+
projectBoardsResult.boardsData.boards,
|
|
1209
|
+
options.board
|
|
1210
|
+
);
|
|
1077
1211
|
const column = resolveColumn(board.columns, options.column);
|
|
1078
1212
|
const description = await resolveTextInput(
|
|
1079
1213
|
options.description,
|
|
@@ -1114,10 +1248,202 @@ addBaseUrlOption(
|
|
|
1114
1248
|
}
|
|
1115
1249
|
)
|
|
1116
1250
|
);
|
|
1251
|
+
addBaseUrlOption(
|
|
1252
|
+
tasksCommand.command("bulk-create").description("Create many tasks at once from a file").requiredOption("-p, --project <project>", "Project id or title").requiredOption(
|
|
1253
|
+
"--file <path>",
|
|
1254
|
+
"Tasks file: JSON array, `{ tasks: [...] }`, or one title per line"
|
|
1255
|
+
).option("--board <board>", "Board id or title").option(
|
|
1256
|
+
"-c, --column <column>",
|
|
1257
|
+
"Default column id or title for rows without their own"
|
|
1258
|
+
).option(
|
|
1259
|
+
"--due-date <date>",
|
|
1260
|
+
"Default due date (YYYY-MM-DD) for rows without their own"
|
|
1261
|
+
).option(
|
|
1262
|
+
"-a, --assignee <member>",
|
|
1263
|
+
"Default assignee (@username, email, id, or me) for rows without their own",
|
|
1264
|
+
collectOptionValue,
|
|
1265
|
+
[]
|
|
1266
|
+
).option(
|
|
1267
|
+
"--continue-on-error",
|
|
1268
|
+
"Best-effort: create row by row and keep going on failures (non-atomic)"
|
|
1269
|
+
).option("--dry-run", "Resolve and preview tasks without creating them").option("--json", "Print results as JSON").action(
|
|
1270
|
+
async (options) => {
|
|
1271
|
+
const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
|
|
1272
|
+
const rows = parseBulkTaskRows(
|
|
1273
|
+
await readTextFile(options.file, "tasks")
|
|
1274
|
+
);
|
|
1275
|
+
if (!rows.length) {
|
|
1276
|
+
throw new Error("No tasks found in the file.");
|
|
1277
|
+
}
|
|
1278
|
+
const [projectBoardsResult] = await loadProjectBoardsResults(
|
|
1279
|
+
baseUrl,
|
|
1280
|
+
accessToken,
|
|
1281
|
+
options.project
|
|
1282
|
+
);
|
|
1283
|
+
const board = resolveBoard(
|
|
1284
|
+
projectBoardsResult.boardsData.boards,
|
|
1285
|
+
options.board
|
|
1286
|
+
);
|
|
1287
|
+
const members = projectBoardsResult.boardsData.members;
|
|
1288
|
+
const resolved = [];
|
|
1289
|
+
const failed = [];
|
|
1290
|
+
rows.forEach((row, index) => {
|
|
1291
|
+
const label = row.title || `Task ${index + 1}`;
|
|
1292
|
+
try {
|
|
1293
|
+
if (!row.title) {
|
|
1294
|
+
throw new Error("Task title is required.");
|
|
1295
|
+
}
|
|
1296
|
+
const column = resolveColumn(
|
|
1297
|
+
board.columns,
|
|
1298
|
+
row.column ?? options.column
|
|
1299
|
+
);
|
|
1300
|
+
const assigneeIds = resolveProjectMemberIds(
|
|
1301
|
+
members,
|
|
1302
|
+
row.assignees ?? options.assignee,
|
|
1303
|
+
user.id
|
|
1304
|
+
);
|
|
1305
|
+
resolved.push({
|
|
1306
|
+
column,
|
|
1307
|
+
input: {
|
|
1308
|
+
assigneeIds,
|
|
1309
|
+
boardColumnId: column.id,
|
|
1310
|
+
description: row.description ?? "",
|
|
1311
|
+
dueDate: row.dueDate ?? options.dueDate ?? "",
|
|
1312
|
+
title: row.title
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
failed.push({
|
|
1317
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1318
|
+
title: label
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
const toTaskEntry = (createdTask) => ({
|
|
1323
|
+
board,
|
|
1324
|
+
column: board.columns.find(
|
|
1325
|
+
(column) => column.id === createdTask.boardColumnId
|
|
1326
|
+
) ?? board.columns[0],
|
|
1327
|
+
members,
|
|
1328
|
+
project: projectBoardsResult.project,
|
|
1329
|
+
task: createdTask
|
|
1330
|
+
});
|
|
1331
|
+
if (options.dryRun) {
|
|
1332
|
+
if (options.json) {
|
|
1333
|
+
console.log(
|
|
1334
|
+
JSON.stringify(
|
|
1335
|
+
{
|
|
1336
|
+
failed,
|
|
1337
|
+
plan: resolved.map(({ column, input }) => ({
|
|
1338
|
+
boardColumnId: input.boardColumnId,
|
|
1339
|
+
column: column.title,
|
|
1340
|
+
dueDate: input.dueDate || null,
|
|
1341
|
+
title: input.title
|
|
1342
|
+
}))
|
|
1343
|
+
},
|
|
1344
|
+
null,
|
|
1345
|
+
2
|
|
1346
|
+
)
|
|
1347
|
+
);
|
|
1348
|
+
} else {
|
|
1349
|
+
printInfo(
|
|
1350
|
+
`Dry run: ${resolved.length} task(s) ready for ${projectBoardsResult.project.title} / ${board.title}.`
|
|
1351
|
+
);
|
|
1352
|
+
for (const { column, input } of resolved) {
|
|
1353
|
+
console.log(
|
|
1354
|
+
` ${import_picocolors.default.green("+")} ${input.title} ${import_picocolors.default.dim(`(${column.title})`)}`
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
for (const failure of failed) {
|
|
1358
|
+
console.log(
|
|
1359
|
+
` ${import_picocolors.default.red("x")} ${failure.title} ${import_picocolors.default.dim(`\u2014 ${failure.error}`)}`
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (failed.length) {
|
|
1364
|
+
process.exitCode = 1;
|
|
1365
|
+
}
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
const created = [];
|
|
1369
|
+
if (options.continueOnError) {
|
|
1370
|
+
for (const { input } of resolved) {
|
|
1371
|
+
try {
|
|
1372
|
+
created.push(
|
|
1373
|
+
await createTask(
|
|
1374
|
+
baseUrl,
|
|
1375
|
+
accessToken,
|
|
1376
|
+
projectBoardsResult.project.id,
|
|
1377
|
+
board.id,
|
|
1378
|
+
input
|
|
1379
|
+
)
|
|
1380
|
+
);
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
failed.push({
|
|
1383
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1384
|
+
title: input.title
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
} else {
|
|
1389
|
+
if (failed.length) {
|
|
1390
|
+
throw new Error(
|
|
1391
|
+
`${failed.length} task(s) could not be resolved. Fix them or pass --continue-on-error.
|
|
1392
|
+
` + failed.map((failure) => ` - ${failure.title}: ${failure.error}`).join("\n")
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
created.push(
|
|
1396
|
+
...await createTasks(
|
|
1397
|
+
baseUrl,
|
|
1398
|
+
accessToken,
|
|
1399
|
+
projectBoardsResult.project.id,
|
|
1400
|
+
board.id,
|
|
1401
|
+
resolved.map(({ input }) => input)
|
|
1402
|
+
)
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
const createdEntries = created.map(toTaskEntry);
|
|
1406
|
+
if (options.json) {
|
|
1407
|
+
console.log(
|
|
1408
|
+
JSON.stringify(
|
|
1409
|
+
{
|
|
1410
|
+
created: createdEntries.map(
|
|
1411
|
+
(entry) => serializeTaskEntry(entry)
|
|
1412
|
+
),
|
|
1413
|
+
failed
|
|
1414
|
+
},
|
|
1415
|
+
null,
|
|
1416
|
+
2
|
|
1417
|
+
)
|
|
1418
|
+
);
|
|
1419
|
+
} else {
|
|
1420
|
+
printSuccess(
|
|
1421
|
+
`Created ${created.length} task(s) in ${projectBoardsResult.project.title} / ${board.title}.`
|
|
1422
|
+
);
|
|
1423
|
+
for (const entry of createdEntries) {
|
|
1424
|
+
console.log(
|
|
1425
|
+
` ${import_picocolors.default.green("+")} ${entry.task.title} ${import_picocolors.default.dim(`(${entry.column.title})`)}`
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
if (failed.length) {
|
|
1429
|
+
printWarning(`${failed.length} task(s) failed:`);
|
|
1430
|
+
for (const failure of failed) {
|
|
1431
|
+
console.log(
|
|
1432
|
+
` ${import_picocolors.default.red("x")} ${failure.title} ${import_picocolors.default.dim(`\u2014 ${failure.error}`)}`
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (failed.length) {
|
|
1438
|
+
process.exitCode = 1;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
)
|
|
1442
|
+
);
|
|
1117
1443
|
addBaseUrlOption(
|
|
1118
1444
|
tasksCommand.command("update <taskId>").description("Update a task").option("-p, --project <project>", "Limit lookup to a project id or title").option("-t, --title <title>", "New task title").option("--description <description>", "New task description").option("--description-file <path>", "Read task description from a file").option("--clear-description", "Clear the task description").option("--due-date <date>", "Set the due date in YYYY-MM-DD format").option("--clear-due-date", "Clear the due date").option(
|
|
1119
1445
|
"-a, --assignee <member>",
|
|
1120
|
-
"Replace assignees using
|
|
1446
|
+
"Replace assignees using @username, email, id, or me",
|
|
1121
1447
|
collectOptionValue,
|
|
1122
1448
|
[]
|
|
1123
1449
|
).option("--clear-assignees", "Remove all assignees").option("--json", "Print the updated task as JSON").action(
|
|
@@ -1152,7 +1478,11 @@ addBaseUrlOption(
|
|
|
1152
1478
|
options.descriptionFile,
|
|
1153
1479
|
"description"
|
|
1154
1480
|
);
|
|
1155
|
-
const assigneeIds = options.clearAssignees ? [] : options.assignee.length ? resolveProjectMemberIds(
|
|
1481
|
+
const assigneeIds = options.clearAssignees ? [] : options.assignee.length ? resolveProjectMemberIds(
|
|
1482
|
+
taskEntry.members,
|
|
1483
|
+
options.assignee,
|
|
1484
|
+
user.id
|
|
1485
|
+
) : taskEntry.task.assignees.map((assignee) => assignee.id);
|
|
1156
1486
|
const updatedTask = await updateTask(
|
|
1157
1487
|
baseUrl,
|
|
1158
1488
|
accessToken,
|
|
@@ -1171,7 +1501,9 @@ addBaseUrlOption(
|
|
|
1171
1501
|
task: updatedTask
|
|
1172
1502
|
};
|
|
1173
1503
|
if (options.json) {
|
|
1174
|
-
console.log(
|
|
1504
|
+
console.log(
|
|
1505
|
+
JSON.stringify(serializeTaskEntry(updatedTaskEntry), null, 2)
|
|
1506
|
+
);
|
|
1175
1507
|
return;
|
|
1176
1508
|
}
|
|
1177
1509
|
printSuccess("Updated task.");
|
|
@@ -1179,6 +1511,58 @@ addBaseUrlOption(
|
|
|
1179
1511
|
}
|
|
1180
1512
|
)
|
|
1181
1513
|
);
|
|
1514
|
+
addBaseUrlOption(
|
|
1515
|
+
tasksCommand.command("move <taskId>").description("Move a task to a different column").requiredOption("-c, --column <column>", "Target column id or title").option("-p, --project <project>", "Limit lookup to a project id or title").option("--position <position>", "Target position in the column (0 = top)").option("--json", "Print the moved task as JSON").action(
|
|
1516
|
+
async (taskId, options) => {
|
|
1517
|
+
const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
|
|
1518
|
+
const taskEntry = await resolveTaskEntry(
|
|
1519
|
+
baseUrl,
|
|
1520
|
+
accessToken,
|
|
1521
|
+
taskId,
|
|
1522
|
+
options.project
|
|
1523
|
+
);
|
|
1524
|
+
const targetColumn = resolveColumn(
|
|
1525
|
+
taskEntry.board.columns,
|
|
1526
|
+
options.column
|
|
1527
|
+
);
|
|
1528
|
+
await runTaskMove(
|
|
1529
|
+
baseUrl,
|
|
1530
|
+
accessToken,
|
|
1531
|
+
taskEntry,
|
|
1532
|
+
targetColumn,
|
|
1533
|
+
parseTargetPosition(options.position),
|
|
1534
|
+
options.json
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
)
|
|
1538
|
+
);
|
|
1539
|
+
addBaseUrlOption(
|
|
1540
|
+
tasksCommand.command("done <taskId>").description("Mark a task done (move it to the board's done column)").option("-p, --project <project>", "Limit lookup to a project id or title").option("--json", "Print the moved task as JSON").action(
|
|
1541
|
+
async (taskId, options) => {
|
|
1542
|
+
const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
|
|
1543
|
+
const taskEntry = await resolveTaskEntry(
|
|
1544
|
+
baseUrl,
|
|
1545
|
+
accessToken,
|
|
1546
|
+
taskId,
|
|
1547
|
+
options.project
|
|
1548
|
+
);
|
|
1549
|
+
const doneColumn = taskEntry.board.columns.find(
|
|
1550
|
+
(column) => column.role === "done"
|
|
1551
|
+
);
|
|
1552
|
+
if (!doneColumn) {
|
|
1553
|
+
throw new Error("This board has no done column.");
|
|
1554
|
+
}
|
|
1555
|
+
await runTaskMove(
|
|
1556
|
+
baseUrl,
|
|
1557
|
+
accessToken,
|
|
1558
|
+
taskEntry,
|
|
1559
|
+
doneColumn,
|
|
1560
|
+
0,
|
|
1561
|
+
options.json
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
)
|
|
1565
|
+
);
|
|
1182
1566
|
addBaseUrlOption(
|
|
1183
1567
|
tasksCommand.command("delete <taskId>").description("Delete a task").option("-p, --project <project>", "Limit lookup to a project id or title").option("--json", "Print the deleted task metadata as JSON").action(
|
|
1184
1568
|
async (taskId, options) => {
|
|
@@ -1228,7 +1612,7 @@ addBaseUrlOption(
|
|
|
1228
1612
|
);
|
|
1229
1613
|
var taskHandoffCommand = tasksCommand.command("handoff").description("Create and manage task handoffs");
|
|
1230
1614
|
addBaseUrlOption(
|
|
1231
|
-
taskHandoffCommand.command("create <taskId>").description("Create a handoff for a task").requiredOption("--to <member>", "Target
|
|
1615
|
+
taskHandoffCommand.command("create <taskId>").description("Create a handoff for a task").requiredOption("--to <member>", "Target @username, email, or user id").requiredOption("--summary <summary>", "Short handoff summary").option("-p, --project <project>", "Limit lookup to a project id or title").option("--next <member>", "Next assignee by @username, email, id, or me").option("--context <markdown>", "Handoff AI context markdown").option("--context-file <path>", "Read handoff AI context from a file").option("--json", "Print the created handoff as JSON").action(
|
|
1232
1616
|
async (taskId, options) => {
|
|
1233
1617
|
const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
|
|
1234
1618
|
const taskEntry = await resolveTaskEntry(
|
|
@@ -1247,7 +1631,11 @@ addBaseUrlOption(
|
|
|
1247
1631
|
"Handoff context is required. Pass `--context` or `--context-file`."
|
|
1248
1632
|
);
|
|
1249
1633
|
}
|
|
1250
|
-
const toUser = resolveProjectMember(
|
|
1634
|
+
const toUser = resolveProjectMember(
|
|
1635
|
+
taskEntry.members,
|
|
1636
|
+
options.to,
|
|
1637
|
+
user.id
|
|
1638
|
+
);
|
|
1251
1639
|
if (toUser.id === user.id) {
|
|
1252
1640
|
throw new Error("Choose another project member for `--to`.");
|
|
1253
1641
|
}
|
|
@@ -1271,7 +1659,11 @@ addBaseUrlOption(
|
|
|
1271
1659
|
);
|
|
1272
1660
|
if (options.json) {
|
|
1273
1661
|
console.log(
|
|
1274
|
-
JSON.stringify(
|
|
1662
|
+
JSON.stringify(
|
|
1663
|
+
serializeTaskHandoff(taskEntry, taskHandoff),
|
|
1664
|
+
null,
|
|
1665
|
+
2
|
|
1666
|
+
)
|
|
1275
1667
|
);
|
|
1276
1668
|
return;
|
|
1277
1669
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khangal.j/fireside-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "Fireside CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"picocolors": "^1.1.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
+
"@types/node": "^25.6.0",
|
|
33
34
|
"tsup": "^8.5.0"
|
|
34
35
|
}
|
|
35
36
|
}
|