@khangal.j/fireside-cli 0.0.1 → 0.0.3

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 +33 -3
  2. package/dist/index.js +858 -46
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -11,19 +11,49 @@ npm install -g @khangal.j/fireside-cli
11
11
  ## Usage
12
12
 
13
13
  ```sh
14
- fireside login --base-url http://localhost:3000
14
+ fireside login
15
15
  fireside status
16
16
  fireside hello
17
17
  fireside my-stuff
18
+ fireside my-stuff --project personal
19
+ fireside my-stuff --json
18
20
  fireside projects list
21
+ fireside tasks list
22
+ fireside tasks list --project personal
23
+ fireside tasks get <task-id>
24
+ fireside tasks get <task-id> --context
25
+ fireside tasks create --project personal --board main --column maybe --title "Ship CLI"
26
+ fireside tasks update <task-id> --title "Ship task CLI"
27
+ fireside tasks delete <task-id>
28
+ fireside tasks handoff create <task-id> --to @alice --summary "Ready for review" --context-file handoff.md
19
29
  ```
20
30
 
31
+ ## Task Commands
32
+
33
+ - `fireside tasks list [--project <project>] [--board <board>] [--column <column>] [--json]`
34
+ - `fireside tasks get <task-id> [--project <project>] [--context] [--json]`
35
+ - `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
+ - `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
+ - `fireside tasks delete <task-id> [--project <project>] [--json]`
38
+ - `fireside tasks handoff create <task-id> --to <member> --summary <summary> [--project <project>] [--next <member>] [--context <markdown> | --context-file <path>] [--json]`
39
+
40
+ Member selectors accept a user id, email, `@username`, or `me`.
41
+ For handoffs, `--to` must be another project member.
42
+
43
+ By default, the CLI talks to:
44
+
45
+ ```text
46
+ https://fireside.khangaljargal.workers.dev
47
+ ```
48
+
49
+ Override it for local development or another environment with `--base-url` or
50
+ `FIRESIDE_BASE_URL`.
51
+
21
52
  ## Local State
22
53
 
23
54
  The CLI stores local files under the `fireside` config directory.
24
55
 
25
- - macOS: `~/Library/Application Support/fireside/{config,auth}.json`
26
- - Linux: `$XDG_CONFIG_HOME/fireside/{config,auth}.json` or `~/.config/fireside/{config,auth}.json`
56
+ - macOS/Linux: `$XDG_CONFIG_HOME/fireside/{config,auth}.json` or `~/.config/fireside/{config,auth}.json`
27
57
  - Windows: `%APPDATA%\\fireside\\{config,auth}.json`
28
58
 
29
59
  `config.json` stores settings like `baseUrl`. `auth.json` stores the local session token.
package/dist/index.js CHANGED
@@ -1,8 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
3
25
 
4
26
  // src/index.ts
27
+ var import_promises2 = require("fs/promises");
5
28
  var import_commander = require("commander");
29
+ var import_picocolors = __toESM(require("picocolors"));
6
30
 
7
31
  // src/lib/api.ts
8
32
  var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
@@ -109,6 +133,70 @@ async function listProjects(baseUrl, accessToken) {
109
133
  headers: getAuthHeaders(accessToken)
110
134
  });
111
135
  }
136
+ async function getProjectBoards(baseUrl, accessToken, projectId) {
137
+ return requestJson(
138
+ `${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards`,
139
+ {
140
+ headers: getAuthHeaders(accessToken)
141
+ }
142
+ );
143
+ }
144
+ async function createTask(baseUrl, accessToken, projectId, boardId, input) {
145
+ return requestJson(
146
+ `${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards/${encodeURIComponent(boardId)}/tasks`,
147
+ {
148
+ method: "POST",
149
+ headers: {
150
+ ...getAuthHeaders(accessToken),
151
+ "content-type": "application/json"
152
+ },
153
+ body: JSON.stringify(input)
154
+ }
155
+ );
156
+ }
157
+ async function updateTask(baseUrl, accessToken, projectId, boardId, taskId, input) {
158
+ return requestJson(
159
+ `${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards/${encodeURIComponent(boardId)}/tasks/${encodeURIComponent(taskId)}`,
160
+ {
161
+ method: "PATCH",
162
+ headers: {
163
+ ...getAuthHeaders(accessToken),
164
+ "content-type": "application/json"
165
+ },
166
+ body: JSON.stringify(input)
167
+ }
168
+ );
169
+ }
170
+ async function deleteTask(baseUrl, accessToken, projectId, boardId, taskId) {
171
+ return requestJson(
172
+ `${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards/${encodeURIComponent(boardId)}/tasks/${encodeURIComponent(taskId)}`,
173
+ {
174
+ method: "DELETE",
175
+ headers: getAuthHeaders(accessToken)
176
+ }
177
+ );
178
+ }
179
+ async function getTaskContext(baseUrl, accessToken, projectId, boardId, taskId) {
180
+ return requestJson(
181
+ `${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards/${encodeURIComponent(boardId)}/tasks/${encodeURIComponent(taskId)}/context`,
182
+ {
183
+ headers: getAuthHeaders(accessToken)
184
+ }
185
+ );
186
+ }
187
+ async function createTaskHandoff(baseUrl, accessToken, projectId, boardId, taskId, input) {
188
+ return requestJson(
189
+ `${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards/${encodeURIComponent(boardId)}/tasks/${encodeURIComponent(taskId)}/handoffs`,
190
+ {
191
+ method: "POST",
192
+ headers: {
193
+ ...getAuthHeaders(accessToken),
194
+ "content-type": "application/json"
195
+ },
196
+ body: JSON.stringify(input)
197
+ }
198
+ );
199
+ }
112
200
  async function listAssignedTasks(baseUrl, accessToken) {
113
201
  return requestJson(`${baseUrl}/api/my-stuff`, {
114
202
  headers: getAuthHeaders(accessToken)
@@ -134,27 +222,28 @@ async function getHello(baseUrl, accessToken) {
134
222
  var import_promises = require("fs/promises");
135
223
  var import_node_os = require("os");
136
224
  var import_node_path = require("path");
137
- var DEFAULT_BASE_URL = "http://localhost:3000";
225
+ var DEFAULT_BASE_URL = "https://fireside.khangaljargal.workers.dev";
138
226
  var legacyMigrationPromise = null;
139
227
  function normalizeBaseUrl(baseUrl) {
140
228
  return baseUrl.trim().replace(/\/+$/, "");
141
229
  }
230
+ function getUnixConfigHomeDirectory() {
231
+ return process.env.XDG_CONFIG_HOME || (0, import_node_path.join)((0, import_node_os.homedir)(), ".config");
232
+ }
142
233
  function getConfigDirectory(appName = "fireside") {
143
234
  switch (process.platform) {
144
- case "darwin":
145
- return (0, import_node_path.join)((0, import_node_os.homedir)(), "Library", "Application Support", appName);
146
235
  case "win32":
147
236
  return (0, import_node_path.join)(
148
237
  process.env.APPDATA || (0, import_node_path.join)((0, import_node_os.homedir)(), "AppData", "Roaming"),
149
238
  appName
150
239
  );
151
240
  default:
152
- return (0, import_node_path.join)(
153
- process.env.XDG_CONFIG_HOME || (0, import_node_path.join)((0, import_node_os.homedir)(), ".config"),
154
- appName
155
- );
241
+ return (0, import_node_path.join)(getUnixConfigHomeDirectory(), appName);
156
242
  }
157
243
  }
244
+ function getLegacyMacOsConfigDirectory(appName) {
245
+ return (0, import_node_path.join)((0, import_node_os.homedir)(), "Library", "Application Support", appName);
246
+ }
158
247
  function getAuthStateFilePath() {
159
248
  return (0, import_node_path.join)(getConfigDirectory(), "auth.json");
160
249
  }
@@ -162,8 +251,17 @@ function getConfigStateFilePath() {
162
251
  return (0, import_node_path.join)(getConfigDirectory(), "config.json");
163
252
  }
164
253
  function getLegacyAuthStateFilePath() {
254
+ if (process.platform === "darwin") {
255
+ return (0, import_node_path.join)(getLegacyMacOsConfigDirectory("fireside-cli"), "auth.json");
256
+ }
165
257
  return (0, import_node_path.join)(getConfigDirectory("fireside-cli"), "auth.json");
166
258
  }
259
+ function getLegacyMacOsAuthStateFilePath() {
260
+ return (0, import_node_path.join)(getLegacyMacOsConfigDirectory("fireside"), "auth.json");
261
+ }
262
+ function getLegacyMacOsConfigStateFilePath() {
263
+ return (0, import_node_path.join)(getLegacyMacOsConfigDirectory("fireside"), "config.json");
264
+ }
167
265
  async function fileExists(filePath) {
168
266
  try {
169
267
  await (0, import_promises.access)(filePath);
@@ -215,6 +313,40 @@ function isLegacyAuthState(value) {
215
313
  return isAuthState(value) && isConfigState(value);
216
314
  }
217
315
  async function migrateLegacyState() {
316
+ const authStateFilePath = getAuthStateFilePath();
317
+ const configStateFilePath = getConfigStateFilePath();
318
+ let hasAuthState = await fileExists(authStateFilePath);
319
+ let hasConfigState = await fileExists(configStateFilePath);
320
+ if (process.platform === "darwin") {
321
+ const legacyMacOsAuthState = await readStateFile(
322
+ getLegacyMacOsAuthStateFilePath()
323
+ );
324
+ const legacyMacOsConfigState = await readStateFile(
325
+ getLegacyMacOsConfigStateFilePath()
326
+ );
327
+ if (legacyMacOsAuthState !== null) {
328
+ if (!isAuthState(legacyMacOsAuthState)) {
329
+ throw new Error("Invalid legacy auth state file format.");
330
+ }
331
+ if (!hasAuthState) {
332
+ await writeStateFile(authStateFilePath, legacyMacOsAuthState);
333
+ hasAuthState = true;
334
+ }
335
+ await (0, import_promises.rm)(getLegacyMacOsAuthStateFilePath(), { force: true });
336
+ }
337
+ if (legacyMacOsConfigState !== null) {
338
+ if (!isConfigState(legacyMacOsConfigState)) {
339
+ throw new Error("Invalid legacy config state file format.");
340
+ }
341
+ if (!hasConfigState) {
342
+ await writeStateFile(configStateFilePath, {
343
+ baseUrl: normalizeBaseUrl(legacyMacOsConfigState.baseUrl)
344
+ });
345
+ hasConfigState = true;
346
+ }
347
+ await (0, import_promises.rm)(getLegacyMacOsConfigStateFilePath(), { force: true });
348
+ }
349
+ }
218
350
  const legacyState = await readStateFile(getLegacyAuthStateFilePath());
219
351
  if (legacyState === null) {
220
352
  return;
@@ -222,18 +354,18 @@ async function migrateLegacyState() {
222
354
  if (!isLegacyAuthState(legacyState)) {
223
355
  throw new Error("Invalid legacy auth state file format.");
224
356
  }
225
- const authStateFilePath = getAuthStateFilePath();
226
- const configStateFilePath = getConfigStateFilePath();
227
- if (!await fileExists(authStateFilePath)) {
357
+ if (!hasAuthState) {
228
358
  await writeStateFile(authStateFilePath, {
229
359
  accessToken: legacyState.accessToken,
230
360
  createdAt: legacyState.createdAt
231
361
  });
362
+ hasAuthState = true;
232
363
  }
233
- if (!await fileExists(configStateFilePath)) {
364
+ if (!hasConfigState) {
234
365
  await writeStateFile(configStateFilePath, {
235
366
  baseUrl: normalizeBaseUrl(legacyState.baseUrl)
236
367
  });
368
+ hasConfigState = true;
237
369
  }
238
370
  await (0, import_promises.rm)(getLegacyAuthStateFilePath(), { force: true });
239
371
  }
@@ -322,6 +454,69 @@ function openBrowser(url) {
322
454
  }
323
455
 
324
456
  // src/index.ts
457
+ function printInfo(message) {
458
+ console.log(`${import_picocolors.default.cyan("[i]")} ${message}`);
459
+ }
460
+ function printSuccess(message) {
461
+ console.log(`${import_picocolors.default.green("[ok]")} ${message}`);
462
+ }
463
+ function printWarning(message) {
464
+ console.log(`${import_picocolors.default.yellow("[!]")} ${message}`);
465
+ }
466
+ function printKeyValue(label, value) {
467
+ console.log(`${import_picocolors.default.dim(`${label}:`)} ${value}`);
468
+ }
469
+ function formatUrl(url) {
470
+ return import_picocolors.default.underline(import_picocolors.default.cyan(url));
471
+ }
472
+ function formatCode(code) {
473
+ return import_picocolors.default.bold(import_picocolors.default.magenta(code));
474
+ }
475
+ function formatUsername(username) {
476
+ return username ? import_picocolors.default.cyan(`@${username}`) : import_picocolors.default.dim("Not set");
477
+ }
478
+ function formatHeading(title, id) {
479
+ if (!id) {
480
+ return import_picocolors.default.bold(title);
481
+ }
482
+ return `${import_picocolors.default.bold(title)} ${import_picocolors.default.dim(`(${id})`)}`;
483
+ }
484
+ function formatProjectColor(color) {
485
+ const colorFormatters = {
486
+ amber: import_picocolors.default.yellow,
487
+ blue: import_picocolors.default.blue,
488
+ cyan: import_picocolors.default.cyan,
489
+ emerald: import_picocolors.default.green,
490
+ lime: import_picocolors.default.green,
491
+ pink: import_picocolors.default.magenta,
492
+ red: import_picocolors.default.red,
493
+ slate: import_picocolors.default.white,
494
+ violet: import_picocolors.default.magenta
495
+ };
496
+ const formatter = colorFormatters[color] || ((value) => value);
497
+ return `${formatter("o")} ${formatter(color)}`;
498
+ }
499
+ function formatProjectMarker(color) {
500
+ const colorFormatters = {
501
+ amber: import_picocolors.default.yellow,
502
+ blue: import_picocolors.default.blue,
503
+ cyan: import_picocolors.default.cyan,
504
+ emerald: import_picocolors.default.green,
505
+ lime: import_picocolors.default.green,
506
+ pink: import_picocolors.default.magenta,
507
+ red: import_picocolors.default.red,
508
+ slate: import_picocolors.default.white,
509
+ violet: import_picocolors.default.magenta
510
+ };
511
+ const formatter = colorFormatters[color] || ((value) => value);
512
+ return formatter("o");
513
+ }
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
+ }
325
520
  function formatUserCodeForDisplay(userCode) {
326
521
  return userCode.match(/.{1,4}/g)?.join("-") || userCode;
327
522
  }
@@ -339,15 +534,306 @@ async function requireAuthState() {
339
534
  }
340
535
  return state;
341
536
  }
537
+ function collectOptionValue(value, previous) {
538
+ return [...previous, value];
539
+ }
540
+ function normalizeLookupValue(value) {
541
+ return value.trim().toLowerCase();
542
+ }
543
+ function matchIdOrTitle(item, selector) {
544
+ const trimmedSelector = selector.trim();
545
+ if (!trimmedSelector.length) {
546
+ return false;
547
+ }
548
+ if (item.id === trimmedSelector) {
549
+ return true;
550
+ }
551
+ const normalizedSelector = normalizeLookupValue(trimmedSelector);
552
+ const normalizedTitle = normalizeLookupValue(item.title);
553
+ return normalizedTitle === normalizedSelector || normalizedTitle.includes(normalizedSelector);
554
+ }
555
+ function resolveByIdOrTitle(items, selector, itemLabel) {
556
+ const trimmedSelector = selector.trim();
557
+ if (!trimmedSelector.length) {
558
+ throw new Error(`${itemLabel} is required.`);
559
+ }
560
+ const exactIdMatch = items.find((item) => item.id === trimmedSelector);
561
+ if (exactIdMatch) {
562
+ return exactIdMatch;
563
+ }
564
+ const normalizedSelector = normalizeLookupValue(trimmedSelector);
565
+ const exactTitleMatches = items.filter(
566
+ (item) => normalizeLookupValue(item.title) === normalizedSelector
567
+ );
568
+ if (exactTitleMatches.length === 1) {
569
+ return exactTitleMatches[0];
570
+ }
571
+ if (exactTitleMatches.length > 1) {
572
+ throw new Error(
573
+ `Multiple ${itemLabel}s match "${selector}". Use the ${itemLabel} id instead.`
574
+ );
575
+ }
576
+ const partialMatches = items.filter(
577
+ (item) => normalizeLookupValue(item.title).includes(normalizedSelector)
578
+ );
579
+ if (partialMatches.length === 1) {
580
+ return partialMatches[0];
581
+ }
582
+ if (partialMatches.length > 1) {
583
+ throw new Error(
584
+ `Multiple ${itemLabel}s match "${selector}". Use the ${itemLabel} id instead.`
585
+ );
586
+ }
587
+ throw new Error(`No ${itemLabel} found for "${selector}".`);
588
+ }
589
+ function resolveBoard(boards, boardSelector) {
590
+ if (boardSelector) {
591
+ return resolveByIdOrTitle(boards, boardSelector, "board");
592
+ }
593
+ if (boards.length === 1) {
594
+ return boards[0];
595
+ }
596
+ throw new Error("This project has multiple boards. Pass `--board`.");
597
+ }
598
+ function resolveColumn(columns, columnSelector) {
599
+ if (columnSelector) {
600
+ return resolveByIdOrTitle(columns, columnSelector, "column");
601
+ }
602
+ if (columns.length === 1) {
603
+ return columns[0];
604
+ }
605
+ throw new Error("This board has multiple columns. Pass `--column`.");
606
+ }
607
+ function uniqueStrings(values) {
608
+ return [...new Set(values)];
609
+ }
610
+ function formatMember(member) {
611
+ return member.username ? `${member.name} ${import_picocolors.default.cyan(`@${member.username}`)}` : member.name;
612
+ }
613
+ function formatMemberList(members) {
614
+ if (!members.length) {
615
+ return import_picocolors.default.dim("None");
616
+ }
617
+ return members.map((member) => formatMember(member)).join(import_picocolors.default.dim(", "));
618
+ }
619
+ function compareTaskEntries(left, right) {
620
+ return left.project.title.localeCompare(right.project.title) || left.board.position - right.board.position || left.column.position - right.column.position || left.task.position - right.task.position || left.task.title.localeCompare(right.task.title);
621
+ }
622
+ function flattenTaskEntries(projectBoardsResults) {
623
+ return projectBoardsResults.flatMap(
624
+ ({ boardsData, project }) => boardsData.boards.flatMap(
625
+ (board) => board.columns.flatMap(
626
+ (column) => column.tasks.map((task) => ({
627
+ board,
628
+ column,
629
+ members: boardsData.members,
630
+ project,
631
+ task
632
+ }))
633
+ )
634
+ )
635
+ ).sort(compareTaskEntries);
636
+ }
637
+ function serializeTaskEntry(taskEntry, contextRevision) {
638
+ return {
639
+ board: {
640
+ id: taskEntry.board.id,
641
+ title: taskEntry.board.title
642
+ },
643
+ column: {
644
+ id: taskEntry.column.id,
645
+ role: taskEntry.column.role,
646
+ title: taskEntry.column.title
647
+ },
648
+ ...contextRevision !== void 0 ? { contextRevision } : {},
649
+ project: {
650
+ color: taskEntry.project.color,
651
+ id: taskEntry.project.id,
652
+ title: taskEntry.project.title
653
+ },
654
+ task: taskEntry.task
655
+ };
656
+ }
657
+ function serializeTaskHandoff(taskEntry, taskHandoff) {
658
+ return {
659
+ board: {
660
+ id: taskEntry.board.id,
661
+ title: taskEntry.board.title
662
+ },
663
+ column: {
664
+ id: taskEntry.column.id,
665
+ title: taskEntry.column.title
666
+ },
667
+ handoff: taskHandoff,
668
+ project: {
669
+ color: taskEntry.project.color,
670
+ id: taskEntry.project.id,
671
+ title: taskEntry.project.title
672
+ },
673
+ task: {
674
+ id: taskEntry.task.id,
675
+ title: taskEntry.task.title
676
+ }
677
+ };
678
+ }
679
+ function printTaskSummary(taskEntry) {
680
+ console.log(formatHeading(taskEntry.task.title, taskEntry.task.id));
681
+ console.log(
682
+ ` ${formatProjectMarker(taskEntry.project.color)} ${import_picocolors.default.cyan(taskEntry.project.title)} ${import_picocolors.default.dim(`/ ${taskEntry.board.title} / ${taskEntry.column.title}`)}`
683
+ );
684
+ if (taskEntry.task.dueDate) {
685
+ printKeyValue(" Due", import_picocolors.default.yellow(formatDueDate(taskEntry.task.dueDate)));
686
+ }
687
+ printKeyValue(" Assignees", formatMemberList(taskEntry.task.assignees));
688
+ if (taskEntry.task.description) {
689
+ console.log(` ${taskEntry.task.description}`);
690
+ }
691
+ console.log("");
692
+ }
693
+ function printTaskDetails(taskEntry, contextRevision) {
694
+ console.log(formatHeading(taskEntry.task.title, taskEntry.task.id));
695
+ printKeyValue(" Project", import_picocolors.default.cyan(taskEntry.project.title));
696
+ printKeyValue(" Board", taskEntry.board.title);
697
+ printKeyValue(" Column", taskEntry.column.title);
698
+ printKeyValue(
699
+ " Due",
700
+ taskEntry.task.dueDate ? import_picocolors.default.yellow(formatDueDate(taskEntry.task.dueDate)) : import_picocolors.default.dim("None")
701
+ );
702
+ printKeyValue(" Assignees", formatMemberList(taskEntry.task.assignees));
703
+ printKeyValue(
704
+ " Description",
705
+ taskEntry.task.description || import_picocolors.default.dim("None")
706
+ );
707
+ if (contextRevision !== void 0) {
708
+ printKeyValue(
709
+ " Task context",
710
+ contextRevision ? `v${contextRevision.version}` : import_picocolors.default.dim("None")
711
+ );
712
+ if (contextRevision?.contentMarkdown) {
713
+ console.log("");
714
+ console.log(contextRevision.contentMarkdown);
715
+ }
716
+ }
717
+ }
718
+ function printTaskHandoff(taskEntry, taskHandoff) {
719
+ console.log(formatHeading(taskEntry.task.title, taskEntry.task.id));
720
+ console.log(
721
+ ` ${formatProjectMarker(taskEntry.project.color)} ${import_picocolors.default.cyan(taskEntry.project.title)} ${import_picocolors.default.dim(`/ ${taskEntry.board.title} / ${taskEntry.column.title}`)}`
722
+ );
723
+ printKeyValue(" Handoff", taskHandoff.id);
724
+ printKeyValue(" Status", import_picocolors.default.cyan(taskHandoff.status));
725
+ printKeyValue(" From", formatMember(taskHandoff.fromUser));
726
+ printKeyValue(" To", formatMember(taskHandoff.toUser));
727
+ printKeyValue(
728
+ " Next assignee",
729
+ formatMember(taskHandoff.nextAssigneeUser)
730
+ );
731
+ console.log(` ${taskHandoff.summary}`);
732
+ }
733
+ async function readTextFile(filePath, label) {
734
+ try {
735
+ return await (0, import_promises2.readFile)(filePath, "utf8");
736
+ } catch (error) {
737
+ const message = error instanceof Error ? error.message : `Unable to read ${label} file.`;
738
+ throw new Error(`Failed to read ${label} file: ${message}`);
739
+ }
740
+ }
741
+ async function resolveTextInput(value, filePath, label) {
742
+ if (value !== void 0 && filePath !== void 0) {
743
+ throw new Error(
744
+ `Pass either \`--${label}\` or \`--${label}-file\`, not both.`
745
+ );
746
+ }
747
+ if (filePath !== void 0) {
748
+ return readTextFile(filePath, label);
749
+ }
750
+ return value;
751
+ }
752
+ async function loadCliContext(baseUrl) {
753
+ const state = await requireAuthState();
754
+ const configState = await loadConfigState();
755
+ return {
756
+ accessToken: state.accessToken,
757
+ baseUrl: await resolveBaseUrl(baseUrl, configState)
758
+ };
759
+ }
760
+ async function loadCliContextWithCurrentUser(baseUrl) {
761
+ const cliContext = await loadCliContext(baseUrl);
762
+ return {
763
+ ...cliContext,
764
+ user: await getCurrentUser(cliContext.baseUrl, cliContext.accessToken)
765
+ };
766
+ }
767
+ async function loadProjectBoardsResults(baseUrl, accessToken, projectSelector) {
768
+ const projects = await listProjects(baseUrl, accessToken);
769
+ const selectedProjects = projectSelector ? [resolveByIdOrTitle(projects, projectSelector, "project")] : projects;
770
+ return Promise.all(
771
+ selectedProjects.map(async (project) => ({
772
+ boardsData: await getProjectBoards(baseUrl, accessToken, project.id),
773
+ project
774
+ }))
775
+ );
776
+ }
777
+ async function resolveTaskEntry(baseUrl, accessToken, taskId, projectSelector) {
778
+ const taskEntries = flattenTaskEntries(
779
+ await loadProjectBoardsResults(baseUrl, accessToken, projectSelector)
780
+ ).filter((taskEntry) => taskEntry.task.id === taskId);
781
+ if (!taskEntries.length) {
782
+ throw new Error(`Task ${taskId} was not found.`);
783
+ }
784
+ return taskEntries[0];
785
+ }
786
+ function resolveProjectMember(members, selector, currentUserId) {
787
+ const trimmedSelector = selector.trim();
788
+ if (!trimmedSelector.length) {
789
+ throw new Error("Member selector is required.");
790
+ }
791
+ if (trimmedSelector === "me") {
792
+ const currentMember = members.find((member) => member.id === currentUserId);
793
+ if (!currentMember) {
794
+ throw new Error("You are not a member of this project.");
795
+ }
796
+ return currentMember;
797
+ }
798
+ const exactIdMatch = members.find((member) => member.id === trimmedSelector);
799
+ if (exactIdMatch) {
800
+ return exactIdMatch;
801
+ }
802
+ const normalizedSelector = normalizeLookupValue(
803
+ trimmedSelector.startsWith("@") ? trimmedSelector.slice(1) : trimmedSelector
804
+ );
805
+ const usernameMatch = members.find(
806
+ (member) => normalizeLookupValue(member.username || "") === normalizedSelector
807
+ );
808
+ if (usernameMatch) {
809
+ return usernameMatch;
810
+ }
811
+ const emailMatch = members.find(
812
+ (member) => normalizeLookupValue(member.email) === normalizedSelector
813
+ );
814
+ if (emailMatch) {
815
+ return emailMatch;
816
+ }
817
+ throw new Error(
818
+ `No project member found for "${selector}". Use a user id, email, @username, or \`me\`.`
819
+ );
820
+ }
821
+ function resolveProjectMemberIds(members, selectors, currentUserId) {
822
+ return uniqueStrings(
823
+ selectors.map(
824
+ (selector) => resolveProjectMember(members, selector, currentUserId).id
825
+ )
826
+ );
827
+ }
342
828
  function printProjects(projects) {
343
829
  if (!projects.length) {
344
- console.log("No projects found.");
830
+ printWarning("No projects found.");
345
831
  return;
346
832
  }
347
833
  for (const project of projects) {
348
- console.log(`${project.title} (${project.id})`);
349
- console.log(` Color: ${project.color}`);
350
- console.log(` Members: ${project.members.length}`);
834
+ console.log(formatHeading(project.title, project.id));
835
+ printKeyValue(" Color", formatProjectColor(project.color));
836
+ printKeyValue(" Members", import_picocolors.default.cyan(String(project.members.length)));
351
837
  console.log(` ${project.description}`);
352
838
  console.log("");
353
839
  }
@@ -362,49 +848,69 @@ function formatDueDate(dueDate) {
362
848
  year: "numeric"
363
849
  }).format(/* @__PURE__ */ new Date(`${dueDate}T00:00:00`));
364
850
  }
365
- function printAssignedTasks(baseUrl, tasks) {
851
+ function filterAssignedTasksByProject(tasks, projectFilter) {
852
+ const trimmedProjectFilter = projectFilter?.trim();
853
+ const normalizedProjectFilter = trimmedProjectFilter?.toLowerCase();
854
+ if (!normalizedProjectFilter) {
855
+ return tasks;
856
+ }
857
+ return tasks.filter((task) => {
858
+ const normalizedProjectTitle = task.projectTitle.toLowerCase();
859
+ return task.projectId === trimmedProjectFilter || normalizedProjectTitle === normalizedProjectFilter || normalizedProjectTitle.includes(normalizedProjectFilter);
860
+ });
861
+ }
862
+ function printAssignedTasks(tasks, projectFilter) {
366
863
  if (!tasks.length) {
367
- console.log("Nothing assigned right now.");
864
+ if (projectFilter) {
865
+ printWarning(
866
+ `No assigned tasks found for project filter: ${projectFilter}`
867
+ );
868
+ return;
869
+ }
870
+ printWarning("Nothing assigned right now.");
368
871
  return;
369
872
  }
370
873
  for (const task of tasks) {
371
- console.log(`${task.title} (${task.id})`);
874
+ console.log(formatHeading(task.title, task.id));
372
875
  console.log(
373
- ` ${task.projectTitle} / ${task.boardTitle} / ${task.columnTitle}`
876
+ ` ${formatProjectMarker(task.projectColor)} ${import_picocolors.default.cyan(task.projectTitle)} ${import_picocolors.default.dim(`/ ${task.boardTitle} / ${task.columnTitle}`)}`
374
877
  );
375
- console.log(` Due: ${formatDueDate(task.dueDate)}`);
376
- console.log(
377
- ` Assignees: ${task.assignees.map((assignee) => assignee.name).join(", ") || "None"}`
878
+ if (task.dueDate) {
879
+ printKeyValue(" Due", import_picocolors.default.yellow(formatDueDate(task.dueDate)));
880
+ }
881
+ printKeyValue(
882
+ " Assignees",
883
+ formatNames(task.assignees.map((assignee) => assignee.name))
378
884
  );
379
885
  if (task.description) {
380
886
  console.log(` ${task.description}`);
381
887
  }
382
- console.log(
383
- ` ${baseUrl}/projects/${encodeURIComponent(task.projectId)}/boards/tasks/${encodeURIComponent(task.id)}`
384
- );
385
888
  console.log("");
386
889
  }
387
890
  }
388
891
  var program = new import_commander.Command();
389
- program.name("fireside").description("Fireside CLI").version("0.0.1").showHelpAfterError();
892
+ program.name("fireside").description("Fireside CLI").version("0.0.2").configureOutput({
893
+ outputError: (message, write) => write(import_picocolors.default.red(message))
894
+ }).showHelpAfterError();
390
895
  addBaseUrlOption(
391
896
  program.command("login").description("Authenticate with Fireside using device authorization").option("--no-open", "Do not open the browser automatically").action(async (options) => {
392
897
  const configState = await loadConfigState();
393
898
  const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
394
899
  const deviceCode = await createDeviceCode(baseUrl);
395
900
  const verificationUrl = deviceCode.verification_uri;
396
- console.log(`Base URL: ${baseUrl}`);
397
- console.log(`Open this URL: ${verificationUrl}`);
398
- console.log(
399
- `Enter this code in the browser: ${formatUserCodeForDisplay(deviceCode.user_code)}`
901
+ printKeyValue("Base URL", formatUrl(baseUrl));
902
+ printKeyValue("Open", formatUrl(verificationUrl));
903
+ printKeyValue(
904
+ "Code",
905
+ formatCode(formatUserCodeForDisplay(deviceCode.user_code))
400
906
  );
401
907
  if (options.open) {
402
908
  const opened = openBrowser(verificationUrl);
403
909
  if (opened) {
404
- console.log("Opened the browser for approval.");
910
+ printSuccess("Opened the browser for approval.");
405
911
  }
406
912
  }
407
- console.log("Waiting for approval...");
913
+ printInfo("Waiting for approval...");
408
914
  const accessToken = await pollForAccessToken(
409
915
  baseUrl,
410
916
  deviceCode.device_code,
@@ -416,17 +922,20 @@ addBaseUrlOption(
416
922
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
417
923
  });
418
924
  const user = await getCurrentUser(baseUrl, accessToken);
419
- console.log(`Signed in as ${user.email}.`);
925
+ printSuccess(`Signed in as ${import_picocolors.default.bold(user.email)}.`);
926
+ if (user.username) {
927
+ printKeyValue("Username", formatUsername(user.username));
928
+ }
420
929
  })
421
930
  );
422
931
  program.command("logout").description("Remove the local CLI session").action(async () => {
423
932
  const state = await loadAuthState();
424
933
  if (!state) {
425
- console.log("Already signed out.");
934
+ printInfo("Already signed out.");
426
935
  return;
427
936
  }
428
937
  await clearAuthState();
429
- console.log("Removed the local CLI session.");
938
+ printSuccess("Removed the local CLI session.");
430
939
  });
431
940
  addBaseUrlOption(
432
941
  program.command("status").description("Show the current authenticated CLI user").action(async (options) => {
@@ -434,8 +943,11 @@ addBaseUrlOption(
434
943
  const configState = await loadConfigState();
435
944
  const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
436
945
  const user = await getCurrentUser(baseUrl, state.accessToken);
437
- console.log(`Base URL: ${baseUrl}`);
438
- console.log(`Signed in as: ${user.name} <${user.email}>`);
946
+ printKeyValue("Base URL", formatUrl(baseUrl));
947
+ printSuccess(
948
+ `Signed in as ${import_picocolors.default.bold(user.name)} <${import_picocolors.default.cyan(user.email)}>`
949
+ );
950
+ printKeyValue("Username", formatUsername(user.username));
439
951
  })
440
952
  );
441
953
  addBaseUrlOption(
@@ -444,17 +956,27 @@ addBaseUrlOption(
444
956
  const configState = await loadConfigState();
445
957
  const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
446
958
  const message = await getHello(baseUrl, state.accessToken);
447
- console.log(message);
959
+ printInfo(message);
448
960
  })
449
961
  );
450
962
  addBaseUrlOption(
451
- program.command("my-stuff").description("List tasks currently assigned to you").action(async (options) => {
452
- const state = await requireAuthState();
453
- const configState = await loadConfigState();
454
- const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
455
- const tasks = await listAssignedTasks(baseUrl, state.accessToken);
456
- printAssignedTasks(baseUrl, tasks);
457
- })
963
+ 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(
964
+ async (options) => {
965
+ const state = await requireAuthState();
966
+ const configState = await loadConfigState();
967
+ const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
968
+ const tasks = await listAssignedTasks(baseUrl, state.accessToken);
969
+ const filteredTasks = filterAssignedTasksByProject(
970
+ tasks,
971
+ options.project
972
+ );
973
+ if (options.json) {
974
+ console.log(JSON.stringify(filteredTasks, null, 2));
975
+ return;
976
+ }
977
+ printAssignedTasks(filteredTasks, options.project);
978
+ }
979
+ )
458
980
  );
459
981
  addBaseUrlOption(
460
982
  program.command("projects").description("Interact with project APIs").command("list").description("List accessible projects").action(async function() {
@@ -468,12 +990,302 @@ addBaseUrlOption(
468
990
  printProjects(projects);
469
991
  })
470
992
  );
993
+ var tasksCommand = program.command("tasks").description("Interact with tasks");
994
+ addBaseUrlOption(
995
+ 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(
996
+ async (options) => {
997
+ const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
998
+ let taskEntries = flattenTaskEntries(
999
+ await loadProjectBoardsResults(baseUrl, accessToken, options.project)
1000
+ );
1001
+ if (options.board) {
1002
+ taskEntries = taskEntries.filter(
1003
+ (taskEntry) => matchIdOrTitle(taskEntry.board, options.board)
1004
+ );
1005
+ }
1006
+ if (options.column) {
1007
+ taskEntries = taskEntries.filter(
1008
+ (taskEntry) => matchIdOrTitle(taskEntry.column, options.column)
1009
+ );
1010
+ }
1011
+ if (options.json) {
1012
+ console.log(
1013
+ JSON.stringify(
1014
+ taskEntries.map((taskEntry) => serializeTaskEntry(taskEntry)),
1015
+ null,
1016
+ 2
1017
+ )
1018
+ );
1019
+ return;
1020
+ }
1021
+ if (!taskEntries.length) {
1022
+ printWarning("No tasks found.");
1023
+ return;
1024
+ }
1025
+ for (const taskEntry of taskEntries) {
1026
+ printTaskSummary(taskEntry);
1027
+ }
1028
+ }
1029
+ )
1030
+ );
1031
+ 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(
1033
+ async (taskId, options) => {
1034
+ const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
1035
+ const taskEntry = await resolveTaskEntry(
1036
+ baseUrl,
1037
+ accessToken,
1038
+ taskId,
1039
+ options.project
1040
+ );
1041
+ const contextRevision = options.context ? await getTaskContext(
1042
+ baseUrl,
1043
+ accessToken,
1044
+ taskEntry.project.id,
1045
+ taskEntry.board.id,
1046
+ taskId
1047
+ ) : void 0;
1048
+ if (options.json) {
1049
+ console.log(
1050
+ JSON.stringify(
1051
+ serializeTaskEntry(taskEntry, contextRevision),
1052
+ null,
1053
+ 2
1054
+ )
1055
+ );
1056
+ return;
1057
+ }
1058
+ printTaskDetails(taskEntry, contextRevision);
1059
+ }
1060
+ )
1061
+ );
1062
+ addBaseUrlOption(
1063
+ 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
+ "-a, --assignee <member>",
1065
+ "Assign a member by id, email, @username, or me",
1066
+ collectOptionValue,
1067
+ []
1068
+ ).option("--json", "Print the created task as JSON").action(
1069
+ async (options) => {
1070
+ const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
1071
+ const [projectBoardsResult] = await loadProjectBoardsResults(
1072
+ baseUrl,
1073
+ accessToken,
1074
+ options.project
1075
+ );
1076
+ const board = resolveBoard(projectBoardsResult.boardsData.boards, options.board);
1077
+ const column = resolveColumn(board.columns, options.column);
1078
+ const description = await resolveTextInput(
1079
+ options.description,
1080
+ options.descriptionFile,
1081
+ "description"
1082
+ );
1083
+ const assigneeIds = resolveProjectMemberIds(
1084
+ projectBoardsResult.boardsData.members,
1085
+ options.assignee,
1086
+ user.id
1087
+ );
1088
+ const createdTask = await createTask(
1089
+ baseUrl,
1090
+ accessToken,
1091
+ projectBoardsResult.project.id,
1092
+ board.id,
1093
+ {
1094
+ assigneeIds,
1095
+ boardColumnId: column.id,
1096
+ description: description ?? "",
1097
+ dueDate: options.dueDate ?? "",
1098
+ title: options.title
1099
+ }
1100
+ );
1101
+ const taskEntry = {
1102
+ board,
1103
+ column,
1104
+ members: projectBoardsResult.boardsData.members,
1105
+ project: projectBoardsResult.project,
1106
+ task: createdTask
1107
+ };
1108
+ if (options.json) {
1109
+ console.log(JSON.stringify(serializeTaskEntry(taskEntry), null, 2));
1110
+ return;
1111
+ }
1112
+ printSuccess("Created task.");
1113
+ printTaskSummary(taskEntry);
1114
+ }
1115
+ )
1116
+ );
1117
+ addBaseUrlOption(
1118
+ 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
+ "-a, --assignee <member>",
1120
+ "Replace assignees using id, email, @username, or me",
1121
+ collectOptionValue,
1122
+ []
1123
+ ).option("--clear-assignees", "Remove all assignees").option("--json", "Print the updated task as JSON").action(
1124
+ async (taskId, options) => {
1125
+ if (options.title === void 0 && options.description === void 0 && options.descriptionFile === void 0 && !options.clearDescription && options.dueDate === void 0 && !options.clearDueDate && !options.assignee.length && !options.clearAssignees) {
1126
+ throw new Error("No updates requested.");
1127
+ }
1128
+ if (options.clearDescription && (options.description !== void 0 || options.descriptionFile !== void 0)) {
1129
+ throw new Error(
1130
+ "Pass either a new description or `--clear-description`, not both."
1131
+ );
1132
+ }
1133
+ if (options.clearDueDate && options.dueDate !== void 0) {
1134
+ throw new Error(
1135
+ "Pass either `--due-date` or `--clear-due-date`, not both."
1136
+ );
1137
+ }
1138
+ if (options.clearAssignees && options.assignee.length) {
1139
+ throw new Error(
1140
+ "Pass either `--assignee` or `--clear-assignees`, not both."
1141
+ );
1142
+ }
1143
+ const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
1144
+ const taskEntry = await resolveTaskEntry(
1145
+ baseUrl,
1146
+ accessToken,
1147
+ taskId,
1148
+ options.project
1149
+ );
1150
+ const description = await resolveTextInput(
1151
+ options.description,
1152
+ options.descriptionFile,
1153
+ "description"
1154
+ );
1155
+ const assigneeIds = options.clearAssignees ? [] : options.assignee.length ? resolveProjectMemberIds(taskEntry.members, options.assignee, user.id) : taskEntry.task.assignees.map((assignee) => assignee.id);
1156
+ const updatedTask = await updateTask(
1157
+ baseUrl,
1158
+ accessToken,
1159
+ taskEntry.project.id,
1160
+ taskEntry.board.id,
1161
+ taskId,
1162
+ {
1163
+ assigneeIds,
1164
+ description: options.clearDescription ? "" : description ?? taskEntry.task.description ?? "",
1165
+ dueDate: options.clearDueDate ? "" : options.dueDate ?? taskEntry.task.dueDate ?? "",
1166
+ title: options.title ?? taskEntry.task.title
1167
+ }
1168
+ );
1169
+ const updatedTaskEntry = {
1170
+ ...taskEntry,
1171
+ task: updatedTask
1172
+ };
1173
+ if (options.json) {
1174
+ console.log(JSON.stringify(serializeTaskEntry(updatedTaskEntry), null, 2));
1175
+ return;
1176
+ }
1177
+ printSuccess("Updated task.");
1178
+ printTaskDetails(updatedTaskEntry);
1179
+ }
1180
+ )
1181
+ );
1182
+ addBaseUrlOption(
1183
+ 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
+ async (taskId, options) => {
1185
+ const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
1186
+ const taskEntry = await resolveTaskEntry(
1187
+ baseUrl,
1188
+ accessToken,
1189
+ taskId,
1190
+ options.project
1191
+ );
1192
+ const deletedTask = await deleteTask(
1193
+ baseUrl,
1194
+ accessToken,
1195
+ taskEntry.project.id,
1196
+ taskEntry.board.id,
1197
+ taskId
1198
+ );
1199
+ if (options.json) {
1200
+ console.log(
1201
+ JSON.stringify(
1202
+ {
1203
+ board: {
1204
+ id: taskEntry.board.id,
1205
+ title: taskEntry.board.title
1206
+ },
1207
+ column: {
1208
+ id: taskEntry.column.id,
1209
+ title: taskEntry.column.title
1210
+ },
1211
+ id: deletedTask.id,
1212
+ project: {
1213
+ id: taskEntry.project.id,
1214
+ title: taskEntry.project.title
1215
+ }
1216
+ },
1217
+ null,
1218
+ 2
1219
+ )
1220
+ );
1221
+ return;
1222
+ }
1223
+ printSuccess(
1224
+ `Deleted ${import_picocolors.default.bold(taskEntry.task.title)} ${import_picocolors.default.dim(`(${deletedTask.id})`)}.`
1225
+ );
1226
+ }
1227
+ )
1228
+ );
1229
+ var taskHandoffCommand = tasksCommand.command("handoff").description("Create and manage task handoffs");
1230
+ 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(
1232
+ async (taskId, options) => {
1233
+ const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
1234
+ const taskEntry = await resolveTaskEntry(
1235
+ baseUrl,
1236
+ accessToken,
1237
+ taskId,
1238
+ options.project
1239
+ );
1240
+ const contextMarkdown = await resolveTextInput(
1241
+ options.context,
1242
+ options.contextFile,
1243
+ "context"
1244
+ );
1245
+ if (contextMarkdown === void 0) {
1246
+ throw new Error(
1247
+ "Handoff context is required. Pass `--context` or `--context-file`."
1248
+ );
1249
+ }
1250
+ const toUser = resolveProjectMember(taskEntry.members, options.to, user.id);
1251
+ if (toUser.id === user.id) {
1252
+ throw new Error("Choose another project member for `--to`.");
1253
+ }
1254
+ const nextAssigneeUser = resolveProjectMember(
1255
+ taskEntry.members,
1256
+ options.next || "me",
1257
+ user.id
1258
+ );
1259
+ const taskHandoff = await createTaskHandoff(
1260
+ baseUrl,
1261
+ accessToken,
1262
+ taskEntry.project.id,
1263
+ taskEntry.board.id,
1264
+ taskId,
1265
+ {
1266
+ contextMarkdown,
1267
+ nextAssigneeUserId: nextAssigneeUser.id,
1268
+ summary: options.summary,
1269
+ toUserId: toUser.id
1270
+ }
1271
+ );
1272
+ if (options.json) {
1273
+ console.log(
1274
+ JSON.stringify(serializeTaskHandoff(taskEntry, taskHandoff), null, 2)
1275
+ );
1276
+ return;
1277
+ }
1278
+ printSuccess("Created handoff.");
1279
+ printTaskHandoff(taskEntry, taskHandoff);
1280
+ }
1281
+ )
1282
+ );
471
1283
  async function main() {
472
1284
  try {
473
1285
  await program.parseAsync(process.argv);
474
1286
  } catch (error) {
475
1287
  const message = error instanceof Error ? error.message : "Unknown CLI error.";
476
- console.error(message);
1288
+ console.error(`${import_picocolors.default.red("[x]")} ${message}`);
477
1289
  process.exitCode = 1;
478
1290
  }
479
1291
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khangal.j/fireside-cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Fireside CLI",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -26,7 +26,8 @@
26
26
  "start": "node dist/index.js"
27
27
  },
28
28
  "dependencies": {
29
- "commander": "^14.0.1"
29
+ "commander": "^14.0.1",
30
+ "picocolors": "^1.1.1"
30
31
  },
31
32
  "devDependencies": {
32
33
  "tsup": "^8.5.0"