@khangal.j/fireside-cli 0.0.4 → 0.0.6
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 +2 -1
- package/dist/index.js +513 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,7 +38,8 @@ fireside tasks handoff create <task-id> --to @alice --summary "Ready for review"
|
|
|
38
38
|
- `fireside tasks delete <task-id> [--project <project>] [--json]`
|
|
39
39
|
- `fireside tasks handoff create <task-id> --to <member> --summary <summary> [--project <project>] [--next <member>] [--context <markdown> | --context-file <path>] [--json]`
|
|
40
40
|
|
|
41
|
-
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.
|
|
42
43
|
For handoffs, `--to` must be another project member.
|
|
43
44
|
|
|
44
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)}`,
|
|
@@ -483,6 +509,9 @@ function formatCode(code) {
|
|
|
483
509
|
function formatUsername(username) {
|
|
484
510
|
return username ? import_picocolors.default.cyan(`@${username}`) : import_picocolors.default.dim("Not set");
|
|
485
511
|
}
|
|
512
|
+
function formatUserHandle(user) {
|
|
513
|
+
return user.username ? import_picocolors.default.cyan(`@${user.username}`) : import_picocolors.default.cyan(user.email);
|
|
514
|
+
}
|
|
486
515
|
function formatHeading(title, id) {
|
|
487
516
|
if (!id) {
|
|
488
517
|
return import_picocolors.default.bold(title);
|
|
@@ -519,12 +548,6 @@ function formatProjectMarker(color) {
|
|
|
519
548
|
const formatter = colorFormatters[color] || ((value) => value);
|
|
520
549
|
return formatter("o");
|
|
521
550
|
}
|
|
522
|
-
function formatNames(names) {
|
|
523
|
-
if (!names.length) {
|
|
524
|
-
return import_picocolors.default.dim("None");
|
|
525
|
-
}
|
|
526
|
-
return names.map((name) => import_picocolors.default.cyan(name)).join(import_picocolors.default.dim(", "));
|
|
527
|
-
}
|
|
528
551
|
function formatUserCodeForDisplay(userCode) {
|
|
529
552
|
return userCode.match(/.{1,4}/g)?.join("-") || userCode;
|
|
530
553
|
}
|
|
@@ -616,7 +639,7 @@ function uniqueStrings(values) {
|
|
|
616
639
|
return [...new Set(values)];
|
|
617
640
|
}
|
|
618
641
|
function formatMember(member) {
|
|
619
|
-
return
|
|
642
|
+
return formatUserHandle(member);
|
|
620
643
|
}
|
|
621
644
|
function formatMemberList(members) {
|
|
622
645
|
if (!members.length) {
|
|
@@ -685,6 +708,71 @@ function serializeTaskHandoff(taskEntry, taskHandoff) {
|
|
|
685
708
|
}
|
|
686
709
|
};
|
|
687
710
|
}
|
|
711
|
+
function shortId(id) {
|
|
712
|
+
return id.slice(0, 8);
|
|
713
|
+
}
|
|
714
|
+
function leanAssignees(assignees) {
|
|
715
|
+
return assignees.map(
|
|
716
|
+
(member) => member.username ? `@${member.username}` : member.email
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
function serializeTaskRow(taskEntry) {
|
|
720
|
+
const assignees = leanAssignees(taskEntry.task.assignees);
|
|
721
|
+
return {
|
|
722
|
+
id: shortId(taskEntry.task.id),
|
|
723
|
+
title: taskEntry.task.title,
|
|
724
|
+
project: taskEntry.project.title,
|
|
725
|
+
board: taskEntry.board.title,
|
|
726
|
+
column: taskEntry.column.title,
|
|
727
|
+
role: taskEntry.column.role,
|
|
728
|
+
position: taskEntry.task.position,
|
|
729
|
+
...taskEntry.task.dueDate ? { dueDate: taskEntry.task.dueDate } : {},
|
|
730
|
+
...assignees.length ? { assignees } : {},
|
|
731
|
+
...taskEntry.task.description ? { hasDescription: true } : {}
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function serializeAssignedRow(task) {
|
|
735
|
+
const assignees = leanAssignees(task.assignees);
|
|
736
|
+
return {
|
|
737
|
+
id: shortId(task.id),
|
|
738
|
+
title: task.title,
|
|
739
|
+
project: task.projectTitle,
|
|
740
|
+
board: task.boardTitle,
|
|
741
|
+
column: task.columnTitle,
|
|
742
|
+
...task.dueDate ? { dueDate: task.dueDate } : {},
|
|
743
|
+
...assignees.length ? { assignees } : {},
|
|
744
|
+
...task.description ? { hasDescription: true } : {}
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function leanHandoff(handoff) {
|
|
748
|
+
const handle = (user) => user.username ? `@${user.username}` : user.email;
|
|
749
|
+
return {
|
|
750
|
+
id: handoff.id,
|
|
751
|
+
status: handoff.status,
|
|
752
|
+
summary: handoff.summary,
|
|
753
|
+
from: handle(handoff.fromUser),
|
|
754
|
+
to: handle(handoff.toUser),
|
|
755
|
+
next: handle(handoff.nextAssigneeUser),
|
|
756
|
+
contextMarkdown: handoff.contextMarkdown,
|
|
757
|
+
createdAt: handoff.createdAt
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function serializeTaskFull(taskEntry, contextRevision, taskHandoff) {
|
|
761
|
+
return {
|
|
762
|
+
id: taskEntry.task.id,
|
|
763
|
+
title: taskEntry.task.title,
|
|
764
|
+
project: taskEntry.project.title,
|
|
765
|
+
board: taskEntry.board.title,
|
|
766
|
+
column: taskEntry.column.title,
|
|
767
|
+
role: taskEntry.column.role,
|
|
768
|
+
position: taskEntry.task.position,
|
|
769
|
+
dueDate: taskEntry.task.dueDate,
|
|
770
|
+
assignees: leanAssignees(taskEntry.task.assignees),
|
|
771
|
+
description: taskEntry.task.description,
|
|
772
|
+
...contextRevision !== void 0 ? { contextRevision } : {},
|
|
773
|
+
...taskHandoff !== void 0 ? { handoff: taskHandoff ? leanHandoff(taskHandoff) : null } : {}
|
|
774
|
+
};
|
|
775
|
+
}
|
|
688
776
|
function printTaskSummary(taskEntry) {
|
|
689
777
|
console.log(formatHeading(taskEntry.task.title, taskEntry.task.id));
|
|
690
778
|
console.log(
|
|
@@ -772,6 +860,58 @@ async function resolveTextInput(value, filePath, label) {
|
|
|
772
860
|
}
|
|
773
861
|
return value;
|
|
774
862
|
}
|
|
863
|
+
function coerceAssignees(value, label) {
|
|
864
|
+
if (value === void 0 || value === null) {
|
|
865
|
+
return void 0;
|
|
866
|
+
}
|
|
867
|
+
if (typeof value === "string") {
|
|
868
|
+
return value.trim() ? [value.trim()] : [];
|
|
869
|
+
}
|
|
870
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
|
|
871
|
+
return value;
|
|
872
|
+
}
|
|
873
|
+
throw new Error(
|
|
874
|
+
`${label}: \`assignees\` must be a string or array of strings.`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
function normalizeBulkTaskRow(value, label) {
|
|
878
|
+
if (typeof value === "string") {
|
|
879
|
+
return { title: value.trim() };
|
|
880
|
+
}
|
|
881
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
882
|
+
return { title: "" };
|
|
883
|
+
}
|
|
884
|
+
const row = value;
|
|
885
|
+
const optionalString = (key) => typeof row[key] === "string" ? row[key] : void 0;
|
|
886
|
+
return {
|
|
887
|
+
title: typeof row.title === "string" ? row.title.trim() : "",
|
|
888
|
+
description: optionalString("description"),
|
|
889
|
+
column: optionalString("column"),
|
|
890
|
+
dueDate: optionalString("dueDate") ?? optionalString("due-date"),
|
|
891
|
+
assignees: coerceAssignees(row.assignees, label)
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
function parseBulkTaskRows(content) {
|
|
895
|
+
const trimmed = content.trim();
|
|
896
|
+
if (!trimmed) {
|
|
897
|
+
return [];
|
|
898
|
+
}
|
|
899
|
+
let parsedJson;
|
|
900
|
+
try {
|
|
901
|
+
parsedJson = JSON.parse(trimmed);
|
|
902
|
+
} catch {
|
|
903
|
+
return trimmed.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((line) => ({ title: line }));
|
|
904
|
+
}
|
|
905
|
+
const rows = Array.isArray(parsedJson) ? parsedJson : parsedJson && typeof parsedJson === "object" && "tasks" in parsedJson ? parsedJson.tasks : void 0;
|
|
906
|
+
if (!Array.isArray(rows)) {
|
|
907
|
+
throw new Error(
|
|
908
|
+
'Tasks file must be a JSON array, a `{ "tasks": [...] }` object, or one title per line.'
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
return rows.map(
|
|
912
|
+
(row, index) => normalizeBulkTaskRow(row, `Task ${index + 1}`)
|
|
913
|
+
);
|
|
914
|
+
}
|
|
775
915
|
async function loadCliContext(baseUrl) {
|
|
776
916
|
const state = await requireAuthState();
|
|
777
917
|
const configState = await loadConfigState();
|
|
@@ -798,13 +938,56 @@ async function loadProjectBoardsResults(baseUrl, accessToken, projectSelector) {
|
|
|
798
938
|
);
|
|
799
939
|
}
|
|
800
940
|
async function resolveTaskEntry(baseUrl, accessToken, taskId, projectSelector) {
|
|
801
|
-
const
|
|
941
|
+
const allEntries = flattenTaskEntries(
|
|
802
942
|
await loadProjectBoardsResults(baseUrl, accessToken, projectSelector)
|
|
803
|
-
)
|
|
804
|
-
|
|
943
|
+
);
|
|
944
|
+
const trimmed = taskId.trim();
|
|
945
|
+
let matches = allEntries.filter((taskEntry) => taskEntry.task.id === trimmed);
|
|
946
|
+
if (!matches.length && trimmed.length >= 4) {
|
|
947
|
+
matches = allEntries.filter(
|
|
948
|
+
(taskEntry) => taskEntry.task.id.startsWith(trimmed)
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
if (!matches.length) {
|
|
805
952
|
throw new Error(`Task ${taskId} was not found.`);
|
|
806
953
|
}
|
|
807
|
-
|
|
954
|
+
if (matches.length > 1) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
`Task id "${taskId}" is ambiguous (${matches.length} matches). Use more characters or the full id.`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
return matches[0];
|
|
960
|
+
}
|
|
961
|
+
function parseTargetPosition(value) {
|
|
962
|
+
if (value === void 0) {
|
|
963
|
+
return 0;
|
|
964
|
+
}
|
|
965
|
+
const position = Number(value);
|
|
966
|
+
if (!Number.isInteger(position) || position < 0) {
|
|
967
|
+
throw new Error("`--position` must be a non-negative integer.");
|
|
968
|
+
}
|
|
969
|
+
return position;
|
|
970
|
+
}
|
|
971
|
+
async function runTaskMove(baseUrl, accessToken, taskEntry, targetColumn, targetPosition, json) {
|
|
972
|
+
const movedTask = await moveTask(
|
|
973
|
+
baseUrl,
|
|
974
|
+
accessToken,
|
|
975
|
+
taskEntry.project.id,
|
|
976
|
+
taskEntry.board.id,
|
|
977
|
+
taskEntry.task.id,
|
|
978
|
+
{ targetColumnId: targetColumn.id, targetPosition }
|
|
979
|
+
);
|
|
980
|
+
const movedTaskEntry = {
|
|
981
|
+
...taskEntry,
|
|
982
|
+
column: targetColumn,
|
|
983
|
+
task: movedTask
|
|
984
|
+
};
|
|
985
|
+
if (json) {
|
|
986
|
+
console.log(JSON.stringify(serializeTaskEntry(movedTaskEntry), null, 2));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
printSuccess(`Moved task to ${targetColumn.title}.`);
|
|
990
|
+
printTaskSummary(movedTaskEntry);
|
|
808
991
|
}
|
|
809
992
|
function resolveProjectMember(members, selector, currentUserId) {
|
|
810
993
|
const trimmedSelector = selector.trim();
|
|
@@ -901,10 +1084,7 @@ function printAssignedTasks(tasks, projectFilter) {
|
|
|
901
1084
|
if (task.dueDate) {
|
|
902
1085
|
printKeyValue(" Due", import_picocolors.default.yellow(formatDueDate(task.dueDate)));
|
|
903
1086
|
}
|
|
904
|
-
printKeyValue(
|
|
905
|
-
" Assignees",
|
|
906
|
-
formatNames(task.assignees.map((assignee) => assignee.name))
|
|
907
|
-
);
|
|
1087
|
+
printKeyValue(" Assignees", formatMemberList(task.assignees));
|
|
908
1088
|
if (task.description) {
|
|
909
1089
|
console.log(` ${task.description}`);
|
|
910
1090
|
}
|
|
@@ -912,7 +1092,7 @@ function printAssignedTasks(tasks, projectFilter) {
|
|
|
912
1092
|
}
|
|
913
1093
|
}
|
|
914
1094
|
var program = new import_commander.Command();
|
|
915
|
-
program.name("fireside").description("Fireside CLI").version("0.0.
|
|
1095
|
+
program.name("fireside").description("Fireside CLI").version("0.0.6").configureOutput({
|
|
916
1096
|
outputError: (message, write) => write(import_picocolors.default.red(message))
|
|
917
1097
|
}).showHelpAfterError();
|
|
918
1098
|
addBaseUrlOption(
|
|
@@ -945,10 +1125,9 @@ addBaseUrlOption(
|
|
|
945
1125
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
946
1126
|
});
|
|
947
1127
|
const user = await getCurrentUser(baseUrl, accessToken);
|
|
948
|
-
printSuccess(`Signed in as ${import_picocolors.default.bold(user
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
}
|
|
1128
|
+
printSuccess(`Signed in as ${import_picocolors.default.bold(formatUserHandle(user))}.`);
|
|
1129
|
+
printKeyValue("Email", import_picocolors.default.cyan(user.email));
|
|
1130
|
+
printKeyValue("Username", formatUsername(user.username));
|
|
952
1131
|
})
|
|
953
1132
|
);
|
|
954
1133
|
program.command("logout").description("Remove the local CLI session").action(async () => {
|
|
@@ -967,9 +1146,8 @@ addBaseUrlOption(
|
|
|
967
1146
|
const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
|
|
968
1147
|
const user = await getCurrentUser(baseUrl, state.accessToken);
|
|
969
1148
|
printKeyValue("Base URL", formatUrl(baseUrl));
|
|
970
|
-
printSuccess(
|
|
971
|
-
|
|
972
|
-
);
|
|
1149
|
+
printSuccess(`Signed in as ${import_picocolors.default.bold(formatUserHandle(user))}.`);
|
|
1150
|
+
printKeyValue("Email", import_picocolors.default.cyan(user.email));
|
|
973
1151
|
printKeyValue("Username", formatUsername(user.username));
|
|
974
1152
|
})
|
|
975
1153
|
);
|
|
@@ -983,7 +1161,7 @@ addBaseUrlOption(
|
|
|
983
1161
|
})
|
|
984
1162
|
);
|
|
985
1163
|
addBaseUrlOption(
|
|
986
|
-
program.command("my-stuff").description("List tasks currently assigned to you").option("--json", "Print assigned tasks as JSON").option("-p, --project <project>", "Filter by project id or title").action(
|
|
1164
|
+
program.command("my-stuff").description("List tasks currently assigned to you").option("--json", "Print assigned tasks as compact JSON").option("--raw", "With --json, print the full unabridged shape (larger)").option("-p, --project <project>", "Filter by project id or title").action(
|
|
987
1165
|
async (options) => {
|
|
988
1166
|
const state = await requireAuthState();
|
|
989
1167
|
const configState = await loadConfigState();
|
|
@@ -994,7 +1172,11 @@ addBaseUrlOption(
|
|
|
994
1172
|
options.project
|
|
995
1173
|
);
|
|
996
1174
|
if (options.json) {
|
|
997
|
-
|
|
1175
|
+
if (options.raw) {
|
|
1176
|
+
console.log(JSON.stringify(filteredTasks, null, 2));
|
|
1177
|
+
} else {
|
|
1178
|
+
console.log(JSON.stringify(filteredTasks.map(serializeAssignedRow)));
|
|
1179
|
+
}
|
|
998
1180
|
return;
|
|
999
1181
|
}
|
|
1000
1182
|
printAssignedTasks(filteredTasks, options.project);
|
|
@@ -1015,7 +1197,7 @@ addBaseUrlOption(
|
|
|
1015
1197
|
);
|
|
1016
1198
|
var tasksCommand = program.command("tasks").description("Interact with tasks");
|
|
1017
1199
|
addBaseUrlOption(
|
|
1018
|
-
tasksCommand.command("list").description("List accessible tasks").option("--json", "Print tasks as JSON").option("-p, --project <project>", "Filter by project id or title").option("--board <board>", "Filter by board id or title").option("--column <column>", "Filter by column id or title").action(
|
|
1200
|
+
tasksCommand.command("list").description("List accessible tasks").option("--json", "Print tasks as compact JSON").option("--raw", "With --json, print the full unabridged shape (larger)").option("-p, --project <project>", "Filter by project id or title").option("--board <board>", "Filter by board id or title").option("--column <column>", "Filter by column id or title").action(
|
|
1019
1201
|
async (options) => {
|
|
1020
1202
|
const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
|
|
1021
1203
|
let taskEntries = flattenTaskEntries(
|
|
@@ -1032,13 +1214,21 @@ addBaseUrlOption(
|
|
|
1032
1214
|
);
|
|
1033
1215
|
}
|
|
1034
1216
|
if (options.json) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1217
|
+
if (options.raw) {
|
|
1218
|
+
console.log(
|
|
1219
|
+
JSON.stringify(
|
|
1220
|
+
taskEntries.map((taskEntry) => serializeTaskEntry(taskEntry)),
|
|
1221
|
+
null,
|
|
1222
|
+
2
|
|
1223
|
+
)
|
|
1224
|
+
);
|
|
1225
|
+
} else {
|
|
1226
|
+
console.log(
|
|
1227
|
+
JSON.stringify(
|
|
1228
|
+
taskEntries.map((taskEntry) => serializeTaskRow(taskEntry))
|
|
1229
|
+
)
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1042
1232
|
return;
|
|
1043
1233
|
}
|
|
1044
1234
|
if (!taskEntries.length) {
|
|
@@ -1052,7 +1242,48 @@ addBaseUrlOption(
|
|
|
1052
1242
|
)
|
|
1053
1243
|
);
|
|
1054
1244
|
addBaseUrlOption(
|
|
1055
|
-
tasksCommand.command("
|
|
1245
|
+
tasksCommand.command("columns").description("List a board's columns (id, role, title) without tasks").option("--json", "Print columns as JSON").option("-p, --project <project>", "Filter by project id or title").option("--board <board>", "Board id or title").action(
|
|
1246
|
+
async (options) => {
|
|
1247
|
+
const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
|
|
1248
|
+
const [projectBoardsResult] = await loadProjectBoardsResults(
|
|
1249
|
+
baseUrl,
|
|
1250
|
+
accessToken,
|
|
1251
|
+
options.project
|
|
1252
|
+
);
|
|
1253
|
+
if (!projectBoardsResult) {
|
|
1254
|
+
printWarning("No project found.");
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const board = resolveBoard(
|
|
1258
|
+
projectBoardsResult.boardsData.boards,
|
|
1259
|
+
options.board
|
|
1260
|
+
);
|
|
1261
|
+
const columns = [...board.columns].sort(
|
|
1262
|
+
(left, right) => left.position - right.position
|
|
1263
|
+
);
|
|
1264
|
+
if (options.json) {
|
|
1265
|
+
console.log(
|
|
1266
|
+
JSON.stringify(
|
|
1267
|
+
columns.map((column) => ({
|
|
1268
|
+
id: column.id,
|
|
1269
|
+
role: column.role,
|
|
1270
|
+
title: column.title
|
|
1271
|
+
}))
|
|
1272
|
+
)
|
|
1273
|
+
);
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
console.log(
|
|
1277
|
+
formatHeading(`${projectBoardsResult.project.title} / ${board.title}`)
|
|
1278
|
+
);
|
|
1279
|
+
for (const column of columns) {
|
|
1280
|
+
console.log(` ${import_picocolors.default.dim(column.role.padEnd(8))} ${column.title}`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
)
|
|
1284
|
+
);
|
|
1285
|
+
addBaseUrlOption(
|
|
1286
|
+
tasksCommand.command("get <taskId>").description("Show a task by id (accepts a short id prefix)").option("--json", "Print the task as JSON").option("--raw", "With --json, print the full unabridged shape (larger)").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(
|
|
1056
1287
|
async (taskId, options) => {
|
|
1057
1288
|
const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
|
|
1058
1289
|
const taskEntry = await resolveTaskEntry(
|
|
@@ -1078,7 +1309,7 @@ addBaseUrlOption(
|
|
|
1078
1309
|
if (options.json) {
|
|
1079
1310
|
console.log(
|
|
1080
1311
|
JSON.stringify(
|
|
1081
|
-
serializeTaskEntry(taskEntry, contextRevision, taskHandoff),
|
|
1312
|
+
options.raw ? serializeTaskEntry(taskEntry, contextRevision, taskHandoff) : serializeTaskFull(taskEntry, contextRevision, taskHandoff),
|
|
1082
1313
|
null,
|
|
1083
1314
|
2
|
|
1084
1315
|
)
|
|
@@ -1092,7 +1323,7 @@ addBaseUrlOption(
|
|
|
1092
1323
|
addBaseUrlOption(
|
|
1093
1324
|
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(
|
|
1094
1325
|
"-a, --assignee <member>",
|
|
1095
|
-
"Assign a member by
|
|
1326
|
+
"Assign a member by @username, email, id, or me",
|
|
1096
1327
|
collectOptionValue,
|
|
1097
1328
|
[]
|
|
1098
1329
|
).option("--json", "Print the created task as JSON").action(
|
|
@@ -1147,10 +1378,202 @@ addBaseUrlOption(
|
|
|
1147
1378
|
}
|
|
1148
1379
|
)
|
|
1149
1380
|
);
|
|
1381
|
+
addBaseUrlOption(
|
|
1382
|
+
tasksCommand.command("bulk-create").description("Create many tasks at once from a file").requiredOption("-p, --project <project>", "Project id or title").requiredOption(
|
|
1383
|
+
"--file <path>",
|
|
1384
|
+
"Tasks file: JSON array, `{ tasks: [...] }`, or one title per line"
|
|
1385
|
+
).option("--board <board>", "Board id or title").option(
|
|
1386
|
+
"-c, --column <column>",
|
|
1387
|
+
"Default column id or title for rows without their own"
|
|
1388
|
+
).option(
|
|
1389
|
+
"--due-date <date>",
|
|
1390
|
+
"Default due date (YYYY-MM-DD) for rows without their own"
|
|
1391
|
+
).option(
|
|
1392
|
+
"-a, --assignee <member>",
|
|
1393
|
+
"Default assignee (@username, email, id, or me) for rows without their own",
|
|
1394
|
+
collectOptionValue,
|
|
1395
|
+
[]
|
|
1396
|
+
).option(
|
|
1397
|
+
"--continue-on-error",
|
|
1398
|
+
"Best-effort: create row by row and keep going on failures (non-atomic)"
|
|
1399
|
+
).option("--dry-run", "Resolve and preview tasks without creating them").option("--json", "Print results as JSON").action(
|
|
1400
|
+
async (options) => {
|
|
1401
|
+
const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
|
|
1402
|
+
const rows = parseBulkTaskRows(
|
|
1403
|
+
await readTextFile(options.file, "tasks")
|
|
1404
|
+
);
|
|
1405
|
+
if (!rows.length) {
|
|
1406
|
+
throw new Error("No tasks found in the file.");
|
|
1407
|
+
}
|
|
1408
|
+
const [projectBoardsResult] = await loadProjectBoardsResults(
|
|
1409
|
+
baseUrl,
|
|
1410
|
+
accessToken,
|
|
1411
|
+
options.project
|
|
1412
|
+
);
|
|
1413
|
+
const board = resolveBoard(
|
|
1414
|
+
projectBoardsResult.boardsData.boards,
|
|
1415
|
+
options.board
|
|
1416
|
+
);
|
|
1417
|
+
const members = projectBoardsResult.boardsData.members;
|
|
1418
|
+
const resolved = [];
|
|
1419
|
+
const failed = [];
|
|
1420
|
+
rows.forEach((row, index) => {
|
|
1421
|
+
const label = row.title || `Task ${index + 1}`;
|
|
1422
|
+
try {
|
|
1423
|
+
if (!row.title) {
|
|
1424
|
+
throw new Error("Task title is required.");
|
|
1425
|
+
}
|
|
1426
|
+
const column = resolveColumn(
|
|
1427
|
+
board.columns,
|
|
1428
|
+
row.column ?? options.column
|
|
1429
|
+
);
|
|
1430
|
+
const assigneeIds = resolveProjectMemberIds(
|
|
1431
|
+
members,
|
|
1432
|
+
row.assignees ?? options.assignee,
|
|
1433
|
+
user.id
|
|
1434
|
+
);
|
|
1435
|
+
resolved.push({
|
|
1436
|
+
column,
|
|
1437
|
+
input: {
|
|
1438
|
+
assigneeIds,
|
|
1439
|
+
boardColumnId: column.id,
|
|
1440
|
+
description: row.description ?? "",
|
|
1441
|
+
dueDate: row.dueDate ?? options.dueDate ?? "",
|
|
1442
|
+
title: row.title
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
failed.push({
|
|
1447
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1448
|
+
title: label
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
const toTaskEntry = (createdTask) => ({
|
|
1453
|
+
board,
|
|
1454
|
+
column: board.columns.find(
|
|
1455
|
+
(column) => column.id === createdTask.boardColumnId
|
|
1456
|
+
) ?? board.columns[0],
|
|
1457
|
+
members,
|
|
1458
|
+
project: projectBoardsResult.project,
|
|
1459
|
+
task: createdTask
|
|
1460
|
+
});
|
|
1461
|
+
if (options.dryRun) {
|
|
1462
|
+
if (options.json) {
|
|
1463
|
+
console.log(
|
|
1464
|
+
JSON.stringify(
|
|
1465
|
+
{
|
|
1466
|
+
failed,
|
|
1467
|
+
plan: resolved.map(({ column, input }) => ({
|
|
1468
|
+
boardColumnId: input.boardColumnId,
|
|
1469
|
+
column: column.title,
|
|
1470
|
+
dueDate: input.dueDate || null,
|
|
1471
|
+
title: input.title
|
|
1472
|
+
}))
|
|
1473
|
+
},
|
|
1474
|
+
null,
|
|
1475
|
+
2
|
|
1476
|
+
)
|
|
1477
|
+
);
|
|
1478
|
+
} else {
|
|
1479
|
+
printInfo(
|
|
1480
|
+
`Dry run: ${resolved.length} task(s) ready for ${projectBoardsResult.project.title} / ${board.title}.`
|
|
1481
|
+
);
|
|
1482
|
+
for (const { column, input } of resolved) {
|
|
1483
|
+
console.log(
|
|
1484
|
+
` ${import_picocolors.default.green("+")} ${input.title} ${import_picocolors.default.dim(`(${column.title})`)}`
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
for (const failure of failed) {
|
|
1488
|
+
console.log(
|
|
1489
|
+
` ${import_picocolors.default.red("x")} ${failure.title} ${import_picocolors.default.dim(`\u2014 ${failure.error}`)}`
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (failed.length) {
|
|
1494
|
+
process.exitCode = 1;
|
|
1495
|
+
}
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
const created = [];
|
|
1499
|
+
if (options.continueOnError) {
|
|
1500
|
+
for (const { input } of resolved) {
|
|
1501
|
+
try {
|
|
1502
|
+
created.push(
|
|
1503
|
+
await createTask(
|
|
1504
|
+
baseUrl,
|
|
1505
|
+
accessToken,
|
|
1506
|
+
projectBoardsResult.project.id,
|
|
1507
|
+
board.id,
|
|
1508
|
+
input
|
|
1509
|
+
)
|
|
1510
|
+
);
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
failed.push({
|
|
1513
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1514
|
+
title: input.title
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
} else {
|
|
1519
|
+
if (failed.length) {
|
|
1520
|
+
throw new Error(
|
|
1521
|
+
`${failed.length} task(s) could not be resolved. Fix them or pass --continue-on-error.
|
|
1522
|
+
` + failed.map((failure) => ` - ${failure.title}: ${failure.error}`).join("\n")
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
created.push(
|
|
1526
|
+
...await createTasks(
|
|
1527
|
+
baseUrl,
|
|
1528
|
+
accessToken,
|
|
1529
|
+
projectBoardsResult.project.id,
|
|
1530
|
+
board.id,
|
|
1531
|
+
resolved.map(({ input }) => input)
|
|
1532
|
+
)
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
const createdEntries = created.map(toTaskEntry);
|
|
1536
|
+
if (options.json) {
|
|
1537
|
+
console.log(
|
|
1538
|
+
JSON.stringify(
|
|
1539
|
+
{
|
|
1540
|
+
created: createdEntries.map(
|
|
1541
|
+
(entry) => serializeTaskEntry(entry)
|
|
1542
|
+
),
|
|
1543
|
+
failed
|
|
1544
|
+
},
|
|
1545
|
+
null,
|
|
1546
|
+
2
|
|
1547
|
+
)
|
|
1548
|
+
);
|
|
1549
|
+
} else {
|
|
1550
|
+
printSuccess(
|
|
1551
|
+
`Created ${created.length} task(s) in ${projectBoardsResult.project.title} / ${board.title}.`
|
|
1552
|
+
);
|
|
1553
|
+
for (const entry of createdEntries) {
|
|
1554
|
+
console.log(
|
|
1555
|
+
` ${import_picocolors.default.green("+")} ${entry.task.title} ${import_picocolors.default.dim(`(${entry.column.title})`)}`
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
if (failed.length) {
|
|
1559
|
+
printWarning(`${failed.length} task(s) failed:`);
|
|
1560
|
+
for (const failure of failed) {
|
|
1561
|
+
console.log(
|
|
1562
|
+
` ${import_picocolors.default.red("x")} ${failure.title} ${import_picocolors.default.dim(`\u2014 ${failure.error}`)}`
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
if (failed.length) {
|
|
1568
|
+
process.exitCode = 1;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
)
|
|
1572
|
+
);
|
|
1150
1573
|
addBaseUrlOption(
|
|
1151
1574
|
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(
|
|
1152
1575
|
"-a, --assignee <member>",
|
|
1153
|
-
"Replace assignees using
|
|
1576
|
+
"Replace assignees using @username, email, id, or me",
|
|
1154
1577
|
collectOptionValue,
|
|
1155
1578
|
[]
|
|
1156
1579
|
).option("--clear-assignees", "Remove all assignees").option("--json", "Print the updated task as JSON").action(
|
|
@@ -1218,6 +1641,58 @@ addBaseUrlOption(
|
|
|
1218
1641
|
}
|
|
1219
1642
|
)
|
|
1220
1643
|
);
|
|
1644
|
+
addBaseUrlOption(
|
|
1645
|
+
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(
|
|
1646
|
+
async (taskId, options) => {
|
|
1647
|
+
const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
|
|
1648
|
+
const taskEntry = await resolveTaskEntry(
|
|
1649
|
+
baseUrl,
|
|
1650
|
+
accessToken,
|
|
1651
|
+
taskId,
|
|
1652
|
+
options.project
|
|
1653
|
+
);
|
|
1654
|
+
const targetColumn = resolveColumn(
|
|
1655
|
+
taskEntry.board.columns,
|
|
1656
|
+
options.column
|
|
1657
|
+
);
|
|
1658
|
+
await runTaskMove(
|
|
1659
|
+
baseUrl,
|
|
1660
|
+
accessToken,
|
|
1661
|
+
taskEntry,
|
|
1662
|
+
targetColumn,
|
|
1663
|
+
parseTargetPosition(options.position),
|
|
1664
|
+
options.json
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
)
|
|
1668
|
+
);
|
|
1669
|
+
addBaseUrlOption(
|
|
1670
|
+
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(
|
|
1671
|
+
async (taskId, options) => {
|
|
1672
|
+
const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
|
|
1673
|
+
const taskEntry = await resolveTaskEntry(
|
|
1674
|
+
baseUrl,
|
|
1675
|
+
accessToken,
|
|
1676
|
+
taskId,
|
|
1677
|
+
options.project
|
|
1678
|
+
);
|
|
1679
|
+
const doneColumn = taskEntry.board.columns.find(
|
|
1680
|
+
(column) => column.role === "done"
|
|
1681
|
+
);
|
|
1682
|
+
if (!doneColumn) {
|
|
1683
|
+
throw new Error("This board has no done column.");
|
|
1684
|
+
}
|
|
1685
|
+
await runTaskMove(
|
|
1686
|
+
baseUrl,
|
|
1687
|
+
accessToken,
|
|
1688
|
+
taskEntry,
|
|
1689
|
+
doneColumn,
|
|
1690
|
+
0,
|
|
1691
|
+
options.json
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
)
|
|
1695
|
+
);
|
|
1221
1696
|
addBaseUrlOption(
|
|
1222
1697
|
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(
|
|
1223
1698
|
async (taskId, options) => {
|
|
@@ -1267,7 +1742,7 @@ addBaseUrlOption(
|
|
|
1267
1742
|
);
|
|
1268
1743
|
var taskHandoffCommand = tasksCommand.command("handoff").description("Create and manage task handoffs");
|
|
1269
1744
|
addBaseUrlOption(
|
|
1270
|
-
taskHandoffCommand.command("create <taskId>").description("Create a handoff for a task").requiredOption("--to <member>", "Target
|
|
1745
|
+
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(
|
|
1271
1746
|
async (taskId, options) => {
|
|
1272
1747
|
const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
|
|
1273
1748
|
const taskEntry = await resolveTaskEntry(
|