@khangal.j/fireside-cli 0.0.2 → 0.0.4

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 -1
  2. package/dist/index.js +851 -36
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -11,13 +11,45 @@ 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 get <task-id> --handoff
26
+ fireside tasks create --project personal --board main --column maybe --title "Ship CLI"
27
+ fireside tasks update <task-id> --title "Ship task CLI"
28
+ fireside tasks delete <task-id>
29
+ fireside tasks handoff create <task-id> --to @alice --summary "Ready for review" --context-file handoff.md
19
30
  ```
20
31
 
32
+ ## Task Commands
33
+
34
+ - `fireside tasks list [--project <project>] [--board <board>] [--column <column>] [--json]`
35
+ - `fireside tasks get <task-id> [--project <project>] [--context] [--handoff] [--json]`
36
+ - `fireside tasks create --project <project> --column <column> --title <title> [--board <board>] [--description <text> | --description-file <path>] [--due-date YYYY-MM-DD] [--assignee <member>]... [--json]`
37
+ - `fireside tasks update <task-id> [--project <project>] [--title <title>] [--description <text> | --description-file <path> | --clear-description] [--due-date YYYY-MM-DD | --clear-due-date] [--assignee <member>]... [--clear-assignees] [--json]`
38
+ - `fireside tasks delete <task-id> [--project <project>] [--json]`
39
+ - `fireside tasks handoff create <task-id> --to <member> --summary <summary> [--project <project>] [--next <member>] [--context <markdown> | --context-file <path>] [--json]`
40
+
41
+ Member selectors accept a user id, email, `@username`, or `me`.
42
+ For handoffs, `--to` must be another project member.
43
+
44
+ By default, the CLI talks to:
45
+
46
+ ```text
47
+ https://fireside.khangaljargal.workers.dev
48
+ ```
49
+
50
+ Override it for local development or another environment with `--base-url` or
51
+ `FIRESIDE_BASE_URL`.
52
+
21
53
  ## Local State
22
54
 
23
55
  The CLI stores local files under the `fireside` config directory.
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,78 @@ 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
+ }
200
+ async function listTaskHandoffs(baseUrl, accessToken, projectId, boardId, taskId) {
201
+ return requestJson(
202
+ `${baseUrl}/api/projects/${encodeURIComponent(projectId)}/boards/${encodeURIComponent(boardId)}/tasks/${encodeURIComponent(taskId)}/handoffs`,
203
+ {
204
+ headers: getAuthHeaders(accessToken)
205
+ }
206
+ );
207
+ }
112
208
  async function listAssignedTasks(baseUrl, accessToken) {
113
209
  return requestJson(`${baseUrl}/api/my-stuff`, {
114
210
  headers: getAuthHeaders(accessToken)
@@ -134,7 +230,7 @@ async function getHello(baseUrl, accessToken) {
134
230
  var import_promises = require("fs/promises");
135
231
  var import_node_os = require("os");
136
232
  var import_node_path = require("path");
137
- var DEFAULT_BASE_URL = "http://localhost:3000";
233
+ var DEFAULT_BASE_URL = "https://fireside.khangaljargal.workers.dev";
138
234
  var legacyMigrationPromise = null;
139
235
  function normalizeBaseUrl(baseUrl) {
140
236
  return baseUrl.trim().replace(/\/+$/, "");
@@ -366,6 +462,69 @@ function openBrowser(url) {
366
462
  }
367
463
 
368
464
  // src/index.ts
465
+ function printInfo(message) {
466
+ console.log(`${import_picocolors.default.cyan("[i]")} ${message}`);
467
+ }
468
+ function printSuccess(message) {
469
+ console.log(`${import_picocolors.default.green("[ok]")} ${message}`);
470
+ }
471
+ function printWarning(message) {
472
+ console.log(`${import_picocolors.default.yellow("[!]")} ${message}`);
473
+ }
474
+ function printKeyValue(label, value) {
475
+ console.log(`${import_picocolors.default.dim(`${label}:`)} ${value}`);
476
+ }
477
+ function formatUrl(url) {
478
+ return import_picocolors.default.underline(import_picocolors.default.cyan(url));
479
+ }
480
+ function formatCode(code) {
481
+ return import_picocolors.default.bold(import_picocolors.default.magenta(code));
482
+ }
483
+ function formatUsername(username) {
484
+ return username ? import_picocolors.default.cyan(`@${username}`) : import_picocolors.default.dim("Not set");
485
+ }
486
+ function formatHeading(title, id) {
487
+ if (!id) {
488
+ return import_picocolors.default.bold(title);
489
+ }
490
+ return `${import_picocolors.default.bold(title)} ${import_picocolors.default.dim(`(${id})`)}`;
491
+ }
492
+ function formatProjectColor(color) {
493
+ const colorFormatters = {
494
+ amber: import_picocolors.default.yellow,
495
+ blue: import_picocolors.default.blue,
496
+ cyan: import_picocolors.default.cyan,
497
+ emerald: import_picocolors.default.green,
498
+ lime: import_picocolors.default.green,
499
+ pink: import_picocolors.default.magenta,
500
+ red: import_picocolors.default.red,
501
+ slate: import_picocolors.default.white,
502
+ violet: import_picocolors.default.magenta
503
+ };
504
+ const formatter = colorFormatters[color] || ((value) => value);
505
+ return `${formatter("o")} ${formatter(color)}`;
506
+ }
507
+ function formatProjectMarker(color) {
508
+ const colorFormatters = {
509
+ amber: import_picocolors.default.yellow,
510
+ blue: import_picocolors.default.blue,
511
+ cyan: import_picocolors.default.cyan,
512
+ emerald: import_picocolors.default.green,
513
+ lime: import_picocolors.default.green,
514
+ pink: import_picocolors.default.magenta,
515
+ red: import_picocolors.default.red,
516
+ slate: import_picocolors.default.white,
517
+ violet: import_picocolors.default.magenta
518
+ };
519
+ const formatter = colorFormatters[color] || ((value) => value);
520
+ return formatter("o");
521
+ }
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
+ }
369
528
  function formatUserCodeForDisplay(userCode) {
370
529
  return userCode.match(/.{1,4}/g)?.join("-") || userCode;
371
530
  }
@@ -383,15 +542,321 @@ async function requireAuthState() {
383
542
  }
384
543
  return state;
385
544
  }
545
+ function collectOptionValue(value, previous) {
546
+ return [...previous, value];
547
+ }
548
+ function normalizeLookupValue(value) {
549
+ return value.trim().toLowerCase();
550
+ }
551
+ function matchIdOrTitle(item, selector) {
552
+ const trimmedSelector = selector.trim();
553
+ if (!trimmedSelector.length) {
554
+ return false;
555
+ }
556
+ if (item.id === trimmedSelector) {
557
+ return true;
558
+ }
559
+ const normalizedSelector = normalizeLookupValue(trimmedSelector);
560
+ const normalizedTitle = normalizeLookupValue(item.title);
561
+ return normalizedTitle === normalizedSelector || normalizedTitle.includes(normalizedSelector);
562
+ }
563
+ function resolveByIdOrTitle(items, selector, itemLabel) {
564
+ const trimmedSelector = selector.trim();
565
+ if (!trimmedSelector.length) {
566
+ throw new Error(`${itemLabel} is required.`);
567
+ }
568
+ const exactIdMatch = items.find((item) => item.id === trimmedSelector);
569
+ if (exactIdMatch) {
570
+ return exactIdMatch;
571
+ }
572
+ const normalizedSelector = normalizeLookupValue(trimmedSelector);
573
+ const exactTitleMatches = items.filter(
574
+ (item) => normalizeLookupValue(item.title) === normalizedSelector
575
+ );
576
+ if (exactTitleMatches.length === 1) {
577
+ return exactTitleMatches[0];
578
+ }
579
+ if (exactTitleMatches.length > 1) {
580
+ throw new Error(
581
+ `Multiple ${itemLabel}s match "${selector}". Use the ${itemLabel} id instead.`
582
+ );
583
+ }
584
+ const partialMatches = items.filter(
585
+ (item) => normalizeLookupValue(item.title).includes(normalizedSelector)
586
+ );
587
+ if (partialMatches.length === 1) {
588
+ return partialMatches[0];
589
+ }
590
+ if (partialMatches.length > 1) {
591
+ throw new Error(
592
+ `Multiple ${itemLabel}s match "${selector}". Use the ${itemLabel} id instead.`
593
+ );
594
+ }
595
+ throw new Error(`No ${itemLabel} found for "${selector}".`);
596
+ }
597
+ function resolveBoard(boards, boardSelector) {
598
+ if (boardSelector) {
599
+ return resolveByIdOrTitle(boards, boardSelector, "board");
600
+ }
601
+ if (boards.length === 1) {
602
+ return boards[0];
603
+ }
604
+ throw new Error("This project has multiple boards. Pass `--board`.");
605
+ }
606
+ function resolveColumn(columns, columnSelector) {
607
+ if (columnSelector) {
608
+ return resolveByIdOrTitle(columns, columnSelector, "column");
609
+ }
610
+ if (columns.length === 1) {
611
+ return columns[0];
612
+ }
613
+ throw new Error("This board has multiple columns. Pass `--column`.");
614
+ }
615
+ function uniqueStrings(values) {
616
+ return [...new Set(values)];
617
+ }
618
+ function formatMember(member) {
619
+ return member.username ? `${member.name} ${import_picocolors.default.cyan(`@${member.username}`)}` : member.name;
620
+ }
621
+ function formatMemberList(members) {
622
+ if (!members.length) {
623
+ return import_picocolors.default.dim("None");
624
+ }
625
+ return members.map((member) => formatMember(member)).join(import_picocolors.default.dim(", "));
626
+ }
627
+ function compareTaskEntries(left, right) {
628
+ 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);
629
+ }
630
+ function flattenTaskEntries(projectBoardsResults) {
631
+ return projectBoardsResults.flatMap(
632
+ ({ boardsData, project }) => boardsData.boards.flatMap(
633
+ (board) => board.columns.flatMap(
634
+ (column) => column.tasks.map((task) => ({
635
+ board,
636
+ column,
637
+ members: boardsData.members,
638
+ project,
639
+ task
640
+ }))
641
+ )
642
+ )
643
+ ).sort(compareTaskEntries);
644
+ }
645
+ function serializeTaskEntry(taskEntry, contextRevision, taskHandoff) {
646
+ return {
647
+ board: {
648
+ id: taskEntry.board.id,
649
+ title: taskEntry.board.title
650
+ },
651
+ column: {
652
+ id: taskEntry.column.id,
653
+ role: taskEntry.column.role,
654
+ title: taskEntry.column.title
655
+ },
656
+ ...contextRevision !== void 0 ? { contextRevision } : {},
657
+ ...taskHandoff !== void 0 ? { handoff: taskHandoff } : {},
658
+ project: {
659
+ color: taskEntry.project.color,
660
+ id: taskEntry.project.id,
661
+ title: taskEntry.project.title
662
+ },
663
+ task: taskEntry.task
664
+ };
665
+ }
666
+ function serializeTaskHandoff(taskEntry, taskHandoff) {
667
+ return {
668
+ board: {
669
+ id: taskEntry.board.id,
670
+ title: taskEntry.board.title
671
+ },
672
+ column: {
673
+ id: taskEntry.column.id,
674
+ title: taskEntry.column.title
675
+ },
676
+ handoff: taskHandoff,
677
+ project: {
678
+ color: taskEntry.project.color,
679
+ id: taskEntry.project.id,
680
+ title: taskEntry.project.title
681
+ },
682
+ task: {
683
+ id: taskEntry.task.id,
684
+ title: taskEntry.task.title
685
+ }
686
+ };
687
+ }
688
+ function printTaskSummary(taskEntry) {
689
+ console.log(formatHeading(taskEntry.task.title, taskEntry.task.id));
690
+ console.log(
691
+ ` ${formatProjectMarker(taskEntry.project.color)} ${import_picocolors.default.cyan(taskEntry.project.title)} ${import_picocolors.default.dim(`/ ${taskEntry.board.title} / ${taskEntry.column.title}`)}`
692
+ );
693
+ if (taskEntry.task.dueDate) {
694
+ printKeyValue(" Due", import_picocolors.default.yellow(formatDueDate(taskEntry.task.dueDate)));
695
+ }
696
+ printKeyValue(" Assignees", formatMemberList(taskEntry.task.assignees));
697
+ if (taskEntry.task.description) {
698
+ console.log(` ${taskEntry.task.description}`);
699
+ }
700
+ console.log("");
701
+ }
702
+ function printTaskDetails(taskEntry, contextRevision, taskHandoff) {
703
+ console.log(formatHeading(taskEntry.task.title, taskEntry.task.id));
704
+ printKeyValue(" Project", import_picocolors.default.cyan(taskEntry.project.title));
705
+ printKeyValue(" Board", taskEntry.board.title);
706
+ printKeyValue(" Column", taskEntry.column.title);
707
+ printKeyValue(
708
+ " Due",
709
+ taskEntry.task.dueDate ? import_picocolors.default.yellow(formatDueDate(taskEntry.task.dueDate)) : import_picocolors.default.dim("None")
710
+ );
711
+ printKeyValue(" Assignees", formatMemberList(taskEntry.task.assignees));
712
+ printKeyValue(" Description", taskEntry.task.description || import_picocolors.default.dim("None"));
713
+ if (contextRevision !== void 0) {
714
+ printKeyValue(
715
+ " Task context",
716
+ contextRevision ? `v${contextRevision.version}` : import_picocolors.default.dim("None")
717
+ );
718
+ if (contextRevision?.contentMarkdown) {
719
+ console.log("");
720
+ console.log(contextRevision.contentMarkdown);
721
+ }
722
+ }
723
+ if (taskHandoff !== void 0) {
724
+ printKeyValue(
725
+ " Active handoff",
726
+ taskHandoff ? import_picocolors.default.cyan(taskHandoff.id) : import_picocolors.default.dim("None")
727
+ );
728
+ if (taskHandoff) {
729
+ printKeyValue(" Handoff status", import_picocolors.default.cyan(taskHandoff.status));
730
+ printKeyValue(" Handoff from", formatMember(taskHandoff.fromUser));
731
+ printKeyValue(" Handoff to", formatMember(taskHandoff.toUser));
732
+ printKeyValue(
733
+ " Handoff next assignee",
734
+ formatMember(taskHandoff.nextAssigneeUser)
735
+ );
736
+ printKeyValue(" Handoff summary", taskHandoff.summary);
737
+ if (taskHandoff.contextMarkdown) {
738
+ console.log("");
739
+ console.log(taskHandoff.contextMarkdown);
740
+ }
741
+ }
742
+ }
743
+ }
744
+ function printTaskHandoff(taskEntry, taskHandoff) {
745
+ console.log(formatHeading(taskEntry.task.title, taskEntry.task.id));
746
+ console.log(
747
+ ` ${formatProjectMarker(taskEntry.project.color)} ${import_picocolors.default.cyan(taskEntry.project.title)} ${import_picocolors.default.dim(`/ ${taskEntry.board.title} / ${taskEntry.column.title}`)}`
748
+ );
749
+ printKeyValue(" Handoff", taskHandoff.id);
750
+ printKeyValue(" Status", import_picocolors.default.cyan(taskHandoff.status));
751
+ printKeyValue(" From", formatMember(taskHandoff.fromUser));
752
+ printKeyValue(" To", formatMember(taskHandoff.toUser));
753
+ printKeyValue(" Next assignee", formatMember(taskHandoff.nextAssigneeUser));
754
+ console.log(` ${taskHandoff.summary}`);
755
+ }
756
+ async function readTextFile(filePath, label) {
757
+ try {
758
+ return await (0, import_promises2.readFile)(filePath, "utf8");
759
+ } catch (error) {
760
+ const message = error instanceof Error ? error.message : `Unable to read ${label} file.`;
761
+ throw new Error(`Failed to read ${label} file: ${message}`);
762
+ }
763
+ }
764
+ async function resolveTextInput(value, filePath, label) {
765
+ if (value !== void 0 && filePath !== void 0) {
766
+ throw new Error(
767
+ `Pass either \`--${label}\` or \`--${label}-file\`, not both.`
768
+ );
769
+ }
770
+ if (filePath !== void 0) {
771
+ return readTextFile(filePath, label);
772
+ }
773
+ return value;
774
+ }
775
+ async function loadCliContext(baseUrl) {
776
+ const state = await requireAuthState();
777
+ const configState = await loadConfigState();
778
+ return {
779
+ accessToken: state.accessToken,
780
+ baseUrl: await resolveBaseUrl(baseUrl, configState)
781
+ };
782
+ }
783
+ async function loadCliContextWithCurrentUser(baseUrl) {
784
+ const cliContext = await loadCliContext(baseUrl);
785
+ return {
786
+ ...cliContext,
787
+ user: await getCurrentUser(cliContext.baseUrl, cliContext.accessToken)
788
+ };
789
+ }
790
+ async function loadProjectBoardsResults(baseUrl, accessToken, projectSelector) {
791
+ const projects = await listProjects(baseUrl, accessToken);
792
+ const selectedProjects = projectSelector ? [resolveByIdOrTitle(projects, projectSelector, "project")] : projects;
793
+ return Promise.all(
794
+ selectedProjects.map(async (project) => ({
795
+ boardsData: await getProjectBoards(baseUrl, accessToken, project.id),
796
+ project
797
+ }))
798
+ );
799
+ }
800
+ async function resolveTaskEntry(baseUrl, accessToken, taskId, projectSelector) {
801
+ const taskEntries = flattenTaskEntries(
802
+ await loadProjectBoardsResults(baseUrl, accessToken, projectSelector)
803
+ ).filter((taskEntry) => taskEntry.task.id === taskId);
804
+ if (!taskEntries.length) {
805
+ throw new Error(`Task ${taskId} was not found.`);
806
+ }
807
+ return taskEntries[0];
808
+ }
809
+ function resolveProjectMember(members, selector, currentUserId) {
810
+ const trimmedSelector = selector.trim();
811
+ if (!trimmedSelector.length) {
812
+ throw new Error("Member selector is required.");
813
+ }
814
+ if (trimmedSelector === "me") {
815
+ const currentMember = members.find((member) => member.id === currentUserId);
816
+ if (!currentMember) {
817
+ throw new Error("You are not a member of this project.");
818
+ }
819
+ return currentMember;
820
+ }
821
+ const exactIdMatch = members.find((member) => member.id === trimmedSelector);
822
+ if (exactIdMatch) {
823
+ return exactIdMatch;
824
+ }
825
+ const normalizedSelector = normalizeLookupValue(
826
+ trimmedSelector.startsWith("@") ? trimmedSelector.slice(1) : trimmedSelector
827
+ );
828
+ const usernameMatch = members.find(
829
+ (member) => normalizeLookupValue(member.username || "") === normalizedSelector
830
+ );
831
+ if (usernameMatch) {
832
+ return usernameMatch;
833
+ }
834
+ const emailMatch = members.find(
835
+ (member) => normalizeLookupValue(member.email) === normalizedSelector
836
+ );
837
+ if (emailMatch) {
838
+ return emailMatch;
839
+ }
840
+ throw new Error(
841
+ `No project member found for "${selector}". Use a user id, email, @username, or \`me\`.`
842
+ );
843
+ }
844
+ function resolveProjectMemberIds(members, selectors, currentUserId) {
845
+ return uniqueStrings(
846
+ selectors.map(
847
+ (selector) => resolveProjectMember(members, selector, currentUserId).id
848
+ )
849
+ );
850
+ }
386
851
  function printProjects(projects) {
387
852
  if (!projects.length) {
388
- console.log("No projects found.");
853
+ printWarning("No projects found.");
389
854
  return;
390
855
  }
391
856
  for (const project of projects) {
392
- console.log(`${project.title} (${project.id})`);
393
- console.log(` Color: ${project.color}`);
394
- console.log(` Members: ${project.members.length}`);
857
+ console.log(formatHeading(project.title, project.id));
858
+ printKeyValue(" Color", formatProjectColor(project.color));
859
+ printKeyValue(" Members", import_picocolors.default.cyan(String(project.members.length)));
395
860
  console.log(` ${project.description}`);
396
861
  console.log("");
397
862
  }
@@ -406,49 +871,69 @@ function formatDueDate(dueDate) {
406
871
  year: "numeric"
407
872
  }).format(/* @__PURE__ */ new Date(`${dueDate}T00:00:00`));
408
873
  }
409
- function printAssignedTasks(baseUrl, tasks) {
874
+ function filterAssignedTasksByProject(tasks, projectFilter) {
875
+ const trimmedProjectFilter = projectFilter?.trim();
876
+ const normalizedProjectFilter = trimmedProjectFilter?.toLowerCase();
877
+ if (!normalizedProjectFilter) {
878
+ return tasks;
879
+ }
880
+ return tasks.filter((task) => {
881
+ const normalizedProjectTitle = task.projectTitle.toLowerCase();
882
+ return task.projectId === trimmedProjectFilter || normalizedProjectTitle === normalizedProjectFilter || normalizedProjectTitle.includes(normalizedProjectFilter);
883
+ });
884
+ }
885
+ function printAssignedTasks(tasks, projectFilter) {
410
886
  if (!tasks.length) {
411
- console.log("Nothing assigned right now.");
887
+ if (projectFilter) {
888
+ printWarning(
889
+ `No assigned tasks found for project filter: ${projectFilter}`
890
+ );
891
+ return;
892
+ }
893
+ printWarning("Nothing assigned right now.");
412
894
  return;
413
895
  }
414
896
  for (const task of tasks) {
415
- console.log(`${task.title} (${task.id})`);
897
+ console.log(formatHeading(task.title, task.id));
416
898
  console.log(
417
- ` ${task.projectTitle} / ${task.boardTitle} / ${task.columnTitle}`
899
+ ` ${formatProjectMarker(task.projectColor)} ${import_picocolors.default.cyan(task.projectTitle)} ${import_picocolors.default.dim(`/ ${task.boardTitle} / ${task.columnTitle}`)}`
418
900
  );
419
- console.log(` Due: ${formatDueDate(task.dueDate)}`);
420
- console.log(
421
- ` Assignees: ${task.assignees.map((assignee) => assignee.name).join(", ") || "None"}`
901
+ if (task.dueDate) {
902
+ printKeyValue(" Due", import_picocolors.default.yellow(formatDueDate(task.dueDate)));
903
+ }
904
+ printKeyValue(
905
+ " Assignees",
906
+ formatNames(task.assignees.map((assignee) => assignee.name))
422
907
  );
423
908
  if (task.description) {
424
909
  console.log(` ${task.description}`);
425
910
  }
426
- console.log(
427
- ` ${baseUrl}/projects/${encodeURIComponent(task.projectId)}/boards/tasks/${encodeURIComponent(task.id)}`
428
- );
429
911
  console.log("");
430
912
  }
431
913
  }
432
914
  var program = new import_commander.Command();
433
- program.name("fireside").description("Fireside CLI").version("0.0.1").showHelpAfterError();
915
+ program.name("fireside").description("Fireside CLI").version("0.0.2").configureOutput({
916
+ outputError: (message, write) => write(import_picocolors.default.red(message))
917
+ }).showHelpAfterError();
434
918
  addBaseUrlOption(
435
919
  program.command("login").description("Authenticate with Fireside using device authorization").option("--no-open", "Do not open the browser automatically").action(async (options) => {
436
920
  const configState = await loadConfigState();
437
921
  const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
438
922
  const deviceCode = await createDeviceCode(baseUrl);
439
923
  const verificationUrl = deviceCode.verification_uri;
440
- console.log(`Base URL: ${baseUrl}`);
441
- console.log(`Open this URL: ${verificationUrl}`);
442
- console.log(
443
- `Enter this code in the browser: ${formatUserCodeForDisplay(deviceCode.user_code)}`
924
+ printKeyValue("Base URL", formatUrl(baseUrl));
925
+ printKeyValue("Open", formatUrl(verificationUrl));
926
+ printKeyValue(
927
+ "Code",
928
+ formatCode(formatUserCodeForDisplay(deviceCode.user_code))
444
929
  );
445
930
  if (options.open) {
446
931
  const opened = openBrowser(verificationUrl);
447
932
  if (opened) {
448
- console.log("Opened the browser for approval.");
933
+ printSuccess("Opened the browser for approval.");
449
934
  }
450
935
  }
451
- console.log("Waiting for approval...");
936
+ printInfo("Waiting for approval...");
452
937
  const accessToken = await pollForAccessToken(
453
938
  baseUrl,
454
939
  deviceCode.device_code,
@@ -460,17 +945,20 @@ addBaseUrlOption(
460
945
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
461
946
  });
462
947
  const user = await getCurrentUser(baseUrl, accessToken);
463
- console.log(`Signed in as ${user.email}.`);
948
+ printSuccess(`Signed in as ${import_picocolors.default.bold(user.email)}.`);
949
+ if (user.username) {
950
+ printKeyValue("Username", formatUsername(user.username));
951
+ }
464
952
  })
465
953
  );
466
954
  program.command("logout").description("Remove the local CLI session").action(async () => {
467
955
  const state = await loadAuthState();
468
956
  if (!state) {
469
- console.log("Already signed out.");
957
+ printInfo("Already signed out.");
470
958
  return;
471
959
  }
472
960
  await clearAuthState();
473
- console.log("Removed the local CLI session.");
961
+ printSuccess("Removed the local CLI session.");
474
962
  });
475
963
  addBaseUrlOption(
476
964
  program.command("status").description("Show the current authenticated CLI user").action(async (options) => {
@@ -478,8 +966,11 @@ addBaseUrlOption(
478
966
  const configState = await loadConfigState();
479
967
  const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
480
968
  const user = await getCurrentUser(baseUrl, state.accessToken);
481
- console.log(`Base URL: ${baseUrl}`);
482
- console.log(`Signed in as: ${user.name} <${user.email}>`);
969
+ printKeyValue("Base URL", formatUrl(baseUrl));
970
+ printSuccess(
971
+ `Signed in as ${import_picocolors.default.bold(user.name)} <${import_picocolors.default.cyan(user.email)}>`
972
+ );
973
+ printKeyValue("Username", formatUsername(user.username));
483
974
  })
484
975
  );
485
976
  addBaseUrlOption(
@@ -488,17 +979,27 @@ addBaseUrlOption(
488
979
  const configState = await loadConfigState();
489
980
  const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
490
981
  const message = await getHello(baseUrl, state.accessToken);
491
- console.log(message);
982
+ printInfo(message);
492
983
  })
493
984
  );
494
985
  addBaseUrlOption(
495
- program.command("my-stuff").description("List tasks currently assigned to you").action(async (options) => {
496
- const state = await requireAuthState();
497
- const configState = await loadConfigState();
498
- const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
499
- const tasks = await listAssignedTasks(baseUrl, state.accessToken);
500
- printAssignedTasks(baseUrl, tasks);
501
- })
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(
987
+ async (options) => {
988
+ const state = await requireAuthState();
989
+ const configState = await loadConfigState();
990
+ const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
991
+ const tasks = await listAssignedTasks(baseUrl, state.accessToken);
992
+ const filteredTasks = filterAssignedTasksByProject(
993
+ tasks,
994
+ options.project
995
+ );
996
+ if (options.json) {
997
+ console.log(JSON.stringify(filteredTasks, null, 2));
998
+ return;
999
+ }
1000
+ printAssignedTasks(filteredTasks, options.project);
1001
+ }
1002
+ )
502
1003
  );
503
1004
  addBaseUrlOption(
504
1005
  program.command("projects").description("Interact with project APIs").command("list").description("List accessible projects").action(async function() {
@@ -512,12 +1013,326 @@ addBaseUrlOption(
512
1013
  printProjects(projects);
513
1014
  })
514
1015
  );
1016
+ var tasksCommand = program.command("tasks").description("Interact with tasks");
1017
+ 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(
1019
+ async (options) => {
1020
+ const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
1021
+ let taskEntries = flattenTaskEntries(
1022
+ await loadProjectBoardsResults(baseUrl, accessToken, options.project)
1023
+ );
1024
+ if (options.board) {
1025
+ taskEntries = taskEntries.filter(
1026
+ (taskEntry) => matchIdOrTitle(taskEntry.board, options.board)
1027
+ );
1028
+ }
1029
+ if (options.column) {
1030
+ taskEntries = taskEntries.filter(
1031
+ (taskEntry) => matchIdOrTitle(taskEntry.column, options.column)
1032
+ );
1033
+ }
1034
+ if (options.json) {
1035
+ console.log(
1036
+ JSON.stringify(
1037
+ taskEntries.map((taskEntry) => serializeTaskEntry(taskEntry)),
1038
+ null,
1039
+ 2
1040
+ )
1041
+ );
1042
+ return;
1043
+ }
1044
+ if (!taskEntries.length) {
1045
+ printWarning("No tasks found.");
1046
+ return;
1047
+ }
1048
+ for (const taskEntry of taskEntries) {
1049
+ printTaskSummary(taskEntry);
1050
+ }
1051
+ }
1052
+ )
1053
+ );
1054
+ 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(
1056
+ async (taskId, options) => {
1057
+ const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
1058
+ const taskEntry = await resolveTaskEntry(
1059
+ baseUrl,
1060
+ accessToken,
1061
+ taskId,
1062
+ options.project
1063
+ );
1064
+ const contextRevision = options.context ? await getTaskContext(
1065
+ baseUrl,
1066
+ accessToken,
1067
+ taskEntry.project.id,
1068
+ taskEntry.board.id,
1069
+ taskId
1070
+ ) : void 0;
1071
+ const taskHandoff = options.handoff ? (await listTaskHandoffs(
1072
+ baseUrl,
1073
+ accessToken,
1074
+ taskEntry.project.id,
1075
+ taskEntry.board.id,
1076
+ taskId
1077
+ )).find((handoff) => handoff.status === "active") || null : void 0;
1078
+ if (options.json) {
1079
+ console.log(
1080
+ JSON.stringify(
1081
+ serializeTaskEntry(taskEntry, contextRevision, taskHandoff),
1082
+ null,
1083
+ 2
1084
+ )
1085
+ );
1086
+ return;
1087
+ }
1088
+ printTaskDetails(taskEntry, contextRevision, taskHandoff);
1089
+ }
1090
+ )
1091
+ );
1092
+ addBaseUrlOption(
1093
+ 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
+ "-a, --assignee <member>",
1095
+ "Assign a member by id, email, @username, or me",
1096
+ collectOptionValue,
1097
+ []
1098
+ ).option("--json", "Print the created task as JSON").action(
1099
+ async (options) => {
1100
+ const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
1101
+ const [projectBoardsResult] = await loadProjectBoardsResults(
1102
+ baseUrl,
1103
+ accessToken,
1104
+ options.project
1105
+ );
1106
+ const board = resolveBoard(
1107
+ projectBoardsResult.boardsData.boards,
1108
+ options.board
1109
+ );
1110
+ const column = resolveColumn(board.columns, options.column);
1111
+ const description = await resolveTextInput(
1112
+ options.description,
1113
+ options.descriptionFile,
1114
+ "description"
1115
+ );
1116
+ const assigneeIds = resolveProjectMemberIds(
1117
+ projectBoardsResult.boardsData.members,
1118
+ options.assignee,
1119
+ user.id
1120
+ );
1121
+ const createdTask = await createTask(
1122
+ baseUrl,
1123
+ accessToken,
1124
+ projectBoardsResult.project.id,
1125
+ board.id,
1126
+ {
1127
+ assigneeIds,
1128
+ boardColumnId: column.id,
1129
+ description: description ?? "",
1130
+ dueDate: options.dueDate ?? "",
1131
+ title: options.title
1132
+ }
1133
+ );
1134
+ const taskEntry = {
1135
+ board,
1136
+ column,
1137
+ members: projectBoardsResult.boardsData.members,
1138
+ project: projectBoardsResult.project,
1139
+ task: createdTask
1140
+ };
1141
+ if (options.json) {
1142
+ console.log(JSON.stringify(serializeTaskEntry(taskEntry), null, 2));
1143
+ return;
1144
+ }
1145
+ printSuccess("Created task.");
1146
+ printTaskSummary(taskEntry);
1147
+ }
1148
+ )
1149
+ );
1150
+ addBaseUrlOption(
1151
+ 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
+ "-a, --assignee <member>",
1153
+ "Replace assignees using id, email, @username, or me",
1154
+ collectOptionValue,
1155
+ []
1156
+ ).option("--clear-assignees", "Remove all assignees").option("--json", "Print the updated task as JSON").action(
1157
+ async (taskId, options) => {
1158
+ 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) {
1159
+ throw new Error("No updates requested.");
1160
+ }
1161
+ if (options.clearDescription && (options.description !== void 0 || options.descriptionFile !== void 0)) {
1162
+ throw new Error(
1163
+ "Pass either a new description or `--clear-description`, not both."
1164
+ );
1165
+ }
1166
+ if (options.clearDueDate && options.dueDate !== void 0) {
1167
+ throw new Error(
1168
+ "Pass either `--due-date` or `--clear-due-date`, not both."
1169
+ );
1170
+ }
1171
+ if (options.clearAssignees && options.assignee.length) {
1172
+ throw new Error(
1173
+ "Pass either `--assignee` or `--clear-assignees`, not both."
1174
+ );
1175
+ }
1176
+ const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
1177
+ const taskEntry = await resolveTaskEntry(
1178
+ baseUrl,
1179
+ accessToken,
1180
+ taskId,
1181
+ options.project
1182
+ );
1183
+ const description = await resolveTextInput(
1184
+ options.description,
1185
+ options.descriptionFile,
1186
+ "description"
1187
+ );
1188
+ const assigneeIds = options.clearAssignees ? [] : options.assignee.length ? resolveProjectMemberIds(
1189
+ taskEntry.members,
1190
+ options.assignee,
1191
+ user.id
1192
+ ) : taskEntry.task.assignees.map((assignee) => assignee.id);
1193
+ const updatedTask = await updateTask(
1194
+ baseUrl,
1195
+ accessToken,
1196
+ taskEntry.project.id,
1197
+ taskEntry.board.id,
1198
+ taskId,
1199
+ {
1200
+ assigneeIds,
1201
+ description: options.clearDescription ? "" : description ?? taskEntry.task.description ?? "",
1202
+ dueDate: options.clearDueDate ? "" : options.dueDate ?? taskEntry.task.dueDate ?? "",
1203
+ title: options.title ?? taskEntry.task.title
1204
+ }
1205
+ );
1206
+ const updatedTaskEntry = {
1207
+ ...taskEntry,
1208
+ task: updatedTask
1209
+ };
1210
+ if (options.json) {
1211
+ console.log(
1212
+ JSON.stringify(serializeTaskEntry(updatedTaskEntry), null, 2)
1213
+ );
1214
+ return;
1215
+ }
1216
+ printSuccess("Updated task.");
1217
+ printTaskDetails(updatedTaskEntry);
1218
+ }
1219
+ )
1220
+ );
1221
+ addBaseUrlOption(
1222
+ 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
+ async (taskId, options) => {
1224
+ const { accessToken, baseUrl } = await loadCliContext(options.baseUrl);
1225
+ const taskEntry = await resolveTaskEntry(
1226
+ baseUrl,
1227
+ accessToken,
1228
+ taskId,
1229
+ options.project
1230
+ );
1231
+ const deletedTask = await deleteTask(
1232
+ baseUrl,
1233
+ accessToken,
1234
+ taskEntry.project.id,
1235
+ taskEntry.board.id,
1236
+ taskId
1237
+ );
1238
+ if (options.json) {
1239
+ console.log(
1240
+ JSON.stringify(
1241
+ {
1242
+ board: {
1243
+ id: taskEntry.board.id,
1244
+ title: taskEntry.board.title
1245
+ },
1246
+ column: {
1247
+ id: taskEntry.column.id,
1248
+ title: taskEntry.column.title
1249
+ },
1250
+ id: deletedTask.id,
1251
+ project: {
1252
+ id: taskEntry.project.id,
1253
+ title: taskEntry.project.title
1254
+ }
1255
+ },
1256
+ null,
1257
+ 2
1258
+ )
1259
+ );
1260
+ return;
1261
+ }
1262
+ printSuccess(
1263
+ `Deleted ${import_picocolors.default.bold(taskEntry.task.title)} ${import_picocolors.default.dim(`(${deletedTask.id})`)}.`
1264
+ );
1265
+ }
1266
+ )
1267
+ );
1268
+ var taskHandoffCommand = tasksCommand.command("handoff").description("Create and manage task handoffs");
1269
+ 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(
1271
+ async (taskId, options) => {
1272
+ const { accessToken, baseUrl, user } = await loadCliContextWithCurrentUser(options.baseUrl);
1273
+ const taskEntry = await resolveTaskEntry(
1274
+ baseUrl,
1275
+ accessToken,
1276
+ taskId,
1277
+ options.project
1278
+ );
1279
+ const contextMarkdown = await resolveTextInput(
1280
+ options.context,
1281
+ options.contextFile,
1282
+ "context"
1283
+ );
1284
+ if (contextMarkdown === void 0) {
1285
+ throw new Error(
1286
+ "Handoff context is required. Pass `--context` or `--context-file`."
1287
+ );
1288
+ }
1289
+ const toUser = resolveProjectMember(
1290
+ taskEntry.members,
1291
+ options.to,
1292
+ user.id
1293
+ );
1294
+ if (toUser.id === user.id) {
1295
+ throw new Error("Choose another project member for `--to`.");
1296
+ }
1297
+ const nextAssigneeUser = resolveProjectMember(
1298
+ taskEntry.members,
1299
+ options.next || "me",
1300
+ user.id
1301
+ );
1302
+ const taskHandoff = await createTaskHandoff(
1303
+ baseUrl,
1304
+ accessToken,
1305
+ taskEntry.project.id,
1306
+ taskEntry.board.id,
1307
+ taskId,
1308
+ {
1309
+ contextMarkdown,
1310
+ nextAssigneeUserId: nextAssigneeUser.id,
1311
+ summary: options.summary,
1312
+ toUserId: toUser.id
1313
+ }
1314
+ );
1315
+ if (options.json) {
1316
+ console.log(
1317
+ JSON.stringify(
1318
+ serializeTaskHandoff(taskEntry, taskHandoff),
1319
+ null,
1320
+ 2
1321
+ )
1322
+ );
1323
+ return;
1324
+ }
1325
+ printSuccess("Created handoff.");
1326
+ printTaskHandoff(taskEntry, taskHandoff);
1327
+ }
1328
+ )
1329
+ );
515
1330
  async function main() {
516
1331
  try {
517
1332
  await program.parseAsync(process.argv);
518
1333
  } catch (error) {
519
1334
  const message = error instanceof Error ? error.message : "Unknown CLI error.";
520
- console.error(message);
1335
+ console.error(`${import_picocolors.default.red("[x]")} ${message}`);
521
1336
  process.exitCode = 1;
522
1337
  }
523
1338
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khangal.j/fireside-cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Fireside CLI",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -26,9 +26,11 @@
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": {
33
+ "@types/node": "^25.6.0",
32
34
  "tsup": "^8.5.0"
33
35
  }
34
36
  }