@khangal.j/fireside-cli 0.0.2 → 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 +32 -1
  2. package/dist/index.js +804 -36
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -11,13 +11,44 @@ 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.
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,7 +222,7 @@ 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(/\/+$/, "");
@@ -366,6 +454,69 @@ function openBrowser(url) {
366
454
  }
367
455
 
368
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
+ }
369
520
  function formatUserCodeForDisplay(userCode) {
370
521
  return userCode.match(/.{1,4}/g)?.join("-") || userCode;
371
522
  }
@@ -383,15 +534,306 @@ async function requireAuthState() {
383
534
  }
384
535
  return state;
385
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
+ }
386
828
  function printProjects(projects) {
387
829
  if (!projects.length) {
388
- console.log("No projects found.");
830
+ printWarning("No projects found.");
389
831
  return;
390
832
  }
391
833
  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}`);
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)));
395
837
  console.log(` ${project.description}`);
396
838
  console.log("");
397
839
  }
@@ -406,49 +848,69 @@ function formatDueDate(dueDate) {
406
848
  year: "numeric"
407
849
  }).format(/* @__PURE__ */ new Date(`${dueDate}T00:00:00`));
408
850
  }
409
- 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) {
410
863
  if (!tasks.length) {
411
- 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.");
412
871
  return;
413
872
  }
414
873
  for (const task of tasks) {
415
- console.log(`${task.title} (${task.id})`);
874
+ console.log(formatHeading(task.title, task.id));
416
875
  console.log(
417
- ` ${task.projectTitle} / ${task.boardTitle} / ${task.columnTitle}`
876
+ ` ${formatProjectMarker(task.projectColor)} ${import_picocolors.default.cyan(task.projectTitle)} ${import_picocolors.default.dim(`/ ${task.boardTitle} / ${task.columnTitle}`)}`
418
877
  );
419
- console.log(` Due: ${formatDueDate(task.dueDate)}`);
420
- console.log(
421
- ` 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))
422
884
  );
423
885
  if (task.description) {
424
886
  console.log(` ${task.description}`);
425
887
  }
426
- console.log(
427
- ` ${baseUrl}/projects/${encodeURIComponent(task.projectId)}/boards/tasks/${encodeURIComponent(task.id)}`
428
- );
429
888
  console.log("");
430
889
  }
431
890
  }
432
891
  var program = new import_commander.Command();
433
- 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();
434
895
  addBaseUrlOption(
435
896
  program.command("login").description("Authenticate with Fireside using device authorization").option("--no-open", "Do not open the browser automatically").action(async (options) => {
436
897
  const configState = await loadConfigState();
437
898
  const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
438
899
  const deviceCode = await createDeviceCode(baseUrl);
439
900
  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)}`
901
+ printKeyValue("Base URL", formatUrl(baseUrl));
902
+ printKeyValue("Open", formatUrl(verificationUrl));
903
+ printKeyValue(
904
+ "Code",
905
+ formatCode(formatUserCodeForDisplay(deviceCode.user_code))
444
906
  );
445
907
  if (options.open) {
446
908
  const opened = openBrowser(verificationUrl);
447
909
  if (opened) {
448
- console.log("Opened the browser for approval.");
910
+ printSuccess("Opened the browser for approval.");
449
911
  }
450
912
  }
451
- console.log("Waiting for approval...");
913
+ printInfo("Waiting for approval...");
452
914
  const accessToken = await pollForAccessToken(
453
915
  baseUrl,
454
916
  deviceCode.device_code,
@@ -460,17 +922,20 @@ addBaseUrlOption(
460
922
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
461
923
  });
462
924
  const user = await getCurrentUser(baseUrl, accessToken);
463
- 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
+ }
464
929
  })
465
930
  );
466
931
  program.command("logout").description("Remove the local CLI session").action(async () => {
467
932
  const state = await loadAuthState();
468
933
  if (!state) {
469
- console.log("Already signed out.");
934
+ printInfo("Already signed out.");
470
935
  return;
471
936
  }
472
937
  await clearAuthState();
473
- console.log("Removed the local CLI session.");
938
+ printSuccess("Removed the local CLI session.");
474
939
  });
475
940
  addBaseUrlOption(
476
941
  program.command("status").description("Show the current authenticated CLI user").action(async (options) => {
@@ -478,8 +943,11 @@ addBaseUrlOption(
478
943
  const configState = await loadConfigState();
479
944
  const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
480
945
  const user = await getCurrentUser(baseUrl, state.accessToken);
481
- console.log(`Base URL: ${baseUrl}`);
482
- 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));
483
951
  })
484
952
  );
485
953
  addBaseUrlOption(
@@ -488,17 +956,27 @@ addBaseUrlOption(
488
956
  const configState = await loadConfigState();
489
957
  const baseUrl = await resolveBaseUrl(options.baseUrl, configState);
490
958
  const message = await getHello(baseUrl, state.accessToken);
491
- console.log(message);
959
+ printInfo(message);
492
960
  })
493
961
  );
494
962
  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
- })
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
+ )
502
980
  );
503
981
  addBaseUrlOption(
504
982
  program.command("projects").description("Interact with project APIs").command("list").description("List accessible projects").action(async function() {
@@ -512,12 +990,302 @@ addBaseUrlOption(
512
990
  printProjects(projects);
513
991
  })
514
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
+ );
515
1283
  async function main() {
516
1284
  try {
517
1285
  await program.parseAsync(process.argv);
518
1286
  } catch (error) {
519
1287
  const message = error instanceof Error ? error.message : "Unknown CLI error.";
520
- console.error(message);
1288
+ console.error(`${import_picocolors.default.red("[x]")} ${message}`);
521
1289
  process.exitCode = 1;
522
1290
  }
523
1291
  }
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.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"