@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.
- package/README.md +33 -1
- package/dist/index.js +851 -36
- 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
|
|
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 = "
|
|
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
|
-
|
|
853
|
+
printWarning("No projects found.");
|
|
389
854
|
return;
|
|
390
855
|
}
|
|
391
856
|
for (const project of projects) {
|
|
392
|
-
console.log(
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
897
|
+
console.log(formatHeading(task.title, task.id));
|
|
416
898
|
console.log(
|
|
417
|
-
` ${task.projectTitle}
|
|
899
|
+
` ${formatProjectMarker(task.projectColor)} ${import_picocolors.default.cyan(task.projectTitle)} ${import_picocolors.default.dim(`/ ${task.boardTitle} / ${task.columnTitle}`)}`
|
|
418
900
|
);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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.
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
933
|
+
printSuccess("Opened the browser for approval.");
|
|
449
934
|
}
|
|
450
935
|
}
|
|
451
|
-
|
|
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
|
-
|
|
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
|
-
|
|
957
|
+
printInfo("Already signed out.");
|
|
470
958
|
return;
|
|
471
959
|
}
|
|
472
960
|
await clearAuthState();
|
|
473
|
-
|
|
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
|
-
|
|
482
|
-
|
|
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
|
-
|
|
982
|
+
printInfo(message);
|
|
492
983
|
})
|
|
493
984
|
);
|
|
494
985
|
addBaseUrlOption(
|
|
495
|
-
program.command("my-stuff").description("List tasks currently assigned to you").
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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.
|
|
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
|
}
|