@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.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/dist/index.js +431 -39
  3. 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 a user id, email, `@username`, or `me`.
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 member.username ? `${member.name} ${import_picocolors.default.cyan(`@${member.username}`)}` : member.name;
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.email)}.`);
926
- if (user.username) {
927
- printKeyValue("Username", formatUsername(user.username));
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
- `Signed in as ${import_picocolors.default.bold(user.name)} <${import_picocolors.default.cyan(user.email)}>`
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 id, email, @username, or me",
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(projectBoardsResult.boardsData.boards, options.board);
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 id, email, @username, or me",
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(taskEntry.members, options.assignee, user.id) : taskEntry.task.assignees.map((assignee) => assignee.id);
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(JSON.stringify(serializeTaskEntry(updatedTaskEntry), null, 2));
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 user id, email, or @username").requiredOption("--summary <summary>", "Short handoff summary").option("-p, --project <project>", "Limit lookup to a project id or title").option("--next <member>", "Who should own the task after completion").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(
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(taskEntry.members, options.to, user.id);
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(serializeTaskHandoff(taskEntry, taskHandoff), null, 2)
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",
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
  }