@skillcdn/console 0.1.9 → 0.1.10
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 +5 -5
- package/dist/api.d.ts +5 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +4 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +255 -10
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +9 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +80 -0
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +62 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/client.d.ts +31 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +19 -3
- package/dist/client.js.map +1 -1
- package/dist/components/board.d.ts +27 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +75 -7
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +34 -0
- package/dist/components/calendar.d.ts.map +1 -0
- package/dist/components/calendar.js +96 -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 +14 -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 +12 -0
- 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/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 +14 -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 +29 -0
- package/dist/components/work.js.map +1 -0
- package/dist/console.css +511 -0
- package/dist/console.d.ts +8 -0
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +124 -8
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +14 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +36 -2
- 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 +162 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +174 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +169 -1
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +5 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +5 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +50 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +74 -1
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +7 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +9 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +366 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +112 -3
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +15 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +26 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +11 -4
- package/docs/specs/rest.md +33 -8
- package/package.json +1 -1
- package/skills/working-the-board/SKILL.md +7 -6
- package/src/api.ts +30 -0
- package/src/cli/cli.ts +311 -10
- package/src/cli/format.ts +88 -0
- package/src/cli/help.ts +69 -7
- package/src/client.ts +70 -2
- package/src/components/board.tsx +205 -12
- package/src/components/calendar.tsx +255 -0
- package/src/components/categories.tsx +209 -0
- package/src/components/decision-list.tsx +62 -5
- package/src/components/event-feed.tsx +12 -0
- package/src/components/goals.tsx +264 -0
- package/src/components/task-form.tsx +78 -1
- package/src/components/task-list.tsx +61 -0
- package/src/components/task-view.tsx +74 -3
- package/src/components/ui.tsx +116 -1
- package/src/components/work.tsx +83 -0
- package/src/console.tsx +337 -4
- package/src/data.ts +63 -8
- package/src/dates.ts +143 -0
- package/src/i18n/en.ts +184 -1
- package/src/i18n/ko.ts +175 -1
- package/src/index.ts +58 -1
- package/src/limits.ts +5 -0
- package/src/router.ts +119 -6
- package/src/routes.ts +10 -0
- package/src/schemas.ts +141 -1
- package/src/styles/console.css +511 -0
- package/src/vocabulary.ts +33 -0
package/src/cli/cli.ts
CHANGED
|
@@ -7,18 +7,29 @@ import {
|
|
|
7
7
|
type FetchLike,
|
|
8
8
|
type ProjectClient,
|
|
9
9
|
} from "../client.js";
|
|
10
|
+
import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
|
|
10
11
|
import { MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
11
|
-
import type { RestClaim, RestDecision } from "../schemas.js";
|
|
12
|
-
import {
|
|
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,
|
|
22
33
|
formatOptions,
|
|
23
34
|
formatProjectLine,
|
|
24
35
|
formatRun,
|
|
@@ -28,6 +39,7 @@ import {
|
|
|
28
39
|
formatTaskLine,
|
|
29
40
|
formatVersion,
|
|
30
41
|
formatVersionLine,
|
|
42
|
+
formatWork,
|
|
31
43
|
} from "./format.js";
|
|
32
44
|
import { COMMAND_HELP, helpFor, usage } from "./help.js";
|
|
33
45
|
import { createServeHandler, DEFAULT_SERVE_PORT, type Listen } from "./serve.js";
|
|
@@ -255,6 +267,79 @@ function priorityOf(value: string | undefined): TaskPriority | undefined {
|
|
|
255
267
|
return found;
|
|
256
268
|
}
|
|
257
269
|
|
|
270
|
+
function goalStateOf(value: string | undefined): GoalState | undefined {
|
|
271
|
+
if (value === undefined) {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
const found = GOAL_STATES.find((state) => state === value);
|
|
275
|
+
if (found === undefined) {
|
|
276
|
+
throw misuse(`Not a state of a goal: ${value}. One of ${GOAL_STATES.join(", ")}`);
|
|
277
|
+
}
|
|
278
|
+
return found;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Today by this machine's clock, as a day. */
|
|
282
|
+
const todayOf = (io: CliIo): string => dayOfDate(new Date(io.now()));
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* A day as a flag takes one: `2026-10-24`, `today`, `tomorrow`, or `+<n>` days from today; and
|
|
286
|
+
* `none` where a field may be cleared, which is `null` on the wire.
|
|
287
|
+
*/
|
|
288
|
+
function dayOf(io: CliIo, value: string | undefined, clearable = false): string | null | undefined {
|
|
289
|
+
if (value === undefined) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
if (clearable && value === "none") {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
if (value === "today") {
|
|
296
|
+
return todayOf(io);
|
|
297
|
+
}
|
|
298
|
+
if (value === "tomorrow") {
|
|
299
|
+
return addDays(todayOf(io), 1);
|
|
300
|
+
}
|
|
301
|
+
const ahead = /^\+(\d{1,3})$/.exec(value);
|
|
302
|
+
if (ahead !== null) {
|
|
303
|
+
return addDays(todayOf(io), Number(ahead[1]));
|
|
304
|
+
}
|
|
305
|
+
if (!isDay(value)) {
|
|
306
|
+
throw misuse(`Not a day: ${value}. Say it as 2026-10-24, today, tomorrow, or +7`);
|
|
307
|
+
}
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** A value a field is set to, or `none` to clear it, which is `null` on the wire. */
|
|
312
|
+
const orNone = (value: string | undefined): string | null | undefined =>
|
|
313
|
+
value === "none" ? null : value;
|
|
314
|
+
|
|
315
|
+
/** The id of the person a flag names: `me`, or a login of the workspace. */
|
|
316
|
+
async function personIdOf(
|
|
317
|
+
session: Session,
|
|
318
|
+
value: string | undefined,
|
|
319
|
+
): Promise<string | null | undefined> {
|
|
320
|
+
if (value === undefined) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
if (value === "none") {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
if (value === "me") {
|
|
327
|
+
const me = await session.client.me();
|
|
328
|
+
if (me.person === null) {
|
|
329
|
+
throw failed("The console does not know this token. Sign in again: console login");
|
|
330
|
+
}
|
|
331
|
+
return me.person.id;
|
|
332
|
+
}
|
|
333
|
+
const { items } = await session.client.people();
|
|
334
|
+
const found = items.find((person) => person.login.toLowerCase() === value.toLowerCase());
|
|
335
|
+
if (found === undefined) {
|
|
336
|
+
throw misuse(
|
|
337
|
+
`Nobody of the workspace is called ${value}. console people is not a command; the logins are as people sign in.`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
return found.id;
|
|
341
|
+
}
|
|
342
|
+
|
|
258
343
|
function secondsOf(value: string | undefined, fallback: number): number {
|
|
259
344
|
if (value === undefined) {
|
|
260
345
|
return fallback;
|
|
@@ -452,17 +537,67 @@ const use: Command = async (args, io) => {
|
|
|
452
537
|
};
|
|
453
538
|
|
|
454
539
|
const tasks: Command = async (args, io) => {
|
|
455
|
-
const { values } = parse(
|
|
540
|
+
const { values } = parse(
|
|
541
|
+
args,
|
|
542
|
+
{
|
|
543
|
+
state: { type: "string" },
|
|
544
|
+
open: { type: "boolean" },
|
|
545
|
+
mine: { type: "boolean" },
|
|
546
|
+
category: { type: "string" },
|
|
547
|
+
goal: { type: "string" },
|
|
548
|
+
due: { type: "string" },
|
|
549
|
+
},
|
|
550
|
+
false,
|
|
551
|
+
);
|
|
456
552
|
const state = stateOf(text(values, "state"));
|
|
457
553
|
const session = await open(io, values);
|
|
458
554
|
const project = await session.project();
|
|
459
|
-
|
|
555
|
+
// What is due: a day, or the words for the usual windows; overdue keeps only the open ones.
|
|
556
|
+
const due = text(values, "due");
|
|
557
|
+
const today = todayOf(io);
|
|
558
|
+
const dueBy =
|
|
559
|
+
due === undefined
|
|
560
|
+
? undefined
|
|
561
|
+
: due === "overdue"
|
|
562
|
+
? addDays(today, -1)
|
|
563
|
+
: due === "week"
|
|
564
|
+
? addDays(today, 6)
|
|
565
|
+
: (dayOf(io, due) ?? undefined);
|
|
566
|
+
const assignee = on(values, "mine")
|
|
567
|
+
? ((await personIdOf(session, "me")) ?? undefined)
|
|
568
|
+
: undefined;
|
|
569
|
+
const found = await project.tasks({
|
|
570
|
+
...(state === undefined ? {} : { state }),
|
|
571
|
+
...(on(values, "open") || due === "overdue" ? { open: true } : {}),
|
|
572
|
+
...(assignee === undefined ? {} : { assignee }),
|
|
573
|
+
...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
|
|
574
|
+
...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
|
|
575
|
+
...(dueBy === undefined ? {} : { dueBy }),
|
|
576
|
+
});
|
|
460
577
|
answer(io, session, found, () =>
|
|
461
578
|
found.items.length === 0 ? "No tasks." : found.items.map(formatTaskLine).join("\n"),
|
|
462
579
|
);
|
|
463
580
|
return EXIT.ok;
|
|
464
581
|
};
|
|
465
582
|
|
|
583
|
+
const work: Command = async (args, io) => {
|
|
584
|
+
const { values } = parse(args, {}, false);
|
|
585
|
+
const session = await open(io, values);
|
|
586
|
+
const found = await session.client.work();
|
|
587
|
+
answer(io, session, found, () => formatWork(found));
|
|
588
|
+
return EXIT.ok;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const categories: Command = async (args, io) => {
|
|
592
|
+
const { values } = parse(args, {}, false);
|
|
593
|
+
const session = await open(io, values);
|
|
594
|
+
const found = await session.client.categories();
|
|
595
|
+
answer(io, session, found, () =>
|
|
596
|
+
found.items.length === 0 ? "No categories." : found.items.map(formatCategoryLine).join("\n"),
|
|
597
|
+
);
|
|
598
|
+
return EXIT.ok;
|
|
599
|
+
};
|
|
600
|
+
|
|
466
601
|
const skills: Command = async (args, io) => {
|
|
467
602
|
const { values } = parse(args, {}, false);
|
|
468
603
|
const session = await open(io, values);
|
|
@@ -472,33 +607,105 @@ const skills: Command = async (args, io) => {
|
|
|
472
607
|
return EXIT.ok;
|
|
473
608
|
};
|
|
474
609
|
|
|
610
|
+
/** The flags that set a task's fields, shared by writing one and changing one. */
|
|
611
|
+
const TASK_FIELDS: OptionSpec = {
|
|
612
|
+
state: { type: "string" },
|
|
613
|
+
priority: { type: "string" },
|
|
614
|
+
category: { type: "string" },
|
|
615
|
+
goal: { type: "string" },
|
|
616
|
+
starts: { type: "string" },
|
|
617
|
+
due: { type: "string" },
|
|
618
|
+
assignee: { type: "string" },
|
|
619
|
+
parent: { type: "string" },
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
/** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
|
|
623
|
+
async function taskFieldsOf(
|
|
624
|
+
io: CliIo,
|
|
625
|
+
session: Session,
|
|
626
|
+
project: ProjectClient,
|
|
627
|
+
values: Values,
|
|
628
|
+
): Promise<RestTaskPatch> {
|
|
629
|
+
const state = stateOf(text(values, "state"));
|
|
630
|
+
const priority = priorityOf(text(values, "priority"));
|
|
631
|
+
const category = orNone(text(values, "category"));
|
|
632
|
+
const goalId = orNone(text(values, "goal"));
|
|
633
|
+
const startsOn = dayOf(io, text(values, "starts"), true);
|
|
634
|
+
const dueOn = dayOf(io, text(values, "due"), true);
|
|
635
|
+
const assigneeId = await personIdOf(session, text(values, "assignee"));
|
|
636
|
+
const parent = text(values, "parent");
|
|
637
|
+
const parentId =
|
|
638
|
+
parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
|
|
639
|
+
return {
|
|
640
|
+
...(state === undefined ? {} : { state }),
|
|
641
|
+
...(priority === undefined ? {} : { priority }),
|
|
642
|
+
...(category === undefined ? {} : { category }),
|
|
643
|
+
...(goalId === undefined ? {} : { goalId }),
|
|
644
|
+
...(startsOn === undefined ? {} : { startsOn }),
|
|
645
|
+
...(dueOn === undefined ? {} : { dueOn }),
|
|
646
|
+
...(assigneeId === undefined ? {} : { assigneeId }),
|
|
647
|
+
...(parentId === undefined ? {} : { parentId }),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
475
651
|
const newTask: Command = async (args, io) => {
|
|
476
652
|
const { values, positionals } = parse(args, {
|
|
653
|
+
...TASK_FIELDS,
|
|
477
654
|
body: { type: "string" },
|
|
478
655
|
file: { type: "string" },
|
|
479
|
-
state: { type: "string" },
|
|
480
|
-
priority: { type: "string" },
|
|
481
656
|
});
|
|
482
657
|
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
658
|
const body = await bodyOf(io, values, text(values, "body"));
|
|
486
659
|
const session = await open(io, values);
|
|
487
660
|
const project = await session.project();
|
|
661
|
+
const fields = await taskFieldsOf(io, session, project, values);
|
|
488
662
|
const made = await project.createTask({
|
|
489
663
|
title,
|
|
490
664
|
...(body === undefined ? {} : { body }),
|
|
491
|
-
...
|
|
492
|
-
...(priority === undefined ? {} : { priority }),
|
|
665
|
+
...fields,
|
|
493
666
|
});
|
|
494
667
|
answer(io, session, made, () => `Task #${made.number} written: ${made.title}\nid: ${made.id}`);
|
|
495
668
|
return EXIT.ok;
|
|
496
669
|
};
|
|
497
670
|
|
|
671
|
+
const editTask: Command = async (args, io) => {
|
|
672
|
+
const { values, positionals } = parse(args, {
|
|
673
|
+
...TASK_FIELDS,
|
|
674
|
+
title: { type: "string" },
|
|
675
|
+
body: { type: "string" },
|
|
676
|
+
file: { type: "string" },
|
|
677
|
+
});
|
|
678
|
+
const ref = theOne(
|
|
679
|
+
positionals,
|
|
680
|
+
"Say which task: console task edit <number|id> --due <day> [--category <key>] ...",
|
|
681
|
+
);
|
|
682
|
+
const body = await bodyOf(io, values, text(values, "body"));
|
|
683
|
+
const session = await open(io, values);
|
|
684
|
+
const project = await session.project();
|
|
685
|
+
const title = text(values, "title");
|
|
686
|
+
const patch: RestTaskPatch = {
|
|
687
|
+
...(title === undefined ? {} : { title }),
|
|
688
|
+
...(body === undefined ? {} : { body }),
|
|
689
|
+
...(await taskFieldsOf(io, session, project, values)),
|
|
690
|
+
};
|
|
691
|
+
if (Object.keys(patch).length === 0) {
|
|
692
|
+
throw misuse(
|
|
693
|
+
"Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --assignee or --parent; none clears a field.",
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
const found = await project.task(ref);
|
|
697
|
+
const changed = await project.updateTask(found.id, patch);
|
|
698
|
+
answer(io, session, changed, () => formatTask(changed));
|
|
699
|
+
return EXIT.ok;
|
|
700
|
+
};
|
|
701
|
+
|
|
498
702
|
const task: Command = async (args, io) => {
|
|
499
703
|
if (args[0] === "new") {
|
|
500
704
|
return newTask(args.slice(1), io);
|
|
501
705
|
}
|
|
706
|
+
if (args[0] === "edit") {
|
|
707
|
+
return editTask(args.slice(1), io);
|
|
708
|
+
}
|
|
502
709
|
const { values, positionals } = parse(args, {});
|
|
503
710
|
const ref = theOne(positionals, "Say which task: console task <number|id>");
|
|
504
711
|
const session = await open(io, values);
|
|
@@ -864,6 +1071,88 @@ const restore: Command = async (args, io) => {
|
|
|
864
1071
|
return EXIT.ok;
|
|
865
1072
|
};
|
|
866
1073
|
|
|
1074
|
+
const goals: Command = async (args, io) => {
|
|
1075
|
+
const { values } = parse(args, {}, false);
|
|
1076
|
+
const session = await open(io, values);
|
|
1077
|
+
const project = await session.project();
|
|
1078
|
+
const found = await project.goals();
|
|
1079
|
+
answer(io, session, found, () =>
|
|
1080
|
+
found.items.length === 0
|
|
1081
|
+
? "No goals. An owner sets one: console goal new <title> --due <day>"
|
|
1082
|
+
: found.items.map(formatGoalLine).join("\n"),
|
|
1083
|
+
);
|
|
1084
|
+
return EXIT.ok;
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
const GOAL_FIELDS: OptionSpec = {
|
|
1088
|
+
state: { type: "string" },
|
|
1089
|
+
due: { type: "string" },
|
|
1090
|
+
body: { type: "string" },
|
|
1091
|
+
file: { type: "string" },
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
const newGoal: Command = async (args, io) => {
|
|
1095
|
+
const { values, positionals } = parse(args, GOAL_FIELDS);
|
|
1096
|
+
const title = theOne(positionals, 'Say the title: console goal new "<title>" [--due <day>]');
|
|
1097
|
+
const state = goalStateOf(text(values, "state"));
|
|
1098
|
+
const dueOn = dayOf(io, text(values, "due"));
|
|
1099
|
+
const body = await bodyOf(io, values, text(values, "body"));
|
|
1100
|
+
const session = await open(io, values);
|
|
1101
|
+
const project = await session.project();
|
|
1102
|
+
const made = await project.createGoal({
|
|
1103
|
+
title,
|
|
1104
|
+
...(body === undefined ? {} : { body }),
|
|
1105
|
+
...(state === undefined ? {} : { state }),
|
|
1106
|
+
...(dueOn === undefined ? {} : { dueOn }),
|
|
1107
|
+
});
|
|
1108
|
+
answer(io, session, made, () => `Goal set: ${made.title}\nid: ${made.id}`);
|
|
1109
|
+
return EXIT.ok;
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
const editGoal: Command = async (args, io) => {
|
|
1113
|
+
const { values, positionals } = parse(args, { ...GOAL_FIELDS, title: { type: "string" } });
|
|
1114
|
+
const id = theOne(positionals, "Say which goal: console goal edit <id> --state <state> ...");
|
|
1115
|
+
const title = text(values, "title");
|
|
1116
|
+
const state = goalStateOf(text(values, "state"));
|
|
1117
|
+
const dueOn = dayOf(io, text(values, "due"), true);
|
|
1118
|
+
const body = await bodyOf(io, values, text(values, "body"));
|
|
1119
|
+
const patch = {
|
|
1120
|
+
...(title === undefined ? {} : { title }),
|
|
1121
|
+
...(body === undefined ? {} : { body }),
|
|
1122
|
+
...(state === undefined ? {} : { state }),
|
|
1123
|
+
...(dueOn === undefined ? {} : { dueOn }),
|
|
1124
|
+
};
|
|
1125
|
+
if (Object.keys(patch).length === 0) {
|
|
1126
|
+
throw misuse(
|
|
1127
|
+
"Say what to change: --title, --body, --state or --due; --due none clears the day.",
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
const session = await open(io, values);
|
|
1131
|
+
const project = await session.project();
|
|
1132
|
+
const changed = await project.updateGoal(id, patch);
|
|
1133
|
+
answer(io, session, changed, () => formatGoal(changed));
|
|
1134
|
+
return EXIT.ok;
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const goal: Command = async (args, io) => {
|
|
1138
|
+
if (args[0] === "new") {
|
|
1139
|
+
return newGoal(args.slice(1), io);
|
|
1140
|
+
}
|
|
1141
|
+
if (args[0] === "edit") {
|
|
1142
|
+
return editGoal(args.slice(1), io);
|
|
1143
|
+
}
|
|
1144
|
+
const { values, positionals } = parse(args, {});
|
|
1145
|
+
const id = theOne(positionals, "Say which goal: console goal <id>");
|
|
1146
|
+
const session = await open(io, values);
|
|
1147
|
+
const project = await session.project();
|
|
1148
|
+
const found = await project.goal(id);
|
|
1149
|
+
const towards = await project.tasks({ goal: found.id });
|
|
1150
|
+
answer(io, session, { goal: found, tasks: towards.items }, () =>
|
|
1151
|
+
formatGoal(found, towards.items),
|
|
1152
|
+
);
|
|
1153
|
+
return EXIT.ok;
|
|
1154
|
+
};
|
|
1155
|
+
|
|
867
1156
|
const decisions: Command = async (args, io) => {
|
|
868
1157
|
const { values } = parse(args, { open: { type: "boolean" } }, false);
|
|
869
1158
|
const session = await open(io, values);
|
|
@@ -1024,8 +1313,12 @@ const COMMANDS: Readonly<Record<string, Command>> = {
|
|
|
1024
1313
|
serve,
|
|
1025
1314
|
projects,
|
|
1026
1315
|
use,
|
|
1316
|
+
work,
|
|
1027
1317
|
tasks,
|
|
1028
1318
|
task,
|
|
1319
|
+
goals,
|
|
1320
|
+
goal,
|
|
1321
|
+
categories,
|
|
1029
1322
|
skills,
|
|
1030
1323
|
take,
|
|
1031
1324
|
report,
|
|
@@ -1072,6 +1365,14 @@ function hintFor(error: ApiError): string {
|
|
|
1072
1365
|
return "The page has moved on since the version you started from. Read it again, and write from there.\n";
|
|
1073
1366
|
case "document.archived":
|
|
1074
1367
|
return "The page is archived. console restore <path> brings it back first.\n";
|
|
1368
|
+
case "task.invalid_category":
|
|
1369
|
+
return "No category has that key. console categories lists them.\n";
|
|
1370
|
+
case "task.invalid_goal":
|
|
1371
|
+
return "No goal of this project has that id. console goals lists them.\n";
|
|
1372
|
+
case "task.invalid_dates":
|
|
1373
|
+
return "The due day is before the start. Say a due day on or after it.\n";
|
|
1374
|
+
case "goal.not_found":
|
|
1375
|
+
return "No goal of this project has that id. console goals lists them.\n";
|
|
1075
1376
|
case "connect.too_many":
|
|
1076
1377
|
return "The console holds as many connections as it may at once. Try again in a few minutes.\n";
|
|
1077
1378
|
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.
|
|
@@ -47,6 +50,12 @@ export function formatTaskLine(task: RestTask): string {
|
|
|
47
50
|
if (task.assignee !== null) {
|
|
48
51
|
notes.push(`assigned to ${task.assignee.login}`);
|
|
49
52
|
}
|
|
53
|
+
if (task.category !== null) {
|
|
54
|
+
notes.push(task.category.key);
|
|
55
|
+
}
|
|
56
|
+
if (task.dueOn !== null) {
|
|
57
|
+
notes.push(`due ${task.dueOn}`);
|
|
58
|
+
}
|
|
50
59
|
if (task.openDecisions > 0) {
|
|
51
60
|
notes.push(`${plural(task.openDecisions, "decision")} waiting`);
|
|
52
61
|
}
|
|
@@ -56,6 +65,28 @@ export function formatTaskLine(task: RestTask): string {
|
|
|
56
65
|
return `#${task.number} ${task.state} ${task.priority} ${task.title} (${notes.join("; ")})`;
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
/** The plan and what happened, as a line each, only where there is something to say. */
|
|
69
|
+
function scheduleLines(task: RestTask): string[] {
|
|
70
|
+
const lines: string[] = [];
|
|
71
|
+
const plan = [
|
|
72
|
+
task.category === null ? undefined : `category: ${task.category.key}`,
|
|
73
|
+
task.goalId === null ? undefined : `goal: ${task.goalId}`,
|
|
74
|
+
task.startsOn === null ? undefined : `starts: ${task.startsOn}`,
|
|
75
|
+
task.dueOn === null ? undefined : `due: ${task.dueOn}`,
|
|
76
|
+
].filter((part) => part !== undefined);
|
|
77
|
+
if (plan.length > 0) {
|
|
78
|
+
lines.push(plan.join(" "));
|
|
79
|
+
}
|
|
80
|
+
const happened = [
|
|
81
|
+
task.startedAt === null ? undefined : `started: ${task.startedAt}`,
|
|
82
|
+
task.doneAt === null ? undefined : `done: ${task.doneAt}`,
|
|
83
|
+
].filter((part) => part !== undefined);
|
|
84
|
+
if (happened.length > 0) {
|
|
85
|
+
lines.push(happened.join(" "));
|
|
86
|
+
}
|
|
87
|
+
return lines;
|
|
88
|
+
}
|
|
89
|
+
|
|
59
90
|
/** One task in full; with the decisions about it and the runs on it, when given. */
|
|
60
91
|
export function formatTask(
|
|
61
92
|
task: RestTask,
|
|
@@ -66,6 +97,7 @@ export function formatTask(
|
|
|
66
97
|
`#${task.number} ${task.title}`,
|
|
67
98
|
`state: ${task.state} priority: ${task.priority} owner: ${task.owner.login} assignee: ${task.assignee?.login ?? "nobody"}`,
|
|
68
99
|
`id: ${task.id}${task.parentId === null ? "" : ` part of: ${task.parentId}`}`,
|
|
100
|
+
...scheduleLines(task),
|
|
69
101
|
];
|
|
70
102
|
if (task.links.length > 0) {
|
|
71
103
|
lines.push("links:");
|
|
@@ -165,6 +197,62 @@ export function formatSkills(skills: RestSkills): string {
|
|
|
165
197
|
return lines.join("\n");
|
|
166
198
|
}
|
|
167
199
|
|
|
200
|
+
/** How far a goal is, from its tasks: the done, the at work, and the rest, in one phrase. */
|
|
201
|
+
function progressOf(goal: RestGoal): string {
|
|
202
|
+
const counts = goal.taskCounts;
|
|
203
|
+
const total = counts.idea + counts.ready + counts.in_progress + counts.in_review + counts.done;
|
|
204
|
+
const working = counts.in_progress + counts.in_review;
|
|
205
|
+
return `${counts.done}/${total} done${working === 0 ? "" : `, ${working} in progress`}${counts.dropped === 0 ? "" : `, ${counts.dropped} dropped`}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** One goal: its id, its state, its title, when it is due, and how far it is. */
|
|
209
|
+
export function formatGoalLine(goal: RestGoal): string {
|
|
210
|
+
const notes = [goal.dueOn === null ? "no target day" : `due ${goal.dueOn}`, progressOf(goal)];
|
|
211
|
+
return `${goal.id} ${goal.state} ${goal.title} (${notes.join("; ")})`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** One goal in full; with the tasks that work towards it, when given. */
|
|
215
|
+
export function formatGoal(goal: RestGoal, tasks?: readonly RestTask[]): string {
|
|
216
|
+
const lines = [
|
|
217
|
+
`goal ${goal.id}: ${goal.title}`,
|
|
218
|
+
`state: ${goal.state} due: ${goal.dueOn ?? "none"} set by: ${goal.createdBy.login}`,
|
|
219
|
+
`progress: ${progressOf(goal)}`,
|
|
220
|
+
];
|
|
221
|
+
if (goal.body.trim().length > 0) {
|
|
222
|
+
lines.push("", goal.body.trimEnd());
|
|
223
|
+
}
|
|
224
|
+
if (tasks !== undefined) {
|
|
225
|
+
lines.push("", tasks.length === 0 ? "tasks: none" : "tasks:");
|
|
226
|
+
for (const task of tasks) {
|
|
227
|
+
lines.push(` ${formatTaskLine(task)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return lines.join("\n");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** One category: its key, what people call it, and its colour. */
|
|
234
|
+
export function formatCategoryLine(category: RestCategory): string {
|
|
235
|
+
return `${category.key} ${category.name} (${category.color})`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** What is on a person's plate: their open tasks by project, and the decisions waiting. */
|
|
239
|
+
export function formatWork(work: RestWork): string {
|
|
240
|
+
const lines: string[] = [];
|
|
241
|
+
if (work.tasks.length === 0) {
|
|
242
|
+
lines.push("No open task is assigned to you.");
|
|
243
|
+
} else {
|
|
244
|
+
lines.push("tasks assigned to you:");
|
|
245
|
+
for (const { project, task } of work.tasks) {
|
|
246
|
+
lines.push(` ${project.key} ${formatTaskLine(task)}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
lines.push("", work.decisions.length === 0 ? "decisions waiting: none" : "decisions waiting:");
|
|
250
|
+
for (const { project, decision } of work.decisions) {
|
|
251
|
+
lines.push(` ${project.key} ${formatDecisionLine(decision)}`);
|
|
252
|
+
}
|
|
253
|
+
return lines.join("\n");
|
|
254
|
+
}
|
|
255
|
+
|
|
168
256
|
export function formatDecisionLine(decision: RestDecision): string {
|
|
169
257
|
const state = decision.answer === null ? "waiting" : `answered ${decision.answer.option}`;
|
|
170
258
|
return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)})`;
|
package/src/cli/help.ts
CHANGED
|
@@ -51,6 +51,22 @@ const MARKDOWN = `Markdown, up to ${MAX_BODY_LENGTH} characters`;
|
|
|
51
51
|
const DOCUMENT_PATH = `a path of lowercase letters, digits and hyphens, with slashes between its folders (guides/onboarding), up to ${MAX_DOCUMENT_PATH_LENGTH} characters; it does not change once written`;
|
|
52
52
|
const DOCUMENT_REFUSALS = ["document.not_found: no page has that path in this project"];
|
|
53
53
|
|
|
54
|
+
const DAY = "a day as 2026-10-24, today, tomorrow, or +7 for seven days from today";
|
|
55
|
+
const TASK_FIELD_FLAGS =
|
|
56
|
+
"[--state <state>] [--priority <priority>] [--category <key>] [--goal <id>] [--starts <day>] [--due <day>] [--assignee <login|me>] [--parent <number|id>]";
|
|
57
|
+
const TASK_FIELD_LIMITS = [
|
|
58
|
+
`starts, due: ${DAY}; the due day is not before the start`,
|
|
59
|
+
"category: the key of one of the workspace's categories (console categories); goal: the id of one of the project's goals (console goals)",
|
|
60
|
+
"assignee: a login of the workspace, or me; parent: a task of the project",
|
|
61
|
+
];
|
|
62
|
+
const TASK_FIELD_REFUSALS = [
|
|
63
|
+
"task.invalid_category: no category has that key",
|
|
64
|
+
"task.invalid_goal: the goal is not one of this project",
|
|
65
|
+
"task.invalid_dates: the due day is before the start",
|
|
66
|
+
"task.invalid_assignee: the person may not work in the project",
|
|
67
|
+
"task.invalid_parent: the parent is not a task of the project, or would make a loop",
|
|
68
|
+
];
|
|
69
|
+
|
|
54
70
|
export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
55
71
|
login: {
|
|
56
72
|
usage: "login --url <origin> [--agent <name>] [--token-stdin]",
|
|
@@ -82,18 +98,52 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
82
98
|
"Says which project this directory works in: writes .skillcdn-console.json here, which the commands read from this directory or one above it, and which is meant to be committed. --project <key> on any command, or CONSOLE_PROJECT in the environment, wins over it.",
|
|
83
99
|
refusals: ["project.not_found: no project has the key, or it is not yours to see"],
|
|
84
100
|
},
|
|
101
|
+
work: {
|
|
102
|
+
usage: "work",
|
|
103
|
+
about:
|
|
104
|
+
"What is on your plate across the projects you may see: the open tasks assigned to you, the sooner due first, each with its project; and the decisions waiting for a person. Needs no project.",
|
|
105
|
+
},
|
|
85
106
|
tasks: {
|
|
86
|
-
usage:
|
|
107
|
+
usage:
|
|
108
|
+
"tasks [--state <state>] [--open] [--mine] [--category <key>] [--goal <id>] [--due <day|today|week|overdue>]",
|
|
87
109
|
about:
|
|
88
|
-
"The tasks on the board, newest first: number, state, priority, title, who owns and works on each, and what waits on it. States: idea, ready, in_progress, in_review, done, dropped.",
|
|
110
|
+
"The tasks on the board, newest first: number, state, priority, title, who owns and works on each, its category and its due day, and what waits on it. --open keeps the ones not done or dropped, --mine those assigned to you, --category and --goal those sorted into one, --due those due by a day: today, within the week, or overdue (the open ones past their day). States: idea, ready, in_progress, in_review, done, dropped.",
|
|
89
111
|
},
|
|
90
112
|
task: {
|
|
113
|
+
usage: `task <number|id> | task new <title> [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS} | task edit <number|id> [--title <title>] [--body <markdown>|--file <path>] ${TASK_FIELD_FLAGS}`,
|
|
114
|
+
about:
|
|
115
|
+
"One task in full, with its plan (category, goal, start and due days), what happened to it (when it was started and done, which the board keeps itself), the decisions about it and the runs on it; or a new task written down; or a task changed, only in what the flags say, where none clears a field (priorities: low, normal, high, urgent).",
|
|
116
|
+
limits: [
|
|
117
|
+
`title: one line, up to ${MAX_TITLE_LENGTH} characters`,
|
|
118
|
+
`body: ${MARKDOWN}`,
|
|
119
|
+
...TASK_FIELD_LIMITS,
|
|
120
|
+
],
|
|
121
|
+
refusals: ["task.not_found: no task has that number or id", ...TASK_FIELD_REFUSALS],
|
|
122
|
+
},
|
|
123
|
+
goals: {
|
|
124
|
+
usage: "goals",
|
|
125
|
+
about:
|
|
126
|
+
"The roadmap of the project: its goals, the active first, each with its id, its state (planned, active, done, dropped), its target day, and how far it is, counted from the tasks that work towards it.",
|
|
127
|
+
},
|
|
128
|
+
goal: {
|
|
91
129
|
usage:
|
|
92
|
-
|
|
130
|
+
'goal <id> | goal new "<title>" [--body <markdown>|--file <path>] [--state <state>] [--due <day>] | goal edit <id> [--title <title>] [--body <markdown>|--file <path>] [--state <state>] [--due <day|none>]',
|
|
93
131
|
about:
|
|
94
|
-
"One
|
|
95
|
-
limits: [
|
|
96
|
-
|
|
132
|
+
"One goal in full, with the tasks that work towards it; or a goal set on the roadmap, for an owner of the project; or one changed, only in what the flags say. A task works towards a goal through its --goal.",
|
|
133
|
+
limits: [
|
|
134
|
+
`title: one line, up to ${MAX_TITLE_LENGTH} characters`,
|
|
135
|
+
`body: ${MARKDOWN}`,
|
|
136
|
+
`due: ${DAY}`,
|
|
137
|
+
],
|
|
138
|
+
refusals: [
|
|
139
|
+
"goal.not_found: no goal of this project has that id",
|
|
140
|
+
"auth.forbidden: only an owner of the project sets its roadmap",
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
categories: {
|
|
144
|
+
usage: "categories",
|
|
145
|
+
about:
|
|
146
|
+
"The categories tasks are sorted into, the same in every project: each with its key, which --category takes, its name and its colour. An administrator keeps them on the console's Categories page.",
|
|
97
147
|
},
|
|
98
148
|
skills: {
|
|
99
149
|
usage: "skills",
|
|
@@ -283,7 +333,19 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
|
|
|
283
333
|
},
|
|
284
334
|
{
|
|
285
335
|
title: "The board",
|
|
286
|
-
commands: [
|
|
336
|
+
commands: [
|
|
337
|
+
"work",
|
|
338
|
+
"tasks",
|
|
339
|
+
"task",
|
|
340
|
+
"goals",
|
|
341
|
+
"goal",
|
|
342
|
+
"categories",
|
|
343
|
+
"decisions",
|
|
344
|
+
"decision",
|
|
345
|
+
"runs",
|
|
346
|
+
"run",
|
|
347
|
+
"skills",
|
|
348
|
+
],
|
|
287
349
|
},
|
|
288
350
|
{ title: "Working", commands: ["take", "report", "hand-in", "ask", "finish", "fail", "abandon"] },
|
|
289
351
|
{
|