@khangal.j/fireside-cli 0.0.4 → 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 +2 -1
  2. package/dist/index.js +366 -21
  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) {
@@ -772,6 +795,58 @@ async function resolveTextInput(value, filePath, label) {
772
795
  }
773
796
  return value;
774
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
+ }
775
850
  async function loadCliContext(baseUrl) {
776
851
  const state = await requireAuthState();
777
852
  const configState = await loadConfigState();
@@ -806,6 +881,37 @@ async function resolveTaskEntry(baseUrl, accessToken, taskId, projectSelector) {
806
881
  }
807
882
  return taskEntries[0];
808
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
+ }
809
915
  function resolveProjectMember(members, selector, currentUserId) {
810
916
  const trimmedSelector = selector.trim();
811
917
  if (!trimmedSelector.length) {
@@ -901,10 +1007,7 @@ function printAssignedTasks(tasks, projectFilter) {
901
1007
  if (task.dueDate) {
902
1008
  printKeyValue(" Due", import_picocolors.default.yellow(formatDueDate(task.dueDate)));
903
1009
  }
904
- printKeyValue(
905
- " Assignees",
906
- formatNames(task.assignees.map((assignee) => assignee.name))
907
- );
1010
+ printKeyValue(" Assignees", formatMemberList(task.assignees));
908
1011
  if (task.description) {
909
1012
  console.log(` ${task.description}`);
910
1013
  }
@@ -945,10 +1048,9 @@ addBaseUrlOption(
945
1048
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
946
1049
  });
947
1050
  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
- }
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));
952
1054
  })
953
1055
  );
954
1056
  program.command("logout").description("Remove the local CLI session").action(async () => {
@@ -967,9 +1069,8 @@ addBaseUrlOption(
967
1069
  const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
968
1070
  const user = await getCurrentUser(baseUrl, state.accessToken);
969
1071
  printKeyValue("Base URL", formatUrl(baseUrl));
970
- printSuccess(
971
- `Signed in as ${import_picocolors.default.bold(user.name)} <${import_picocolors.default.cyan(user.email)}>`
972
- );
1072
+ printSuccess(`Signed in as ${import_picocolors.default.bold(formatUserHandle(user))}.`);
1073
+ printKeyValue("Email", import_picocolors.default.cyan(user.email));
973
1074
  printKeyValue("Username", formatUsername(user.username));
974
1075
  })
975
1076
  );
@@ -1092,7 +1193,7 @@ addBaseUrlOption(
1092
1193
  addBaseUrlOption(
1093
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(
1094
1195
  "-a, --assignee <member>",
1095
- "Assign a member by id, email, @username, or me",
1196
+ "Assign a member by @username, email, id, or me",
1096
1197
  collectOptionValue,
1097
1198
  []
1098
1199
  ).option("--json", "Print the created task as JSON").action(
@@ -1147,10 +1248,202 @@ addBaseUrlOption(
1147
1248
  }
1148
1249
  )
1149
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
+ );
1150
1443
  addBaseUrlOption(
1151
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(
1152
1445
  "-a, --assignee <member>",
1153
- "Replace assignees using id, email, @username, or me",
1446
+ "Replace assignees using @username, email, id, or me",
1154
1447
  collectOptionValue,
1155
1448
  []
1156
1449
  ).option("--clear-assignees", "Remove all assignees").option("--json", "Print the updated task as JSON").action(
@@ -1218,6 +1511,58 @@ addBaseUrlOption(
1218
1511
  }
1219
1512
  )
1220
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
+ );
1221
1566
  addBaseUrlOption(
1222
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(
1223
1568
  async (taskId, options) => {
@@ -1267,7 +1612,7 @@ addBaseUrlOption(
1267
1612
  );
1268
1613
  var taskHandoffCommand = tasksCommand.command("handoff").description("Create and manage task handoffs");
1269
1614
  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(
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(
1271
1616
  async (taskId, options) => {
1272
1617
  const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
1273
1618
  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.5",
4
4
  "description": "Fireside CLI",
5
5
  "license": "MIT",
6
6
  "private": false,