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