@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.
- package/README.md +32 -1
- package/dist/index.js +804 -36
- 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
|
|
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 = "
|
|
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
|
-
|
|
830
|
+
printWarning("No projects found.");
|
|
389
831
|
return;
|
|
390
832
|
}
|
|
391
833
|
for (const project of projects) {
|
|
392
|
-
console.log(
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
874
|
+
console.log(formatHeading(task.title, task.id));
|
|
416
875
|
console.log(
|
|
417
|
-
` ${task.projectTitle}
|
|
876
|
+
` ${formatProjectMarker(task.projectColor)} ${import_picocolors.default.cyan(task.projectTitle)} ${import_picocolors.default.dim(`/ ${task.boardTitle} / ${task.columnTitle}`)}`
|
|
418
877
|
);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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.
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
910
|
+
printSuccess("Opened the browser for approval.");
|
|
449
911
|
}
|
|
450
912
|
}
|
|
451
|
-
|
|
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
|
-
|
|
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
|
-
|
|
934
|
+
printInfo("Already signed out.");
|
|
470
935
|
return;
|
|
471
936
|
}
|
|
472
937
|
await clearAuthState();
|
|
473
|
-
|
|
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
|
-
|
|
482
|
-
|
|
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
|
-
|
|
959
|
+
printInfo(message);
|
|
492
960
|
})
|
|
493
961
|
);
|
|
494
962
|
addBaseUrlOption(
|
|
495
|
-
program.command("my-stuff").description("List tasks currently assigned to you").
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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.
|
|
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"
|