@skillcdn/console 0.1.16 → 0.1.18
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 +4 -3
- package/dist/api.d.ts +4 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +3 -2
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +12 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +226 -21
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +8 -3
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +31 -5
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +34 -8
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/install.d.ts +21 -0
- package/dist/cli/install.d.ts.map +1 -0
- package/dist/cli/install.js +87 -0
- package/dist/cli/install.js.map +1 -0
- package/dist/cli/main.js +24 -4
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/notices.d.ts +13 -0
- package/dist/cli/notices.d.ts.map +1 -0
- package/dist/cli/notices.js +31 -0
- package/dist/cli/notices.js.map +1 -0
- package/dist/cli/terminal.d.ts +5 -0
- package/dist/cli/terminal.d.ts.map +1 -1
- package/dist/cli/terminal.js +11 -1
- package/dist/cli/terminal.js.map +1 -1
- package/dist/client.d.ts +43 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +69 -4
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +66 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +115 -0
- package/dist/compat.js.map +1 -0
- package/dist/components/calendar.d.ts +3 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +21 -2
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/comments.d.ts +59 -0
- package/dist/components/comments.d.ts.map +1 -0
- package/dist/components/comments.js +138 -0
- package/dist/components/comments.js.map +1 -0
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +2 -0
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/goals.d.ts +3 -1
- package/dist/components/goals.d.ts.map +1 -1
- package/dist/components/goals.js +23 -11
- package/dist/components/goals.js.map +1 -1
- package/dist/components/markdown.d.ts +6 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +2 -1
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +6 -1
- package/dist/components/projects.js.map +1 -1
- package/dist/components/roadmap.d.ts +37 -0
- package/dist/components/roadmap.d.ts.map +1 -0
- package/dist/components/roadmap.js +76 -0
- package/dist/components/roadmap.js.map +1 -0
- package/dist/components/task-view.d.ts +8 -3
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +6 -4
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/tokens.d.ts +2 -0
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +13 -3
- package/dist/components/tokens.js.map +1 -1
- package/dist/console.css +314 -2
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +30 -11
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +5 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +3 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +38 -2
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +38 -2
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +38 -2
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +3 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +3 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +6 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +10 -1
- package/dist/router.js.map +1 -1
- package/dist/rules.js +2 -2
- package/dist/rules.js.map +1 -1
- package/dist/schemas.d.ts +147 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +50 -1
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +1 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +1 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +17 -10
- package/docs/specs/rest.md +17 -6
- package/package.json +3 -2
- package/skills/working-the-board/SKILL.md +8 -5
- package/src/api.ts +35 -0
- package/src/cli/cli.ts +273 -22
- package/src/cli/format.ts +36 -4
- package/src/cli/help.ts +38 -7
- package/src/cli/install.ts +111 -0
- package/src/cli/main.ts +25 -4
- package/src/cli/notices.ts +48 -0
- package/src/cli/terminal.ts +12 -1
- package/src/client.ts +142 -8
- package/src/compat.ts +173 -0
- package/src/components/calendar.tsx +41 -1
- package/src/components/comments.tsx +342 -0
- package/src/components/event-feed.tsx +2 -0
- package/src/components/goals.tsx +86 -7
- package/src/components/markdown.tsx +9 -1
- package/src/components/projects.tsx +16 -3
- package/src/components/roadmap.tsx +242 -0
- package/src/components/task-view.tsx +32 -33
- package/src/components/tokens.tsx +38 -1
- package/src/console.tsx +45 -3
- package/src/data.ts +9 -1
- package/src/i18n/en.ts +40 -2
- package/src/i18n/ko.ts +40 -2
- package/src/index.ts +20 -0
- package/src/limits.ts +3 -0
- package/src/router.ts +13 -2
- package/src/rules.ts +2 -2
- package/src/schemas.ts +60 -1
- package/src/styles/console.css +314 -2
- package/src/vocabulary.ts +1 -0
package/src/cli/cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type FetchLike,
|
|
8
8
|
type ProjectClient,
|
|
9
9
|
} from "../client.js";
|
|
10
|
+
import { CLIENT_RANGE_HEADER, judgeClient, parseVersion, VERSION_HEADER } from "../compat.js";
|
|
10
11
|
import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
|
|
11
12
|
import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
12
13
|
import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
|
|
@@ -41,6 +42,7 @@ import type { DirectoryStore } from "./directory.js";
|
|
|
41
42
|
import {
|
|
42
43
|
formatAnswer,
|
|
43
44
|
formatCategoryLine,
|
|
45
|
+
formatComment,
|
|
44
46
|
formatDecision,
|
|
45
47
|
formatDecisionLine,
|
|
46
48
|
formatDocument,
|
|
@@ -66,6 +68,8 @@ import {
|
|
|
66
68
|
formatWork,
|
|
67
69
|
} from "./format.js";
|
|
68
70
|
import { COMMAND_HELP, helpFor, usage } from "./help.js";
|
|
71
|
+
import { describeInstall, type Install, updateCommandFor } from "./install.js";
|
|
72
|
+
import type { NoticeStore } from "./notices.js";
|
|
69
73
|
import { createServeHandler, DEFAULT_SERVE_PORT, type Listen } from "./serve.js";
|
|
70
74
|
|
|
71
75
|
// The `console` command (docs/specs/cli.md): the agent's side of the console. A thin client of
|
|
@@ -83,8 +87,13 @@ export const EXIT = {
|
|
|
83
87
|
usage: 2,
|
|
84
88
|
/** The decision still waits for a person. */
|
|
85
89
|
waiting: 3,
|
|
90
|
+
/** The command and the console do not go together (ADR-0025): stderr says which to update, and how. */
|
|
91
|
+
incompatible: 4,
|
|
86
92
|
} as const;
|
|
87
93
|
|
|
94
|
+
/** A day, for how often the command repeats what it noticed of the versions. */
|
|
95
|
+
const DAY_MS = 86_400_000;
|
|
96
|
+
|
|
88
97
|
/** How long one request for a decision's answer asks the console to hold it: what the server allows. */
|
|
89
98
|
const WAIT_STEP_SECONDS = 50;
|
|
90
99
|
/** A breath between two such requests, should the console answer at once. */
|
|
@@ -107,6 +116,8 @@ export interface CliIo {
|
|
|
107
116
|
readonly readFile: (path: string) => Promise<string>;
|
|
108
117
|
/** A file handed in, as bytes. */
|
|
109
118
|
readonly readBytes: (path: string) => Promise<Uint8Array>;
|
|
119
|
+
/** A file read from the console, written at a path, or into a directory under its name; says where. */
|
|
120
|
+
readonly writeBytes: (path: string, bytes: Uint8Array, name: string) => Promise<string>;
|
|
110
121
|
readonly store: CredentialStore;
|
|
111
122
|
/** What the working directory says of the project, and the way to write it. */
|
|
112
123
|
readonly directory: DirectoryStore;
|
|
@@ -114,6 +125,12 @@ export interface CliIo {
|
|
|
114
125
|
readonly now: () => number;
|
|
115
126
|
/** The version of the package the command runs from, when the entry point knows it. */
|
|
116
127
|
readonly version?: string | undefined;
|
|
128
|
+
/** What the command calls itself on every request (ADR-0025); nothing when it does not know its version. */
|
|
129
|
+
readonly userAgent?: string | undefined;
|
|
130
|
+
/** Where the command is installed, so that an update is told the way it takes effect; nothing when unknown. */
|
|
131
|
+
readonly install?: Install | undefined;
|
|
132
|
+
/** What the command last said of the versions; left out, it says it every time. */
|
|
133
|
+
readonly notices?: NoticeStore | undefined;
|
|
117
134
|
/** Where the console's own skills are on this machine, shipped with the package; nothing when they are not there. */
|
|
118
135
|
readonly skillsDir?: string | undefined;
|
|
119
136
|
/** How `serve` listens on the loopback (ADR-0014); left out, the command cannot serve. */
|
|
@@ -197,7 +214,7 @@ async function open(io: CliIo, values: Values): Promise<Session> {
|
|
|
197
214
|
"Not signed in. A person signs the command in: console login --url <the console's origin>",
|
|
198
215
|
);
|
|
199
216
|
}
|
|
200
|
-
const client =
|
|
217
|
+
const client = clientOf(io, url, token);
|
|
201
218
|
const projectKey = async () =>
|
|
202
219
|
text(values, "project") ?? io.env.CONSOLE_PROJECT ?? (await io.directory.load())?.project;
|
|
203
220
|
return {
|
|
@@ -502,7 +519,7 @@ type Command = (args: readonly string[], io: CliIo) => Promise<number>;
|
|
|
502
519
|
|
|
503
520
|
/** Checks a token against the console, keeps it with the address, and says who it is. */
|
|
504
521
|
async function keep(io: CliIo, url: string, token: string, agent?: string): Promise<number> {
|
|
505
|
-
const me = await
|
|
522
|
+
const me = await clientOf(io, url, token).me();
|
|
506
523
|
const person = me.person;
|
|
507
524
|
if (person === null) {
|
|
508
525
|
throw failed(
|
|
@@ -537,7 +554,7 @@ const login: Command = async (args, io) => {
|
|
|
537
554
|
}
|
|
538
555
|
// A connection (ADR-0011): a code a person approves on the console's own pages, and the token
|
|
539
556
|
// handed over here once, never shown.
|
|
540
|
-
const client =
|
|
557
|
+
const client = clientOf(io, url);
|
|
541
558
|
// "<agent> on <machine>": the pages say the agent and keep the machine for a hover.
|
|
542
559
|
const agent = text(values, "agent") ?? agentOf(io.env) ?? "An agent";
|
|
543
560
|
const begun = await client.connect({
|
|
@@ -579,7 +596,7 @@ const logout: Command = async (args, io) => {
|
|
|
579
596
|
// Agents page, and the person is told so.
|
|
580
597
|
let gone = true;
|
|
581
598
|
try {
|
|
582
|
-
await
|
|
599
|
+
await clientOf(io, url, token).giveUpToken();
|
|
583
600
|
} catch (error) {
|
|
584
601
|
if (!(error instanceof ApiError && error.code === "auth.required")) {
|
|
585
602
|
gone = false;
|
|
@@ -617,12 +634,38 @@ const whoami: Command = async (args, io) => {
|
|
|
617
634
|
throw failed("The console does not know this token. Sign in again: console login");
|
|
618
635
|
}
|
|
619
636
|
const key = await session.projectKey();
|
|
637
|
+
const command = parseVersion(io.version);
|
|
638
|
+
const server = parseVersion(me.console?.version);
|
|
639
|
+
const verdict =
|
|
640
|
+
command === undefined || server === undefined
|
|
641
|
+
? undefined
|
|
642
|
+
: judgeClient(command, server).verdict;
|
|
643
|
+
// A console from before the rule says nothing of itself; the command says so instead.
|
|
644
|
+
const consoleWords =
|
|
645
|
+
me.console === undefined
|
|
646
|
+
? "the console does not say its version, so it is older than 0.1.18"
|
|
647
|
+
: `the console is ${me.console.version} and takes ${me.console.clientRange}`;
|
|
620
648
|
answer(
|
|
621
649
|
io,
|
|
622
650
|
session,
|
|
623
|
-
{
|
|
651
|
+
{
|
|
652
|
+
...me,
|
|
653
|
+
console: me.console ?? null,
|
|
654
|
+
project: key ?? null,
|
|
655
|
+
command: { version: io.version ?? null, install: io.install ?? null },
|
|
656
|
+
},
|
|
624
657
|
() =>
|
|
625
|
-
|
|
658
|
+
[
|
|
659
|
+
`${person.login} (${person.role}) at ${me.workspace.name}, ${session.url}${key === undefined ? "; no project named here" : `; project ${key}`}`,
|
|
660
|
+
`This command is ${io.version ?? "of a version it does not know"}${io.install === undefined ? "" : `, ${describeInstall(io.install)}`}; ${consoleWords}.`,
|
|
661
|
+
...(verdict === "behind"
|
|
662
|
+
? [`A newer command exists. Update it: ${updateWords(io, me.console?.version)}`]
|
|
663
|
+
: verdict === "ahead"
|
|
664
|
+
? [
|
|
665
|
+
`This command is newer than the console: what the console has not got yet is refused as not found. To match the console: ${updateWords(io, me.console?.version)}`,
|
|
666
|
+
]
|
|
667
|
+
: []),
|
|
668
|
+
].join("\n"),
|
|
626
669
|
);
|
|
627
670
|
return EXIT.ok;
|
|
628
671
|
};
|
|
@@ -952,12 +995,94 @@ const task: Command = async (args, io) => {
|
|
|
952
995
|
const session = await open(io, values);
|
|
953
996
|
const project = await session.project();
|
|
954
997
|
const found = await project.task(ref);
|
|
955
|
-
const [decisions, runs] = await Promise.all([
|
|
998
|
+
const [decisions, runs, comments] = await Promise.all([
|
|
956
999
|
project.decisions({ task: found.id }),
|
|
957
1000
|
project.runs({ task: found.id }),
|
|
1001
|
+
project.comments(found.id),
|
|
958
1002
|
]);
|
|
959
|
-
answer(
|
|
960
|
-
|
|
1003
|
+
answer(
|
|
1004
|
+
io,
|
|
1005
|
+
session,
|
|
1006
|
+
{ task: found, decisions: decisions.items, runs: runs.items, comments: comments.items },
|
|
1007
|
+
() => formatTask(found, decisions.items, runs.items, comments.items),
|
|
1008
|
+
);
|
|
1009
|
+
return EXIT.ok;
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* A word in the discussion on a task (ADR-0024): what is said, as given, from a file or from
|
|
1014
|
+
* standard input, with the files named by --attach put in it; one request, one comment.
|
|
1015
|
+
*/
|
|
1016
|
+
const comment: Command = async (args, io) => {
|
|
1017
|
+
const { values, positionals } = parse(args, {
|
|
1018
|
+
file: { type: "string" },
|
|
1019
|
+
attach: { type: "string", multiple: true },
|
|
1020
|
+
});
|
|
1021
|
+
const ask =
|
|
1022
|
+
'Say which task, and what: console comment <number|id> "<markdown>" [--file <path> | -] [--attach <path>]...';
|
|
1023
|
+
if (positionals.length === 0 || positionals.length > 2) {
|
|
1024
|
+
throw misuse(ask);
|
|
1025
|
+
}
|
|
1026
|
+
const ref = positionals[0] ?? "";
|
|
1027
|
+
const body = (await bodyOf(io, values, positionals[1])) ?? "";
|
|
1028
|
+
const paths = texts(values, "attach");
|
|
1029
|
+
if (body.trim().length === 0 && paths.length === 0) {
|
|
1030
|
+
throw misuse(`Say something, or attach a file. ${ask}`);
|
|
1031
|
+
}
|
|
1032
|
+
const files = [];
|
|
1033
|
+
for (const path of paths) {
|
|
1034
|
+
let bytes: Uint8Array;
|
|
1035
|
+
try {
|
|
1036
|
+
bytes = await io.readBytes(path);
|
|
1037
|
+
} catch {
|
|
1038
|
+
throw failed(`No file could be read at ${path}. Name the path of a file to attach.`);
|
|
1039
|
+
}
|
|
1040
|
+
const name = basename(path);
|
|
1041
|
+
const contentType = CONTENT_TYPES[extname(name).toLowerCase()];
|
|
1042
|
+
files.push({ name, bytes, ...(contentType === undefined ? {} : { contentType }) });
|
|
1043
|
+
}
|
|
1044
|
+
const session = await open(io, values);
|
|
1045
|
+
const project = await session.project();
|
|
1046
|
+
const task = await project.task(ref);
|
|
1047
|
+
const written = await project.comment(task.id, { body, files });
|
|
1048
|
+
answer(io, session, written, () =>
|
|
1049
|
+
[
|
|
1050
|
+
`Commented on #${task.number} ${task.title}${written.files.length === 0 ? "" : `, with ${written.files.length} file${written.files.length === 1 ? "" : "s"}`}.`,
|
|
1051
|
+
formatComment(written),
|
|
1052
|
+
].join("\n"),
|
|
1053
|
+
);
|
|
1054
|
+
return EXIT.ok;
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* A file of the project, read from the console and written on this machine: one a run handed
|
|
1059
|
+
* in, one put in a comment, or one attached to a page, by its id, so that an agent can look at
|
|
1060
|
+
* a picture a person put on the board.
|
|
1061
|
+
*/
|
|
1062
|
+
const file: Command = async (args, io) => {
|
|
1063
|
+
const { values, positionals } = parse(args, { out: { type: "string" } });
|
|
1064
|
+
const id = theOne(positionals, "Say which file: console file <id> [--out <path>]");
|
|
1065
|
+
const session = await open(io, values);
|
|
1066
|
+
const project = await session.project();
|
|
1067
|
+
const read = await project.readFile(id);
|
|
1068
|
+
const target = text(values, "out") ?? read.name;
|
|
1069
|
+
let saved: string;
|
|
1070
|
+
try {
|
|
1071
|
+
saved = await io.writeBytes(target, read.bytes, read.name);
|
|
1072
|
+
} catch {
|
|
1073
|
+
throw failed(`The file could not be written at ${target}. Name a path with --out <path>.`);
|
|
1074
|
+
}
|
|
1075
|
+
answer(
|
|
1076
|
+
io,
|
|
1077
|
+
session,
|
|
1078
|
+
{
|
|
1079
|
+
id,
|
|
1080
|
+
name: read.name,
|
|
1081
|
+
contentType: read.contentType,
|
|
1082
|
+
size: read.bytes.byteLength,
|
|
1083
|
+
path: saved,
|
|
1084
|
+
},
|
|
1085
|
+
() => `Saved ${read.name} (${read.bytes.byteLength} bytes, ${read.contentType}) at ${saved}`,
|
|
961
1086
|
);
|
|
962
1087
|
return EXIT.ok;
|
|
963
1088
|
};
|
|
@@ -975,10 +1100,11 @@ const take: Command = async (args, io) => {
|
|
|
975
1100
|
});
|
|
976
1101
|
// The rules come with the run, as they read for the person: everything from here acts
|
|
977
1102
|
// within them (ADR-0023); the task is read again, as it stands now that it is taken.
|
|
978
|
-
const [ruling, me, taken] = await Promise.all([
|
|
1103
|
+
const [ruling, me, taken, comments] = await Promise.all([
|
|
979
1104
|
project.rules(),
|
|
980
1105
|
session.client.me(),
|
|
981
1106
|
project.task(ref),
|
|
1107
|
+
project.comments(found.id),
|
|
982
1108
|
]);
|
|
983
1109
|
// Taking a task with a run open on it ends that run (ADR-0020): said when it happened.
|
|
984
1110
|
const tookOver =
|
|
@@ -989,16 +1115,16 @@ const take: Command = async (args, io) => {
|
|
|
989
1115
|
workspace: forPerson(ruling.workspace, me.language),
|
|
990
1116
|
project: forPerson(ruling.project, me.language),
|
|
991
1117
|
};
|
|
992
|
-
answer(io, session, { run, task: taken, rules }, () =>
|
|
1118
|
+
answer(io, session, { run, task: taken, comments: comments.items, rules }, () =>
|
|
993
1119
|
[
|
|
994
1120
|
`Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.${tookOver}`,
|
|
995
1121
|
"",
|
|
996
|
-
formatTask(taken),
|
|
1122
|
+
formatTask(taken, undefined, undefined, comments.items),
|
|
997
1123
|
"",
|
|
998
1124
|
formatRules(rules),
|
|
999
1125
|
"",
|
|
1000
1126
|
'Say how it goes: console report "<what you found or did>". Hand in what you make: console hand-in <https url | file path>.',
|
|
1001
|
-
'Ask when a person must decide: console ask "<question>" --option "..." --option "...". End with: console finish --summary "<what was done, what is left>".',
|
|
1127
|
+
'Ask when a person must decide: console ask "<question>" --option "..." --option "...". A word for the task\'s people that is not a report: console comment <number> "<markdown>". End with: console finish --summary "<what was done, what is left>".',
|
|
1002
1128
|
].join("\n"),
|
|
1003
1129
|
);
|
|
1004
1130
|
return EXIT.ok;
|
|
@@ -1331,13 +1457,24 @@ const restore: Command = async (args, io) => {
|
|
|
1331
1457
|
};
|
|
1332
1458
|
|
|
1333
1459
|
const goals: Command = async (args, io) => {
|
|
1334
|
-
const { values } = parse(
|
|
1460
|
+
const { values } = parse(
|
|
1461
|
+
args,
|
|
1462
|
+
{ category: { type: "string" }, state: { type: "string" } },
|
|
1463
|
+
false,
|
|
1464
|
+
);
|
|
1465
|
+
const state = goalStateOf(text(values, "state"));
|
|
1466
|
+
const category = text(values, "category");
|
|
1335
1467
|
const session = await open(io, values);
|
|
1336
1468
|
const project = await session.project();
|
|
1337
|
-
const found = await project.goals(
|
|
1469
|
+
const found = await project.goals({
|
|
1470
|
+
...(state === undefined ? {} : { state }),
|
|
1471
|
+
...(category === undefined ? {} : { category }),
|
|
1472
|
+
});
|
|
1338
1473
|
answer(io, session, found, () =>
|
|
1339
1474
|
found.items.length === 0
|
|
1340
|
-
?
|
|
1475
|
+
? state === undefined && category === undefined
|
|
1476
|
+
? "No goals. An owner sets one: console goal new <title> --due <day>"
|
|
1477
|
+
: "No goal matches."
|
|
1341
1478
|
: found.items.map(formatGoalLine).join("\n"),
|
|
1342
1479
|
);
|
|
1343
1480
|
return EXIT.ok;
|
|
@@ -1345,15 +1482,24 @@ const goals: Command = async (args, io) => {
|
|
|
1345
1482
|
|
|
1346
1483
|
const GOAL_FIELDS: OptionSpec = {
|
|
1347
1484
|
state: { type: "string" },
|
|
1485
|
+
category: { type: "string" },
|
|
1486
|
+
starts: { type: "string" },
|
|
1348
1487
|
due: { type: "string" },
|
|
1349
1488
|
body: { type: "string" },
|
|
1350
1489
|
file: { type: "string" },
|
|
1351
1490
|
};
|
|
1352
1491
|
|
|
1492
|
+
/** A category's key as a flag names one, or `null` for `none` where the field may be cleared. */
|
|
1493
|
+
function categoryOf(value: string | undefined, clearable = false): string | null | undefined {
|
|
1494
|
+
return clearable && value === "none" ? null : value;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1353
1497
|
const newGoal: Command = async (args, io) => {
|
|
1354
1498
|
const { values, positionals } = parse(args, GOAL_FIELDS);
|
|
1355
1499
|
const title = theOne(positionals, 'Say the title: console goal new "<title>" [--due <day>]');
|
|
1356
1500
|
const state = goalStateOf(text(values, "state"));
|
|
1501
|
+
const category = categoryOf(text(values, "category"));
|
|
1502
|
+
const startsOn = dayOf(io, text(values, "starts"));
|
|
1357
1503
|
const dueOn = dayOf(io, text(values, "due"));
|
|
1358
1504
|
const body = await bodyOf(io, values, text(values, "body"));
|
|
1359
1505
|
const session = await open(io, values);
|
|
@@ -1362,6 +1508,8 @@ const newGoal: Command = async (args, io) => {
|
|
|
1362
1508
|
title,
|
|
1363
1509
|
...(body === undefined ? {} : { body }),
|
|
1364
1510
|
...(state === undefined ? {} : { state }),
|
|
1511
|
+
...(category === undefined ? {} : { category }),
|
|
1512
|
+
...(startsOn === undefined ? {} : { startsOn }),
|
|
1365
1513
|
...(dueOn === undefined ? {} : { dueOn }),
|
|
1366
1514
|
});
|
|
1367
1515
|
answer(io, session, made, () => `Goal set: ${made.title}\nid: ${made.id}`);
|
|
@@ -1373,17 +1521,21 @@ const editGoal: Command = async (args, io) => {
|
|
|
1373
1521
|
const id = theOne(positionals, "Say which goal: console goal edit <id> --state <state> ...");
|
|
1374
1522
|
const title = text(values, "title");
|
|
1375
1523
|
const state = goalStateOf(text(values, "state"));
|
|
1524
|
+
const category = categoryOf(text(values, "category"), true);
|
|
1525
|
+
const startsOn = dayOf(io, text(values, "starts"), true);
|
|
1376
1526
|
const dueOn = dayOf(io, text(values, "due"), true);
|
|
1377
1527
|
const body = await bodyOf(io, values, text(values, "body"));
|
|
1378
1528
|
const patch = {
|
|
1379
1529
|
...(title === undefined ? {} : { title }),
|
|
1380
1530
|
...(body === undefined ? {} : { body }),
|
|
1381
1531
|
...(state === undefined ? {} : { state }),
|
|
1532
|
+
...(category === undefined ? {} : { category }),
|
|
1533
|
+
...(startsOn === undefined ? {} : { startsOn }),
|
|
1382
1534
|
...(dueOn === undefined ? {} : { dueOn }),
|
|
1383
1535
|
};
|
|
1384
1536
|
if (Object.keys(patch).length === 0) {
|
|
1385
1537
|
throw misuse(
|
|
1386
|
-
"Say what to change: --title, --body, --state or --due;
|
|
1538
|
+
"Say what to change: --title, --body, --state, --category, --starts or --due; none clears a category or a day.",
|
|
1387
1539
|
);
|
|
1388
1540
|
}
|
|
1389
1541
|
const session = await open(io, values);
|
|
@@ -1904,6 +2056,8 @@ const COMMANDS: Readonly<Record<string, Command>> = {
|
|
|
1904
2056
|
take,
|
|
1905
2057
|
report,
|
|
1906
2058
|
"hand-in": handIn,
|
|
2059
|
+
comment,
|
|
2060
|
+
file,
|
|
1907
2061
|
ask,
|
|
1908
2062
|
decision,
|
|
1909
2063
|
decisions,
|
|
@@ -1933,9 +2087,82 @@ function help(io: CliIo, name: string | undefined): number {
|
|
|
1933
2087
|
return EXIT.ok;
|
|
1934
2088
|
}
|
|
1935
2089
|
|
|
2090
|
+
/** A client of the console at `url`, naming the command on every request (ADR-0025). */
|
|
2091
|
+
function clientOf(io: CliIo, url: string, token?: string): ConsoleClient {
|
|
2092
|
+
return createClient({
|
|
2093
|
+
baseUrl: url,
|
|
2094
|
+
fetch: io.fetch,
|
|
2095
|
+
...(token === undefined ? {} : { token }),
|
|
2096
|
+
...(io.userAgent === undefined ? {} : { userAgent: io.userAgent }),
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
/** What the console said of itself on its last answer (ADR-0025): its version, and the range of command versions it takes. */
|
|
2101
|
+
interface ConsoleSeen {
|
|
2102
|
+
version?: string | undefined;
|
|
2103
|
+
range?: string | undefined;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
/** The command that puts the console's version where this command is installed. */
|
|
2107
|
+
function updateWords(io: CliIo, version: string | undefined): string {
|
|
2108
|
+
const wanted = version ?? "latest";
|
|
2109
|
+
if (io.install === undefined) {
|
|
2110
|
+
return `npm install -g @skillcdn/console@${wanted}`;
|
|
2111
|
+
}
|
|
2112
|
+
const command = updateCommandFor(io.install, wanted);
|
|
2113
|
+
return io.install.kind === "project" ? `${command} (in ${io.install.root})` : command;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
/**
|
|
2117
|
+
* What the command says of the versions once the console answered: that a newer command exists,
|
|
2118
|
+
* or that this one is newer than the console; once a day for one pair of versions, on stderr,
|
|
2119
|
+
* so that the console's JSON stays what it is. What is out of the range never gets here: the
|
|
2120
|
+
* console refused it, and the refusal says what to do.
|
|
2121
|
+
*/
|
|
2122
|
+
async function noticeVersions(io: CliIo, seen: ConsoleSeen): Promise<void> {
|
|
2123
|
+
const command = parseVersion(io.version);
|
|
2124
|
+
const server = parseVersion(seen.version);
|
|
2125
|
+
if (
|
|
2126
|
+
io.version === undefined ||
|
|
2127
|
+
seen.version === undefined ||
|
|
2128
|
+
command === undefined ||
|
|
2129
|
+
server === undefined
|
|
2130
|
+
) {
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
const { verdict } = judgeClient(command, server);
|
|
2134
|
+
if (verdict !== "behind" && verdict !== "ahead") {
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
const previous = await io.notices?.load();
|
|
2138
|
+
const now = io.now();
|
|
2139
|
+
if (
|
|
2140
|
+
previous !== undefined &&
|
|
2141
|
+
previous.command === io.version &&
|
|
2142
|
+
previous.console === seen.version &&
|
|
2143
|
+
now - Date.parse(previous.shownAt) < DAY_MS
|
|
2144
|
+
) {
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
io.stderr(
|
|
2148
|
+
verdict === "behind"
|
|
2149
|
+
? `A newer command exists: the console runs ${seen.version}, this command is ${io.version}. Update it: ${updateWords(io, seen.version)}\n`
|
|
2150
|
+
: `This command (${io.version}) is newer than the console (${seen.version}, which takes ${seen.range ?? "an older line"}): what the console has not got yet is refused as not found. The console's operator updates it; to match the console meanwhile: ${updateWords(io, seen.version)}\n`,
|
|
2151
|
+
);
|
|
2152
|
+
await io.notices?.save({
|
|
2153
|
+
command: io.version,
|
|
2154
|
+
console: seen.version,
|
|
2155
|
+
shownAt: new Date(now).toISOString(),
|
|
2156
|
+
});
|
|
2157
|
+
}
|
|
2158
|
+
|
|
1936
2159
|
/** What a refusal of the console is followed by, when there is something to do about it. */
|
|
1937
|
-
function hintFor(error: ApiError): string {
|
|
2160
|
+
function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
|
|
1938
2161
|
switch (error.code) {
|
|
2162
|
+
case "client.outdated":
|
|
2163
|
+
return `Update the command: ${updateWords(io, seen.version)}\n`;
|
|
2164
|
+
case "client.too_new":
|
|
2165
|
+
return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
|
|
1939
2166
|
case "auth.required":
|
|
1940
2167
|
return "The token is not one the console knows, any more or at all. A person signs the command in again: console login\n";
|
|
1941
2168
|
case "project.not_found":
|
|
@@ -1954,6 +2181,10 @@ function hintFor(error: ApiError): string {
|
|
|
1954
2181
|
return "The due day is before the start. Say a due day on or after it.\n";
|
|
1955
2182
|
case "goal.not_found":
|
|
1956
2183
|
return "No goal of this project has that id. console goals lists them.\n";
|
|
2184
|
+
case "goal.invalid_category":
|
|
2185
|
+
return "No category has that key. console categories lists them.\n";
|
|
2186
|
+
case "goal.invalid_dates":
|
|
2187
|
+
return "The target day is before the start. Say a target day on or after it.\n";
|
|
1957
2188
|
case "auth.forbidden":
|
|
1958
2189
|
return "What was asked is an administrator's, or an owner's of the project, and the person this token acts as is not one here. Hand the step to them.\n";
|
|
1959
2190
|
case "project.key_taken":
|
|
@@ -1994,9 +2225,23 @@ function hintFor(error: ApiError): string {
|
|
|
1994
2225
|
/** Runs the command and answers with its exit code. What it prints goes through `io`. */
|
|
1995
2226
|
export async function runCli(argv: readonly string[], io: CliIo): Promise<number> {
|
|
1996
2227
|
const [name, ...rest] = argv;
|
|
2228
|
+
// Every answer of the console says its version and the range it takes; the command reads
|
|
2229
|
+
// them off the answers it gets anyway, and asks nothing more (ADR-0025).
|
|
2230
|
+
const seen: ConsoleSeen = {};
|
|
2231
|
+
const watched: CliIo = {
|
|
2232
|
+
...io,
|
|
2233
|
+
fetch: async (input, init) => {
|
|
2234
|
+
const response = await io.fetch(input, init);
|
|
2235
|
+
seen.version = response.headers.get(VERSION_HEADER) ?? seen.version;
|
|
2236
|
+
seen.range = response.headers.get(CLIENT_RANGE_HEADER) ?? seen.range;
|
|
2237
|
+
return response;
|
|
2238
|
+
},
|
|
2239
|
+
};
|
|
1997
2240
|
try {
|
|
1998
2241
|
if (name === "--version" || name === "-v" || name === "version") {
|
|
1999
|
-
io.stdout(
|
|
2242
|
+
io.stdout(
|
|
2243
|
+
`console ${io.version ?? "(version unknown)"} (@skillcdn/console)${io.install === undefined ? "" : `, ${describeInstall(io.install)}`}\n`,
|
|
2244
|
+
);
|
|
2000
2245
|
return EXIT.ok;
|
|
2001
2246
|
}
|
|
2002
2247
|
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
@@ -2009,15 +2254,21 @@ export async function runCli(argv: readonly string[], io: CliIo): Promise<number
|
|
|
2009
2254
|
if (command === undefined) {
|
|
2010
2255
|
throw misuse(`Not a command: ${name}. Run: console help`);
|
|
2011
2256
|
}
|
|
2012
|
-
|
|
2257
|
+
const exit = await command(rest, watched);
|
|
2258
|
+
if (name !== "whoami") {
|
|
2259
|
+
await noticeVersions(io, seen);
|
|
2260
|
+
}
|
|
2261
|
+
return exit;
|
|
2013
2262
|
} catch (error) {
|
|
2014
2263
|
if (error instanceof CliExit) {
|
|
2015
2264
|
io.stderr(`${error.message}\n`);
|
|
2016
2265
|
return error.exit;
|
|
2017
2266
|
}
|
|
2018
2267
|
if (error instanceof ApiError) {
|
|
2019
|
-
io.stderr(`${error.code}: ${error.message}\n${hintFor(error)}`);
|
|
2020
|
-
return
|
|
2268
|
+
io.stderr(`${error.code}: ${error.message}\n${hintFor(error, io, seen)}`);
|
|
2269
|
+
return error.code === "client.outdated" || error.code === "client.too_new"
|
|
2270
|
+
? EXIT.incompatible
|
|
2271
|
+
: EXIT.failed;
|
|
2021
2272
|
}
|
|
2022
2273
|
throw error;
|
|
2023
2274
|
}
|
package/src/cli/format.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
RestArtifact,
|
|
3
3
|
RestCategory,
|
|
4
|
+
RestComment,
|
|
4
5
|
RestDecision,
|
|
5
6
|
RestDocument,
|
|
6
7
|
RestDocumentSummary,
|
|
@@ -131,6 +132,7 @@ export function formatTask(
|
|
|
131
132
|
task: RestTask,
|
|
132
133
|
decisions?: readonly RestDecision[],
|
|
133
134
|
runs?: readonly RestRun[],
|
|
135
|
+
comments?: readonly RestComment[],
|
|
134
136
|
): string {
|
|
135
137
|
const lines = [
|
|
136
138
|
`#${task.number} ${task.title}`,
|
|
@@ -159,6 +161,31 @@ export function formatTask(
|
|
|
159
161
|
lines.push(` ${formatRunLine(run)}`);
|
|
160
162
|
}
|
|
161
163
|
}
|
|
164
|
+
if (comments !== undefined) {
|
|
165
|
+
lines.push("", comments.length === 0 ? "comments: none" : "comments:");
|
|
166
|
+
for (const comment of comments) {
|
|
167
|
+
lines.push(formatComment(comment));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* One comment of the discussion, indented under the list: who said it, through which agent,
|
|
175
|
+
* when, the whole word, and each file put in it with the command that reads it.
|
|
176
|
+
*/
|
|
177
|
+
export function formatComment(comment: RestComment): string {
|
|
178
|
+
const who =
|
|
179
|
+
comment.agent === null ? comment.author.login : `${comment.author.login} as ${comment.agent}`;
|
|
180
|
+
const lines = [` ${who}, ${comment.createdAt}, comment ${comment.id}:`];
|
|
181
|
+
if (comment.body.trim().length > 0) {
|
|
182
|
+
lines.push(indent(comment.body));
|
|
183
|
+
}
|
|
184
|
+
for (const attached of comment.files) {
|
|
185
|
+
lines.push(
|
|
186
|
+
` file: ${attached.file.name} (${attached.file.size} bytes, ${attached.file.contentType}): console file ${attached.id}`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
162
189
|
return lines.join("\n");
|
|
163
190
|
}
|
|
164
191
|
|
|
@@ -205,7 +232,7 @@ export function formatArtifact(
|
|
|
205
232
|
const what =
|
|
206
233
|
artifact.file === null
|
|
207
234
|
? (artifact.url ?? "")
|
|
208
|
-
: `${artifact.file.name} (${artifact.file.size} bytes, ${artifact.file.contentType}${fileUrl === undefined ? "" : `; read at ${fileUrl(artifact.id)}`})`;
|
|
235
|
+
: `${artifact.file.name} (${artifact.file.size} bytes, ${artifact.file.contentType}${fileUrl === undefined ? "" : `; read at ${fileUrl(artifact.id)}`}; console file ${artifact.id})`;
|
|
209
236
|
return artifact.label === null ? what : `${artifact.label}: ${what}`;
|
|
210
237
|
}
|
|
211
238
|
|
|
@@ -287,9 +314,14 @@ function progressOf(goal: RestGoal): string {
|
|
|
287
314
|
return `${counts.done}/${total} done${working === 0 ? "" : `, ${working} in progress`}${counts.dropped === 0 ? "" : `, ${counts.dropped} dropped`}`;
|
|
288
315
|
}
|
|
289
316
|
|
|
290
|
-
/** One goal: its id, its state, its title,
|
|
317
|
+
/** One goal: its id, its state, its title, its category, its days, and how far it is. */
|
|
291
318
|
export function formatGoalLine(goal: RestGoal): string {
|
|
292
|
-
const notes = [
|
|
319
|
+
const notes = [
|
|
320
|
+
...(goal.category === null ? [] : [goal.category.key]),
|
|
321
|
+
...(goal.startsOn === null ? [] : [`starts ${goal.startsOn}`]),
|
|
322
|
+
goal.dueOn === null ? "no target day" : `due ${goal.dueOn}`,
|
|
323
|
+
progressOf(goal),
|
|
324
|
+
];
|
|
293
325
|
return `${goal.id} ${goal.state} ${goal.title} (${notes.join("; ")})`;
|
|
294
326
|
}
|
|
295
327
|
|
|
@@ -297,7 +329,7 @@ export function formatGoalLine(goal: RestGoal): string {
|
|
|
297
329
|
export function formatGoal(goal: RestGoal, tasks?: readonly RestTask[]): string {
|
|
298
330
|
const lines = [
|
|
299
331
|
`goal ${goal.id}: ${goal.title}`,
|
|
300
|
-
`state: ${goal.state} due: ${goal.dueOn ?? "none"} set by: ${goal.createdBy.login}`,
|
|
332
|
+
`state: ${goal.state} category: ${goal.category?.key ?? "none"} starts: ${goal.startsOn ?? "none"} due: ${goal.dueOn ?? "none"} set by: ${goal.createdBy.login}`,
|
|
301
333
|
`progress: ${progressOf(goal)}`,
|
|
302
334
|
];
|
|
303
335
|
if (goal.body.trim().length > 0) {
|
package/src/cli/help.ts
CHANGED
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
MAX_CATEGORIES,
|
|
7
7
|
MAX_CATEGORY_KEY_LENGTH,
|
|
8
8
|
MAX_CATEGORY_NAME_LENGTH,
|
|
9
|
+
MAX_COMMENTS_PER_TASK,
|
|
9
10
|
MAX_DOCUMENT_LENGTH,
|
|
10
11
|
MAX_DOCUMENT_PATH_LENGTH,
|
|
11
12
|
MAX_FILE_BYTES,
|
|
13
|
+
MAX_FILES_PER_COMMENT,
|
|
12
14
|
MAX_FILES_PER_DOCUMENT,
|
|
13
15
|
MAX_LINK_LABEL_LENGTH,
|
|
14
16
|
MAX_OPTION_LABEL_LENGTH,
|
|
@@ -124,7 +126,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
124
126
|
task: {
|
|
125
127
|
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}`,
|
|
126
128
|
about:
|
|
127
|
-
"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
|
|
129
|
+
"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, the runs on it and the discussion on it, each comment whole with its files; 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).",
|
|
128
130
|
limits: [
|
|
129
131
|
`title: one line, up to ${MAX_TITLE_LENGTH} characters, saying what is to be done on its own; what it refers to goes in the body`,
|
|
130
132
|
`body: ${MARKDOWN}`,
|
|
@@ -133,22 +135,24 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
133
135
|
refusals: ["task.not_found: no task has that number or id", ...TASK_FIELD_REFUSALS],
|
|
134
136
|
},
|
|
135
137
|
goals: {
|
|
136
|
-
usage: "goals",
|
|
138
|
+
usage: "goals [--category <key>] [--state <state>]",
|
|
137
139
|
about:
|
|
138
|
-
"The roadmap of the project: its goals, the active first, each with its id, its state (planned, active, done, dropped), its
|
|
140
|
+
"The roadmap of the project: its goals, the active first, each with its id, its state (planned, active, done, dropped), its category, the days it starts and is meant to be reached by, and how far it is, counted from the tasks that work towards it. --category keeps those sorted into one, --state those in one state.",
|
|
139
141
|
},
|
|
140
142
|
goal: {
|
|
141
143
|
usage:
|
|
142
|
-
'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>]',
|
|
144
|
+
'goal <id> | goal new "<title>" [--body <markdown>|--file <path>] [--state <state>] [--category <key>] [--starts <day>] [--due <day>] | goal edit <id> [--title <title>] [--body <markdown>|--file <path>] [--state <state>] [--category <key|none>] [--starts <day|none>] [--due <day|none>]',
|
|
143
145
|
about:
|
|
144
|
-
"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.",
|
|
146
|
+
"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, where none clears a category or a day. A goal is planned in days as a task is, a start and a target, and sorted into a category of the workspace (console categories). A task works towards a goal through its --goal.",
|
|
145
147
|
limits: [
|
|
146
148
|
`title: one line, up to ${MAX_TITLE_LENGTH} characters`,
|
|
147
149
|
`body: ${MARKDOWN}`,
|
|
148
|
-
`due: ${DAY}`,
|
|
150
|
+
`starts, due: ${DAY}`,
|
|
149
151
|
],
|
|
150
152
|
refusals: [
|
|
151
153
|
"goal.not_found: no goal of this project has that id",
|
|
154
|
+
"goal.invalid_category: no category of the workspace has that key",
|
|
155
|
+
"goal.invalid_dates: the target day is before the start",
|
|
152
156
|
"auth.forbidden: only an owner of the project sets its roadmap",
|
|
153
157
|
],
|
|
154
158
|
},
|
|
@@ -336,6 +340,30 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
336
340
|
about: "One run in full: its reports, what it handed in, what it waits for, how it ended.",
|
|
337
341
|
refusals: ["run.not_found: no run has that id"],
|
|
338
342
|
},
|
|
343
|
+
comment: {
|
|
344
|
+
usage:
|
|
345
|
+
'comment <number|id> "<markdown>" [--attach <path>]... | comment <number|id> --file <path> | comment <number|id> -',
|
|
346
|
+
about:
|
|
347
|
+
"Says something in the discussion on a task, for its people: an answer to what they asked there, a question that is not a decision, a remark; with files from this machine put in it, which the console keeps and shows on the task, pictures in place. Needs no run. A report is the run's record; a comment is a word to people. Never rewritten: a mistake is corrected in the next one.",
|
|
348
|
+
limits: [
|
|
349
|
+
`the comment: ${MARKDOWN}; blank only when a file comes with it`,
|
|
350
|
+
`a file: up to ${MAX_FILE_BYTES} bytes, not empty; called by its name, without the path; a picture is placed in the text as `,
|
|
351
|
+
`at most ${MAX_FILES_PER_COMMENT} files on one comment, together up to ${MAX_FILE_BYTES} bytes`,
|
|
352
|
+
`at most ${MAX_COMMENTS_PER_TASK} comments on one task`,
|
|
353
|
+
],
|
|
354
|
+
refusals: [
|
|
355
|
+
"task.not_found: no task has that number or id",
|
|
356
|
+
"comment.too_many: the task carries as many comments as one may",
|
|
357
|
+
"comment.too_many_files: more files than one comment may carry",
|
|
358
|
+
"request.too_large: the files are over the limit together",
|
|
359
|
+
],
|
|
360
|
+
},
|
|
361
|
+
file: {
|
|
362
|
+
usage: "file <id> [--out <path>]",
|
|
363
|
+
about:
|
|
364
|
+
"Reads a file of the project from the console and writes it on this machine: one a run handed in, one put in a comment, or one attached to a page, by the id the task, the run or the page shows. Written under its own name in the working directory, at --out <path>, or into --out <directory>. Then look at it with your own tools.",
|
|
365
|
+
refusals: ["file.not_found: no file of this project has that id"],
|
|
366
|
+
},
|
|
339
367
|
docs: {
|
|
340
368
|
usage: "docs [<folder>] [--search <words>] [--archived]",
|
|
341
369
|
about:
|
|
@@ -447,7 +475,10 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
|
|
|
447
475
|
"rules",
|
|
448
476
|
],
|
|
449
477
|
},
|
|
450
|
-
{
|
|
478
|
+
{
|
|
479
|
+
title: "Working",
|
|
480
|
+
commands: ["take", "report", "hand-in", "comment", "file", "ask", "finish", "fail", "abandon"],
|
|
481
|
+
},
|
|
451
482
|
{
|
|
452
483
|
title: "The documents: the project's pages of Markdown, in folders, by path",
|
|
453
484
|
commands: ["docs", "doc", "write", "attach", "archive", "restore"],
|