@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.
Files changed (3) hide show
  1. package/README.md +2 -1
  2. package/dist/index.js +513 -38
  3. 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 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.
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 member.username ? `${member.name} ${import_picocolors.default.cyan(`@${member.username}`)}` : member.name;
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 taskEntries = flattenTaskEntries(
941
+ const allEntries = flattenTaskEntries(
802
942
  await loadProjectBoardsResults(baseUrl, accessToken, projectSelector)
803
- ).filter((taskEntry) => taskEntry.task.id === taskId);
804
- if (!taskEntries.length) {
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
- return taskEntries[0];
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.2").configureOutput({
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.email)}.`);
949
- if (user.username) {
950
- printKeyValue("Username", formatUsername(user.username));
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
- `Signed in as ${import_picocolors.default.bold(user.name)} <${import_picocolors.default.cyan(user.email)}>`
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
- console.log(JSON.stringify(filteredTasks, null, 2));
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
- console.log(
1036
- JSON.stringify(
1037
- taskEntries.map((taskEntry) => serializeTaskEntry(taskEntry)),
1038
- null,
1039
- 2
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("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(
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 id, email, @username, or me",
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 id, email, @username, or me",
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 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(
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khangal.j/fireside-cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Fireside CLI",
5
5
  "license": "MIT",
6
6
  "private": false,