@skillcdn/console 0.1.9 → 0.1.11
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 +6 -6
- package/dist/api.d.ts +6 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +303 -18
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +11 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +87 -6
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +70 -14
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +44 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +32 -4
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +24 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +66 -5
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +54 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +156 -0
- package/dist/components/calendar.js.map +1 -0
- package/dist/components/categories.d.ts +13 -0
- package/dist/components/categories.d.ts.map +1 -0
- package/dist/components/categories.js +52 -0
- package/dist/components/categories.js.map +1 -0
- package/dist/components/decision-list.d.ts +10 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +16 -1
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +23 -7
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +35 -0
- package/dist/components/goals.d.ts.map +1 -0
- package/dist/components/goals.js +45 -0
- package/dist/components/goals.js.map +1 -0
- package/dist/components/paged.d.ts +14 -0
- package/dist/components/paged.d.ts.map +1 -0
- package/dist/components/paged.js +28 -0
- package/dist/components/paged.js.map +1 -0
- package/dist/components/projects.d.ts +22 -2
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +35 -7
- package/dist/components/projects.js.map +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +2 -2
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.d.ts +2 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +1 -1
- package/dist/components/shell.js.map +1 -1
- package/dist/components/task-form.d.ts +5 -1
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +13 -1
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.d.ts +16 -0
- package/dist/components/task-list.d.ts.map +1 -0
- package/dist/components/task-list.js +16 -0
- package/dist/components/task-list.js.map +1 -0
- package/dist/components/task-view.d.ts +9 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +10 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +30 -1
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +60 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +10 -0
- package/dist/components/work.d.ts.map +1 -0
- package/dist/components/work.js +33 -0
- package/dist/components/work.js.map +1 -0
- package/dist/console.css +658 -0
- package/dist/console.d.ts +8 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +145 -13
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +19 -3
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +51 -4
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +39 -0
- package/dist/dates.d.ts.map +1 -0
- package/dist/dates.js +120 -0
- package/dist/dates.js.map +1 -0
- package/dist/i18n/en.d.ts +172 -5
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +183 -6
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +178 -6
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +11 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +11 -0
- package/dist/limits.js.map +1 -1
- package/dist/marks.d.ts +12 -0
- package/dist/marks.d.ts.map +1 -0
- package/dist/marks.js +31 -0
- package/dist/marks.js.map +1 -0
- package/dist/router.d.ts +51 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +80 -1
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +14 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +17 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +489 -3
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +150 -9
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +21 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +31 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +17 -8
- package/docs/specs/rest.md +48 -17
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +7 -6
- package/src/api.ts +42 -0
- package/src/cli/cli.ts +362 -18
- package/src/cli/format.ts +96 -6
- package/src/cli/help.ts +77 -15
- package/src/client.ts +98 -7
- package/src/components/board.tsx +173 -13
- package/src/components/calendar.tsx +367 -0
- package/src/components/categories.tsx +209 -0
- package/src/components/decision-list.tsx +67 -5
- package/src/components/event-feed.tsx +46 -27
- package/src/components/goals.tsx +264 -0
- package/src/components/paged.tsx +39 -0
- package/src/components/projects.tsx +128 -10
- package/src/components/runs.tsx +7 -1
- package/src/components/shell.tsx +5 -2
- package/src/components/task-form.tsx +78 -1
- package/src/components/task-list.tsx +64 -0
- package/src/components/task-view.tsx +75 -4
- package/src/components/ui.tsx +116 -1
- package/src/components/work.tsx +93 -0
- package/src/console.tsx +376 -9
- package/src/data.ts +83 -12
- package/src/dates.ts +143 -0
- package/src/i18n/en.ts +193 -6
- package/src/i18n/ko.ts +184 -6
- package/src/index.ts +62 -1
- package/src/limits.ts +11 -0
- package/src/marks.ts +40 -0
- package/src/router.ts +131 -6
- package/src/routes.ts +19 -0
- package/src/schemas.ts +187 -7
- package/src/styles/console.css +658 -0
- package/src/vocabulary.ts +43 -0
package/src/cli/cli.ts
CHANGED
|
@@ -7,18 +7,30 @@ import {
|
|
|
7
7
|
type FetchLike,
|
|
8
8
|
type ProjectClient,
|
|
9
9
|
} from "../client.js";
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
10
|
+
import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
|
|
11
|
+
import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
12
|
+
import type { RestClaim, RestDecision, RestTaskPatch } from "../schemas.js";
|
|
13
|
+
import {
|
|
14
|
+
GOAL_STATES,
|
|
15
|
+
type GoalState,
|
|
16
|
+
TASK_PRIORITIES,
|
|
17
|
+
TASK_STATES,
|
|
18
|
+
type TaskPriority,
|
|
19
|
+
type TaskState,
|
|
20
|
+
} from "../vocabulary.js";
|
|
13
21
|
import { loadWebRoot, type WebRoot, WebRootError } from "../web.js";
|
|
14
22
|
import type { CredentialStore } from "./credentials.js";
|
|
15
23
|
import type { DirectoryStore } from "./directory.js";
|
|
16
24
|
import {
|
|
17
25
|
formatAnswer,
|
|
26
|
+
formatCategoryLine,
|
|
18
27
|
formatDecision,
|
|
19
28
|
formatDecisionLine,
|
|
20
29
|
formatDocument,
|
|
21
30
|
formatDocuments,
|
|
31
|
+
formatGoal,
|
|
32
|
+
formatGoalLine,
|
|
33
|
+
formatMore,
|
|
22
34
|
formatOptions,
|
|
23
35
|
formatProjectLine,
|
|
24
36
|
formatRun,
|
|
@@ -28,6 +40,7 @@ import {
|
|
|
28
40
|
formatTaskLine,
|
|
29
41
|
formatVersion,
|
|
30
42
|
formatVersionLine,
|
|
43
|
+
formatWork,
|
|
31
44
|
} from "./format.js";
|
|
32
45
|
import { COMMAND_HELP, helpFor, usage } from "./help.js";
|
|
33
46
|
import { createServeHandler, DEFAULT_SERVE_PORT, type Listen } from "./serve.js";
|
|
@@ -255,6 +268,79 @@ function priorityOf(value: string | undefined): TaskPriority | undefined {
|
|
|
255
268
|
return found;
|
|
256
269
|
}
|
|
257
270
|
|
|
271
|
+
function goalStateOf(value: string | undefined): GoalState | undefined {
|
|
272
|
+
if (value === undefined) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
const found = GOAL_STATES.find((state) => state === value);
|
|
276
|
+
if (found === undefined) {
|
|
277
|
+
throw misuse(`Not a state of a goal: ${value}. One of ${GOAL_STATES.join(", ")}`);
|
|
278
|
+
}
|
|
279
|
+
return found;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Today by this machine's clock, as a day. */
|
|
283
|
+
const todayOf = (io: CliIo): string => dayOfDate(new Date(io.now()));
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* A day as a flag takes one: `2026-10-24`, `today`, `tomorrow`, or `+<n>` days from today; and
|
|
287
|
+
* `none` where a field may be cleared, which is `null` on the wire.
|
|
288
|
+
*/
|
|
289
|
+
function dayOf(io: CliIo, value: string | undefined, clearable = false): string | null | undefined {
|
|
290
|
+
if (value === undefined) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
if (clearable && value === "none") {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
if (value === "today") {
|
|
297
|
+
return todayOf(io);
|
|
298
|
+
}
|
|
299
|
+
if (value === "tomorrow") {
|
|
300
|
+
return addDays(todayOf(io), 1);
|
|
301
|
+
}
|
|
302
|
+
const ahead = /^\+(\d{1,3})$/.exec(value);
|
|
303
|
+
if (ahead !== null) {
|
|
304
|
+
return addDays(todayOf(io), Number(ahead[1]));
|
|
305
|
+
}
|
|
306
|
+
if (!isDay(value)) {
|
|
307
|
+
throw misuse(`Not a day: ${value}. Say it as 2026-10-24, today, tomorrow, or +7`);
|
|
308
|
+
}
|
|
309
|
+
return value;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** A value a field is set to, or `none` to clear it, which is `null` on the wire. */
|
|
313
|
+
const orNone = (value: string | undefined): string | null | undefined =>
|
|
314
|
+
value === "none" ? null : value;
|
|
315
|
+
|
|
316
|
+
/** The id of the person a flag names: `me`, or a login of the workspace. */
|
|
317
|
+
async function personIdOf(
|
|
318
|
+
session: Session,
|
|
319
|
+
value: string | undefined,
|
|
320
|
+
): Promise<string | null | undefined> {
|
|
321
|
+
if (value === undefined) {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
if (value === "none") {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
if (value === "me") {
|
|
328
|
+
const me = await session.client.me();
|
|
329
|
+
if (me.person === null) {
|
|
330
|
+
throw failed("The console does not know this token. Sign in again: console login");
|
|
331
|
+
}
|
|
332
|
+
return me.person.id;
|
|
333
|
+
}
|
|
334
|
+
const { items } = await session.client.people();
|
|
335
|
+
const found = items.find((person) => person.login.toLowerCase() === value.toLowerCase());
|
|
336
|
+
if (found === undefined) {
|
|
337
|
+
throw misuse(
|
|
338
|
+
`Nobody of the workspace is called ${value}. console people is not a command; the logins are as people sign in.`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
return found.id;
|
|
342
|
+
}
|
|
343
|
+
|
|
258
344
|
function secondsOf(value: string | undefined, fallback: number): number {
|
|
259
345
|
if (value === undefined) {
|
|
260
346
|
return fallback;
|
|
@@ -266,6 +352,26 @@ function secondsOf(value: string | undefined, fallback: number): number {
|
|
|
266
352
|
return seconds;
|
|
267
353
|
}
|
|
268
354
|
|
|
355
|
+
/** The flags of a page of a listing: how many, from which one on. */
|
|
356
|
+
const PAGE_OPTIONS: OptionSpec = { limit: { type: "string" }, offset: { type: "string" } };
|
|
357
|
+
|
|
358
|
+
/** The page the flags ask for: `DEFAULT_PAGE_LIMIT` items from the first when nothing is said. */
|
|
359
|
+
function pageOf(values: Values): { readonly limit: number; readonly offset: number } {
|
|
360
|
+
const limit = text(values, "limit");
|
|
361
|
+
const offset = text(values, "offset");
|
|
362
|
+
const page = {
|
|
363
|
+
limit: limit === undefined ? DEFAULT_PAGE_LIMIT : Number(limit),
|
|
364
|
+
offset: offset === undefined ? 0 : Number(offset),
|
|
365
|
+
};
|
|
366
|
+
if (!Number.isInteger(page.limit) || page.limit < 1 || page.limit > LIST_LIMIT) {
|
|
367
|
+
throw misuse(`Not a number of items, 1 to ${LIST_LIMIT}: ${limit}`);
|
|
368
|
+
}
|
|
369
|
+
if (!Number.isInteger(page.offset) || page.offset < 0) {
|
|
370
|
+
throw misuse(`Not a number of items to skip: ${offset}`);
|
|
371
|
+
}
|
|
372
|
+
return page;
|
|
373
|
+
}
|
|
374
|
+
|
|
269
375
|
/** One positional, and nothing else. */
|
|
270
376
|
function theOne(positionals: readonly string[], ask: string): string {
|
|
271
377
|
const [first] = positionals;
|
|
@@ -452,13 +558,69 @@ const use: Command = async (args, io) => {
|
|
|
452
558
|
};
|
|
453
559
|
|
|
454
560
|
const tasks: Command = async (args, io) => {
|
|
455
|
-
const { values } = parse(
|
|
561
|
+
const { values } = parse(
|
|
562
|
+
args,
|
|
563
|
+
{
|
|
564
|
+
state: { type: "string" },
|
|
565
|
+
open: { type: "boolean" },
|
|
566
|
+
mine: { type: "boolean" },
|
|
567
|
+
category: { type: "string" },
|
|
568
|
+
goal: { type: "string" },
|
|
569
|
+
due: { type: "string" },
|
|
570
|
+
...PAGE_OPTIONS,
|
|
571
|
+
},
|
|
572
|
+
false,
|
|
573
|
+
);
|
|
456
574
|
const state = stateOf(text(values, "state"));
|
|
575
|
+
const page = pageOf(values);
|
|
457
576
|
const session = await open(io, values);
|
|
458
577
|
const project = await session.project();
|
|
459
|
-
|
|
578
|
+
// What is due: a day, or the words for the usual windows; overdue keeps only the open ones.
|
|
579
|
+
const due = text(values, "due");
|
|
580
|
+
const today = todayOf(io);
|
|
581
|
+
const dueBy =
|
|
582
|
+
due === undefined
|
|
583
|
+
? undefined
|
|
584
|
+
: due === "overdue"
|
|
585
|
+
? addDays(today, -1)
|
|
586
|
+
: due === "week"
|
|
587
|
+
? addDays(today, 6)
|
|
588
|
+
: (dayOf(io, due) ?? undefined);
|
|
589
|
+
const assignee = on(values, "mine")
|
|
590
|
+
? ((await personIdOf(session, "me")) ?? undefined)
|
|
591
|
+
: undefined;
|
|
592
|
+
const found = await project.tasks({
|
|
593
|
+
...(state === undefined ? {} : { state }),
|
|
594
|
+
...(on(values, "open") || due === "overdue" ? { open: true } : {}),
|
|
595
|
+
...(assignee === undefined ? {} : { assignee }),
|
|
596
|
+
...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
|
|
597
|
+
...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
|
|
598
|
+
...(dueBy === undefined ? {} : { dueBy }),
|
|
599
|
+
...page,
|
|
600
|
+
});
|
|
460
601
|
answer(io, session, found, () =>
|
|
461
|
-
found.items.length === 0
|
|
602
|
+
found.items.length === 0
|
|
603
|
+
? "No tasks."
|
|
604
|
+
: found.items.map(formatTaskLine).join("\n") +
|
|
605
|
+
formatMore(found.more, page.limit, page.offset),
|
|
606
|
+
);
|
|
607
|
+
return EXIT.ok;
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const work: Command = async (args, io) => {
|
|
611
|
+
const { values } = parse(args, {}, false);
|
|
612
|
+
const session = await open(io, values);
|
|
613
|
+
const found = await session.client.work();
|
|
614
|
+
answer(io, session, found, () => formatWork(found));
|
|
615
|
+
return EXIT.ok;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const categories: Command = async (args, io) => {
|
|
619
|
+
const { values } = parse(args, {}, false);
|
|
620
|
+
const session = await open(io, values);
|
|
621
|
+
const found = await session.client.categories();
|
|
622
|
+
answer(io, session, found, () =>
|
|
623
|
+
found.items.length === 0 ? "No categories." : found.items.map(formatCategoryLine).join("\n"),
|
|
462
624
|
);
|
|
463
625
|
return EXIT.ok;
|
|
464
626
|
};
|
|
@@ -472,33 +634,105 @@ const skills: Command = async (args, io) => {
|
|
|
472
634
|
return EXIT.ok;
|
|
473
635
|
};
|
|
474
636
|
|
|
637
|
+
/** The flags that set a task's fields, shared by writing one and changing one. */
|
|
638
|
+
const TASK_FIELDS: OptionSpec = {
|
|
639
|
+
state: { type: "string" },
|
|
640
|
+
priority: { type: "string" },
|
|
641
|
+
category: { type: "string" },
|
|
642
|
+
goal: { type: "string" },
|
|
643
|
+
starts: { type: "string" },
|
|
644
|
+
due: { type: "string" },
|
|
645
|
+
assignee: { type: "string" },
|
|
646
|
+
parent: { type: "string" },
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
/** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
|
|
650
|
+
async function taskFieldsOf(
|
|
651
|
+
io: CliIo,
|
|
652
|
+
session: Session,
|
|
653
|
+
project: ProjectClient,
|
|
654
|
+
values: Values,
|
|
655
|
+
): Promise<RestTaskPatch> {
|
|
656
|
+
const state = stateOf(text(values, "state"));
|
|
657
|
+
const priority = priorityOf(text(values, "priority"));
|
|
658
|
+
const category = orNone(text(values, "category"));
|
|
659
|
+
const goalId = orNone(text(values, "goal"));
|
|
660
|
+
const startsOn = dayOf(io, text(values, "starts"), true);
|
|
661
|
+
const dueOn = dayOf(io, text(values, "due"), true);
|
|
662
|
+
const assigneeId = await personIdOf(session, text(values, "assignee"));
|
|
663
|
+
const parent = text(values, "parent");
|
|
664
|
+
const parentId =
|
|
665
|
+
parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
|
|
666
|
+
return {
|
|
667
|
+
...(state === undefined ? {} : { state }),
|
|
668
|
+
...(priority === undefined ? {} : { priority }),
|
|
669
|
+
...(category === undefined ? {} : { category }),
|
|
670
|
+
...(goalId === undefined ? {} : { goalId }),
|
|
671
|
+
...(startsOn === undefined ? {} : { startsOn }),
|
|
672
|
+
...(dueOn === undefined ? {} : { dueOn }),
|
|
673
|
+
...(assigneeId === undefined ? {} : { assigneeId }),
|
|
674
|
+
...(parentId === undefined ? {} : { parentId }),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
475
678
|
const newTask: Command = async (args, io) => {
|
|
476
679
|
const { values, positionals } = parse(args, {
|
|
680
|
+
...TASK_FIELDS,
|
|
477
681
|
body: { type: "string" },
|
|
478
682
|
file: { type: "string" },
|
|
479
|
-
state: { type: "string" },
|
|
480
|
-
priority: { type: "string" },
|
|
481
683
|
});
|
|
482
684
|
const title = theOne(positionals, 'Say the title: console task new "<title>"');
|
|
483
|
-
const state = stateOf(text(values, "state"));
|
|
484
|
-
const priority = priorityOf(text(values, "priority"));
|
|
485
685
|
const body = await bodyOf(io, values, text(values, "body"));
|
|
486
686
|
const session = await open(io, values);
|
|
487
687
|
const project = await session.project();
|
|
688
|
+
const fields = await taskFieldsOf(io, session, project, values);
|
|
488
689
|
const made = await project.createTask({
|
|
489
690
|
title,
|
|
490
691
|
...(body === undefined ? {} : { body }),
|
|
491
|
-
...
|
|
492
|
-
...(priority === undefined ? {} : { priority }),
|
|
692
|
+
...fields,
|
|
493
693
|
});
|
|
494
694
|
answer(io, session, made, () => `Task #${made.number} written: ${made.title}\nid: ${made.id}`);
|
|
495
695
|
return EXIT.ok;
|
|
496
696
|
};
|
|
497
697
|
|
|
698
|
+
const editTask: Command = async (args, io) => {
|
|
699
|
+
const { values, positionals } = parse(args, {
|
|
700
|
+
...TASK_FIELDS,
|
|
701
|
+
title: { type: "string" },
|
|
702
|
+
body: { type: "string" },
|
|
703
|
+
file: { type: "string" },
|
|
704
|
+
});
|
|
705
|
+
const ref = theOne(
|
|
706
|
+
positionals,
|
|
707
|
+
"Say which task: console task edit <number|id> --due <day> [--category <key>] ...",
|
|
708
|
+
);
|
|
709
|
+
const body = await bodyOf(io, values, text(values, "body"));
|
|
710
|
+
const session = await open(io, values);
|
|
711
|
+
const project = await session.project();
|
|
712
|
+
const title = text(values, "title");
|
|
713
|
+
const patch: RestTaskPatch = {
|
|
714
|
+
...(title === undefined ? {} : { title }),
|
|
715
|
+
...(body === undefined ? {} : { body }),
|
|
716
|
+
...(await taskFieldsOf(io, session, project, values)),
|
|
717
|
+
};
|
|
718
|
+
if (Object.keys(patch).length === 0) {
|
|
719
|
+
throw misuse(
|
|
720
|
+
"Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --assignee or --parent; none clears a field.",
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
const found = await project.task(ref);
|
|
724
|
+
const changed = await project.updateTask(found.id, patch);
|
|
725
|
+
answer(io, session, changed, () => formatTask(changed));
|
|
726
|
+
return EXIT.ok;
|
|
727
|
+
};
|
|
728
|
+
|
|
498
729
|
const task: Command = async (args, io) => {
|
|
499
730
|
if (args[0] === "new") {
|
|
500
731
|
return newTask(args.slice(1), io);
|
|
501
732
|
}
|
|
733
|
+
if (args[0] === "edit") {
|
|
734
|
+
return editTask(args.slice(1), io);
|
|
735
|
+
}
|
|
502
736
|
const { values, positionals } = parse(args, {});
|
|
503
737
|
const ref = theOne(positionals, "Say which task: console task <number|id>");
|
|
504
738
|
const session = await open(io, values);
|
|
@@ -527,7 +761,7 @@ const take: Command = async (args, io) => {
|
|
|
527
761
|
});
|
|
528
762
|
answer(io, session, { run, task: found }, () =>
|
|
529
763
|
[
|
|
530
|
-
`Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.`,
|
|
764
|
+
`Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs. A run that was open on it is over now: the board cannot know whether its agent still ran, so taking it was deciding.`,
|
|
531
765
|
"",
|
|
532
766
|
formatTask(found),
|
|
533
767
|
"",
|
|
@@ -864,13 +1098,102 @@ const restore: Command = async (args, io) => {
|
|
|
864
1098
|
return EXIT.ok;
|
|
865
1099
|
};
|
|
866
1100
|
|
|
1101
|
+
const goals: Command = async (args, io) => {
|
|
1102
|
+
const { values } = parse(args, {}, false);
|
|
1103
|
+
const session = await open(io, values);
|
|
1104
|
+
const project = await session.project();
|
|
1105
|
+
const found = await project.goals();
|
|
1106
|
+
answer(io, session, found, () =>
|
|
1107
|
+
found.items.length === 0
|
|
1108
|
+
? "No goals. An owner sets one: console goal new <title> --due <day>"
|
|
1109
|
+
: found.items.map(formatGoalLine).join("\n"),
|
|
1110
|
+
);
|
|
1111
|
+
return EXIT.ok;
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
const GOAL_FIELDS: OptionSpec = {
|
|
1115
|
+
state: { type: "string" },
|
|
1116
|
+
due: { type: "string" },
|
|
1117
|
+
body: { type: "string" },
|
|
1118
|
+
file: { type: "string" },
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
const newGoal: Command = async (args, io) => {
|
|
1122
|
+
const { values, positionals } = parse(args, GOAL_FIELDS);
|
|
1123
|
+
const title = theOne(positionals, 'Say the title: console goal new "<title>" [--due <day>]');
|
|
1124
|
+
const state = goalStateOf(text(values, "state"));
|
|
1125
|
+
const dueOn = dayOf(io, text(values, "due"));
|
|
1126
|
+
const body = await bodyOf(io, values, text(values, "body"));
|
|
1127
|
+
const session = await open(io, values);
|
|
1128
|
+
const project = await session.project();
|
|
1129
|
+
const made = await project.createGoal({
|
|
1130
|
+
title,
|
|
1131
|
+
...(body === undefined ? {} : { body }),
|
|
1132
|
+
...(state === undefined ? {} : { state }),
|
|
1133
|
+
...(dueOn === undefined ? {} : { dueOn }),
|
|
1134
|
+
});
|
|
1135
|
+
answer(io, session, made, () => `Goal set: ${made.title}\nid: ${made.id}`);
|
|
1136
|
+
return EXIT.ok;
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
const editGoal: Command = async (args, io) => {
|
|
1140
|
+
const { values, positionals } = parse(args, { ...GOAL_FIELDS, title: { type: "string" } });
|
|
1141
|
+
const id = theOne(positionals, "Say which goal: console goal edit <id> --state <state> ...");
|
|
1142
|
+
const title = text(values, "title");
|
|
1143
|
+
const state = goalStateOf(text(values, "state"));
|
|
1144
|
+
const dueOn = dayOf(io, text(values, "due"), true);
|
|
1145
|
+
const body = await bodyOf(io, values, text(values, "body"));
|
|
1146
|
+
const patch = {
|
|
1147
|
+
...(title === undefined ? {} : { title }),
|
|
1148
|
+
...(body === undefined ? {} : { body }),
|
|
1149
|
+
...(state === undefined ? {} : { state }),
|
|
1150
|
+
...(dueOn === undefined ? {} : { dueOn }),
|
|
1151
|
+
};
|
|
1152
|
+
if (Object.keys(patch).length === 0) {
|
|
1153
|
+
throw misuse(
|
|
1154
|
+
"Say what to change: --title, --body, --state or --due; --due none clears the day.",
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
const session = await open(io, values);
|
|
1158
|
+
const project = await session.project();
|
|
1159
|
+
const changed = await project.updateGoal(id, patch);
|
|
1160
|
+
answer(io, session, changed, () => formatGoal(changed));
|
|
1161
|
+
return EXIT.ok;
|
|
1162
|
+
};
|
|
1163
|
+
|
|
1164
|
+
const goal: Command = async (args, io) => {
|
|
1165
|
+
if (args[0] === "new") {
|
|
1166
|
+
return newGoal(args.slice(1), io);
|
|
1167
|
+
}
|
|
1168
|
+
if (args[0] === "edit") {
|
|
1169
|
+
return editGoal(args.slice(1), io);
|
|
1170
|
+
}
|
|
1171
|
+
const { values, positionals } = parse(args, {});
|
|
1172
|
+
const id = theOne(positionals, "Say which goal: console goal <id>");
|
|
1173
|
+
const session = await open(io, values);
|
|
1174
|
+
const project = await session.project();
|
|
1175
|
+
const found = await project.goal(id);
|
|
1176
|
+
const towards = await project.tasks({ goal: found.id });
|
|
1177
|
+
answer(io, session, { goal: found, tasks: towards.items }, () =>
|
|
1178
|
+
formatGoal(found, towards.items),
|
|
1179
|
+
);
|
|
1180
|
+
return EXIT.ok;
|
|
1181
|
+
};
|
|
1182
|
+
|
|
867
1183
|
const decisions: Command = async (args, io) => {
|
|
868
|
-
const { values } = parse(args, { open: { type: "boolean" } }, false);
|
|
1184
|
+
const { values } = parse(args, { open: { type: "boolean" }, ...PAGE_OPTIONS }, false);
|
|
1185
|
+
const page = pageOf(values);
|
|
869
1186
|
const session = await open(io, values);
|
|
870
1187
|
const project = await session.project();
|
|
871
|
-
const found = await project.decisions(
|
|
1188
|
+
const found = await project.decisions({
|
|
1189
|
+
...(on(values, "open") ? { open: true } : {}),
|
|
1190
|
+
...page,
|
|
1191
|
+
});
|
|
872
1192
|
answer(io, session, found, () =>
|
|
873
|
-
found.items.length === 0
|
|
1193
|
+
found.items.length === 0
|
|
1194
|
+
? "No decisions."
|
|
1195
|
+
: found.items.map(formatDecisionLine).join("\n") +
|
|
1196
|
+
formatMore(found.more, page.limit, page.offset),
|
|
874
1197
|
);
|
|
875
1198
|
return EXIT.ok;
|
|
876
1199
|
};
|
|
@@ -923,9 +1246,15 @@ const abandon: Command = async (args, io) => {
|
|
|
923
1246
|
const runs: Command = async (args, io) => {
|
|
924
1247
|
const { values } = parse(
|
|
925
1248
|
args,
|
|
926
|
-
{
|
|
1249
|
+
{
|
|
1250
|
+
task: { type: "string" },
|
|
1251
|
+
open: { type: "boolean" },
|
|
1252
|
+
mine: { type: "boolean" },
|
|
1253
|
+
...PAGE_OPTIONS,
|
|
1254
|
+
},
|
|
927
1255
|
false,
|
|
928
1256
|
);
|
|
1257
|
+
const page = pageOf(values);
|
|
929
1258
|
const session = await open(io, values);
|
|
930
1259
|
const project = await session.project();
|
|
931
1260
|
const ref = text(values, "task");
|
|
@@ -934,9 +1263,12 @@ const runs: Command = async (args, io) => {
|
|
|
934
1263
|
...(taskId === undefined ? {} : { task: taskId }),
|
|
935
1264
|
...(on(values, "open") ? { open: true } : {}),
|
|
936
1265
|
...(on(values, "mine") ? { mine: true } : {}),
|
|
1266
|
+
...page,
|
|
937
1267
|
});
|
|
938
1268
|
answer(io, session, found, () =>
|
|
939
|
-
found.items.length === 0
|
|
1269
|
+
found.items.length === 0
|
|
1270
|
+
? "No runs."
|
|
1271
|
+
: found.items.map(formatRunLine).join("\n") + formatMore(found.more, page.limit, page.offset),
|
|
940
1272
|
);
|
|
941
1273
|
return EXIT.ok;
|
|
942
1274
|
};
|
|
@@ -1024,8 +1356,12 @@ const COMMANDS: Readonly<Record<string, Command>> = {
|
|
|
1024
1356
|
serve,
|
|
1025
1357
|
projects,
|
|
1026
1358
|
use,
|
|
1359
|
+
work,
|
|
1027
1360
|
tasks,
|
|
1028
1361
|
task,
|
|
1362
|
+
goals,
|
|
1363
|
+
goal,
|
|
1364
|
+
categories,
|
|
1029
1365
|
skills,
|
|
1030
1366
|
take,
|
|
1031
1367
|
report,
|
|
@@ -1072,6 +1408,14 @@ function hintFor(error: ApiError): string {
|
|
|
1072
1408
|
return "The page has moved on since the version you started from. Read it again, and write from there.\n";
|
|
1073
1409
|
case "document.archived":
|
|
1074
1410
|
return "The page is archived. console restore <path> brings it back first.\n";
|
|
1411
|
+
case "task.invalid_category":
|
|
1412
|
+
return "No category has that key. console categories lists them.\n";
|
|
1413
|
+
case "task.invalid_goal":
|
|
1414
|
+
return "No goal of this project has that id. console goals lists them.\n";
|
|
1415
|
+
case "task.invalid_dates":
|
|
1416
|
+
return "The due day is before the start. Say a due day on or after it.\n";
|
|
1417
|
+
case "goal.not_found":
|
|
1418
|
+
return "No goal of this project has that id. console goals lists them.\n";
|
|
1075
1419
|
case "connect.too_many":
|
|
1076
1420
|
return "The console holds as many connections as it may at once. Try again in a few minutes.\n";
|
|
1077
1421
|
case "network":
|
package/src/cli/format.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
RestArtifact,
|
|
3
|
+
RestCategory,
|
|
3
4
|
RestDecision,
|
|
4
5
|
RestDocument,
|
|
5
6
|
RestDocumentSummary,
|
|
6
7
|
RestDocuments,
|
|
8
|
+
RestGoal,
|
|
7
9
|
RestProject,
|
|
8
10
|
RestRun,
|
|
9
11
|
RestSkills,
|
|
10
12
|
RestTask,
|
|
11
13
|
RestVersion,
|
|
12
14
|
RestVersionSummary,
|
|
15
|
+
RestWork,
|
|
13
16
|
} from "../schemas.js";
|
|
14
17
|
|
|
15
18
|
// What the command prints: one line per thing in a list, a few lines for one thing in full.
|
|
@@ -36,26 +39,53 @@ export function formatProjectLine(project: RestProject): string {
|
|
|
36
39
|
if (project.openDecisions > 0) {
|
|
37
40
|
notes.push(`${plural(project.openDecisions, "decision")} waiting`);
|
|
38
41
|
}
|
|
39
|
-
if (project.openRuns > 0) {
|
|
40
|
-
notes.push(`${plural(project.openRuns, "agent")} at work`);
|
|
41
|
-
}
|
|
42
42
|
return `${project.key} ${project.name} (${notes.join("; ")})`;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/** The line under a page of a listing that has more after it: how to ask for the next page. */
|
|
46
|
+
export function formatMore(more: boolean, limit: number, offset: number): string {
|
|
47
|
+
return more ? `\n... more: --offset ${offset + limit}` : "";
|
|
48
|
+
}
|
|
49
|
+
|
|
45
50
|
export function formatTaskLine(task: RestTask): string {
|
|
46
51
|
const notes = [`owner ${task.owner.login}`];
|
|
47
52
|
if (task.assignee !== null) {
|
|
48
53
|
notes.push(`assigned to ${task.assignee.login}`);
|
|
49
54
|
}
|
|
55
|
+
if (task.category !== null) {
|
|
56
|
+
notes.push(task.category.key);
|
|
57
|
+
}
|
|
58
|
+
if (task.dueOn !== null) {
|
|
59
|
+
notes.push(`due ${task.dueOn}`);
|
|
60
|
+
}
|
|
50
61
|
if (task.openDecisions > 0) {
|
|
51
62
|
notes.push(`${plural(task.openDecisions, "decision")} waiting`);
|
|
52
63
|
}
|
|
53
|
-
if (task.openRuns > 0) {
|
|
54
|
-
notes.push("an agent at work");
|
|
55
|
-
}
|
|
56
64
|
return `#${task.number} ${task.state} ${task.priority} ${task.title} (${notes.join("; ")})`;
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
/** The plan and what happened, as a line each, only where there is something to say. */
|
|
68
|
+
function scheduleLines(task: RestTask): string[] {
|
|
69
|
+
const lines: string[] = [];
|
|
70
|
+
const plan = [
|
|
71
|
+
task.category === null ? undefined : `category: ${task.category.key}`,
|
|
72
|
+
task.goalId === null ? undefined : `goal: ${task.goalId}`,
|
|
73
|
+
task.startsOn === null ? undefined : `starts: ${task.startsOn}`,
|
|
74
|
+
task.dueOn === null ? undefined : `due: ${task.dueOn}`,
|
|
75
|
+
].filter((part) => part !== undefined);
|
|
76
|
+
if (plan.length > 0) {
|
|
77
|
+
lines.push(plan.join(" "));
|
|
78
|
+
}
|
|
79
|
+
const happened = [
|
|
80
|
+
task.startedAt === null ? undefined : `started: ${task.startedAt}`,
|
|
81
|
+
task.doneAt === null ? undefined : `done: ${task.doneAt}`,
|
|
82
|
+
].filter((part) => part !== undefined);
|
|
83
|
+
if (happened.length > 0) {
|
|
84
|
+
lines.push(happened.join(" "));
|
|
85
|
+
}
|
|
86
|
+
return lines;
|
|
87
|
+
}
|
|
88
|
+
|
|
59
89
|
/** One task in full; with the decisions about it and the runs on it, when given. */
|
|
60
90
|
export function formatTask(
|
|
61
91
|
task: RestTask,
|
|
@@ -66,6 +96,7 @@ export function formatTask(
|
|
|
66
96
|
`#${task.number} ${task.title}`,
|
|
67
97
|
`state: ${task.state} priority: ${task.priority} owner: ${task.owner.login} assignee: ${task.assignee?.login ?? "nobody"}`,
|
|
68
98
|
`id: ${task.id}${task.parentId === null ? "" : ` part of: ${task.parentId}`}`,
|
|
99
|
+
...scheduleLines(task),
|
|
69
100
|
];
|
|
70
101
|
if (task.links.length > 0) {
|
|
71
102
|
lines.push("links:");
|
|
@@ -96,6 +127,9 @@ export function formatRunLine(run: RestRun): string {
|
|
|
96
127
|
if (run.waitingFor !== null) {
|
|
97
128
|
notes.push(`waiting for decision ${run.waitingFor}`);
|
|
98
129
|
}
|
|
130
|
+
if (run.endedAt === null) {
|
|
131
|
+
notes.push(`last heard ${run.lastHeardAt}`);
|
|
132
|
+
}
|
|
99
133
|
return `${run.id} ${run.status} ${run.agent} for ${run.person.login} task #${run.taskNumber} (${notes.join(", ")})`;
|
|
100
134
|
}
|
|
101
135
|
|
|
@@ -165,6 +199,62 @@ export function formatSkills(skills: RestSkills): string {
|
|
|
165
199
|
return lines.join("\n");
|
|
166
200
|
}
|
|
167
201
|
|
|
202
|
+
/** How far a goal is, from its tasks: the done, the at work, and the rest, in one phrase. */
|
|
203
|
+
function progressOf(goal: RestGoal): string {
|
|
204
|
+
const counts = goal.taskCounts;
|
|
205
|
+
const total = counts.idea + counts.ready + counts.in_progress + counts.in_review + counts.done;
|
|
206
|
+
const working = counts.in_progress + counts.in_review;
|
|
207
|
+
return `${counts.done}/${total} done${working === 0 ? "" : `, ${working} in progress`}${counts.dropped === 0 ? "" : `, ${counts.dropped} dropped`}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** One goal: its id, its state, its title, when it is due, and how far it is. */
|
|
211
|
+
export function formatGoalLine(goal: RestGoal): string {
|
|
212
|
+
const notes = [goal.dueOn === null ? "no target day" : `due ${goal.dueOn}`, progressOf(goal)];
|
|
213
|
+
return `${goal.id} ${goal.state} ${goal.title} (${notes.join("; ")})`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** One goal in full; with the tasks that work towards it, when given. */
|
|
217
|
+
export function formatGoal(goal: RestGoal, tasks?: readonly RestTask[]): string {
|
|
218
|
+
const lines = [
|
|
219
|
+
`goal ${goal.id}: ${goal.title}`,
|
|
220
|
+
`state: ${goal.state} due: ${goal.dueOn ?? "none"} set by: ${goal.createdBy.login}`,
|
|
221
|
+
`progress: ${progressOf(goal)}`,
|
|
222
|
+
];
|
|
223
|
+
if (goal.body.trim().length > 0) {
|
|
224
|
+
lines.push("", goal.body.trimEnd());
|
|
225
|
+
}
|
|
226
|
+
if (tasks !== undefined) {
|
|
227
|
+
lines.push("", tasks.length === 0 ? "tasks: none" : "tasks:");
|
|
228
|
+
for (const task of tasks) {
|
|
229
|
+
lines.push(` ${formatTaskLine(task)}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return lines.join("\n");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** One category: its key, what people call it, and its colour. */
|
|
236
|
+
export function formatCategoryLine(category: RestCategory): string {
|
|
237
|
+
return `${category.key} ${category.name} (${category.color})`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** What is on a person's plate: their open tasks by project, and the decisions waiting. */
|
|
241
|
+
export function formatWork(work: RestWork): string {
|
|
242
|
+
const lines: string[] = [];
|
|
243
|
+
if (work.tasks.length === 0) {
|
|
244
|
+
lines.push("No open task is assigned to you.");
|
|
245
|
+
} else {
|
|
246
|
+
lines.push("tasks assigned to you:");
|
|
247
|
+
for (const { project, task } of work.tasks) {
|
|
248
|
+
lines.push(` ${project.key} ${formatTaskLine(task)}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
lines.push("", work.decisions.length === 0 ? "decisions waiting: none" : "decisions waiting:");
|
|
252
|
+
for (const { project, decision } of work.decisions) {
|
|
253
|
+
lines.push(` ${project.key} ${formatDecisionLine(decision)}`);
|
|
254
|
+
}
|
|
255
|
+
return lines.join("\n");
|
|
256
|
+
}
|
|
257
|
+
|
|
168
258
|
export function formatDecisionLine(decision: RestDecision): string {
|
|
169
259
|
const state = decision.answer === null ? "waiting" : `answered ${decision.answer.option}`;
|
|
170
260
|
return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)})`;
|