@skillcdn/console 0.3.1 → 0.4.0
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 -4
- package/dist/api.d.ts +4 -4
- 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 +4 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +201 -25
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +15 -11
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +59 -26
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +1 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/pack.d.ts +7 -2
- package/dist/cli/pack.d.ts.map +1 -1
- package/dist/cli/pack.js +16 -2
- package/dist/cli/pack.js.map +1 -1
- package/dist/client.d.ts +20 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +17 -3
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +28 -6
- package/dist/compat.d.ts.map +1 -1
- package/dist/compat.js +55 -14
- package/dist/compat.js.map +1 -1
- package/dist/components/board.d.ts +12 -1
- package/dist/components/board.d.ts.map +1 -1
- package/dist/components/board.js +57 -7
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +17 -7
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +89 -38
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-form.d.ts +12 -4
- package/dist/components/decision-form.d.ts.map +1 -1
- package/dist/components/decision-form.js +24 -6
- package/dist/components/decision-form.js.map +1 -1
- package/dist/components/decision-list.d.ts +4 -0
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +9 -6
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/documents.d.ts.map +1 -1
- package/dist/components/documents.js +3 -3
- package/dist/components/documents.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +63 -8
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/my-decisions.d.ts +20 -0
- package/dist/components/my-decisions.d.ts.map +1 -0
- package/dist/components/my-decisions.js +36 -0
- package/dist/components/my-decisions.js.map +1 -0
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +1 -1
- package/dist/components/people.js.map +1 -1
- package/dist/components/picker.d.ts +39 -0
- package/dist/components/picker.d.ts.map +1 -0
- package/dist/components/picker.js +155 -0
- package/dist/components/picker.js.map +1 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -3
- package/dist/components/projects.js.map +1 -1
- package/dist/components/roadmap.js +1 -1
- package/dist/components/roadmap.js.map +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +1 -1
- package/dist/components/runs.js.map +1 -1
- package/dist/components/shell.d.ts.map +1 -1
- package/dist/components/shell.js +36 -1
- package/dist/components/shell.js.map +1 -1
- package/dist/components/task-form.d.ts +18 -4
- package/dist/components/task-form.d.ts.map +1 -1
- package/dist/components/task-form.js +56 -13
- package/dist/components/task-form.js.map +1 -1
- package/dist/components/task-list.js +2 -2
- package/dist/components/task-list.js.map +1 -1
- package/dist/components/task-view.d.ts +7 -1
- package/dist/components/task-view.d.ts.map +1 -1
- package/dist/components/task-view.js +7 -6
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +17 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +26 -11
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +5 -3
- package/dist/components/work.js.map +1 -1
- package/dist/console.css +1794 -433
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +93 -10
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +21 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +24 -1
- package/dist/data.js.map +1 -1
- package/dist/i18n/en.d.ts +45 -3
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +52 -10
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +54 -12
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +9 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +12 -0
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +3 -1
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +3 -1
- package/dist/routes.js.map +1 -1
- package/dist/rules.js +1 -1
- package/dist/rules.js.map +1 -1
- package/dist/schemas.d.ts +252 -12
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +27 -10
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +2 -0
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +2 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +14 -13
- package/docs/specs/rest.md +24 -19
- package/package.json +2 -2
- package/skills/building-a-console/SKILL.md +4 -4
- package/skills/building-a-console/references/the-default-console.md +4 -4
- package/skills/building-a-console/references/the-look-from-a-website.md +5 -5
- package/skills/deploying-a-console/SKILL.md +2 -2
- package/skills/deploying-a-console/references/resources.md +1 -1
- package/skills/working-the-board/SKILL.md +8 -6
- package/skills/working-the-board/references/terms.md +3 -3
- package/src/api.ts +7 -0
- package/src/cli/cli.ts +254 -25
- package/src/cli/format.ts +22 -11
- package/src/cli/help.ts +63 -26
- package/src/cli/main.ts +1 -0
- package/src/cli/pack.ts +17 -1
- package/src/client.ts +44 -11
- package/src/compat.ts +63 -14
- package/src/components/board.tsx +124 -25
- package/src/components/calendar.tsx +258 -117
- package/src/components/decision-form.tsx +71 -23
- package/src/components/decision-list.tsx +30 -3
- package/src/components/documents.tsx +27 -11
- package/src/components/event-feed.tsx +116 -33
- package/src/components/my-decisions.tsx +138 -0
- package/src/components/people.tsx +3 -1
- package/src/components/picker.tsx +297 -0
- package/src/components/projects.tsx +7 -5
- package/src/components/roadmap.tsx +1 -1
- package/src/components/runs.tsx +5 -1
- package/src/components/shell.tsx +36 -1
- package/src/components/task-form.tsx +149 -63
- package/src/components/task-list.tsx +3 -3
- package/src/components/task-view.tsx +71 -56
- package/src/components/ui.tsx +89 -11
- package/src/components/work.tsx +6 -4
- package/src/console.tsx +157 -29
- package/src/data.ts +61 -1
- package/src/i18n/en.ts +54 -10
- package/src/i18n/ko.ts +56 -12
- package/src/index.ts +19 -1
- package/src/limits.ts +2 -0
- package/src/router.ts +18 -1
- package/src/routes.ts +3 -1
- package/src/rules.ts +1 -1
- package/src/schemas.ts +29 -9
- package/src/styles/console.css +1794 -433
- package/src/vocabulary.ts +3 -0
package/src/cli/cli.ts
CHANGED
|
@@ -7,8 +7,18 @@ import {
|
|
|
7
7
|
type FetchLike,
|
|
8
8
|
type ProjectClient,
|
|
9
9
|
} from "../client.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
CLIENT_RANGE_HEADER,
|
|
12
|
+
CONSOLE_AT_LEAST,
|
|
13
|
+
consoleRange,
|
|
14
|
+
formatRange,
|
|
15
|
+
judgeClient,
|
|
16
|
+
judgeConsole,
|
|
17
|
+
parseVersion,
|
|
18
|
+
VERSION_HEADER,
|
|
19
|
+
formatVersion as versionText,
|
|
20
|
+
} from "../compat.js";
|
|
21
|
+
import { addDays, dayOf as dayOfDate, firstDayOfWeek, isDay, weekOf } from "../dates.js";
|
|
12
22
|
import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
13
23
|
import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
|
|
14
24
|
import {
|
|
@@ -168,6 +178,10 @@ export interface CliIo {
|
|
|
168
178
|
readonly install?: Install | undefined;
|
|
169
179
|
/** What the command last said of the versions; left out, it says it every time. */
|
|
170
180
|
readonly notices?: NoticeStore | undefined;
|
|
181
|
+
/** The oldest console this command works with (ADR-0038); left out, the package's own floor. */
|
|
182
|
+
readonly consoleAtLeast?: string | undefined;
|
|
183
|
+
/** The language and region of this machine (`ko-KR`), for what follows a language where the person chose none; left out, English. */
|
|
184
|
+
readonly locale?: string | undefined;
|
|
171
185
|
/** Where the console's own skills are on this machine, shipped with the package; nothing when they are not there. */
|
|
172
186
|
readonly skillsDir?: string | undefined;
|
|
173
187
|
/** How `serve` listens on the loopback (ADR-0014); left out, the command cannot serve. */
|
|
@@ -889,11 +903,15 @@ const whoami: Command = async (args, io) => {
|
|
|
889
903
|
command === undefined || server === undefined
|
|
890
904
|
? undefined
|
|
891
905
|
: judgeClient(command, server).verdict;
|
|
906
|
+
// The oldest console this command works with (ADR-0038), as it stands within the command's line.
|
|
907
|
+
const atLeast = io.consoleAtLeast ?? CONSOLE_AT_LEAST;
|
|
908
|
+
const floor =
|
|
909
|
+
command === undefined ? atLeast : versionText(consoleRange(command, atLeast).atLeast);
|
|
892
910
|
// A console from before the rule says nothing of itself; the command says so instead.
|
|
893
911
|
const consoleWords =
|
|
894
912
|
me.console === undefined
|
|
895
913
|
? "the console does not say its version, so it is older than 0.1.18"
|
|
896
|
-
: `the console is ${me.console.version} and takes ${me.console.clientRange}`;
|
|
914
|
+
: `the console is ${me.console.version} and takes ${me.console.clientRange}; this command works with a console from ${floor} on`;
|
|
897
915
|
answer(
|
|
898
916
|
io,
|
|
899
917
|
session,
|
|
@@ -975,6 +993,16 @@ const use: Command = async (args, io) => {
|
|
|
975
993
|
return EXIT.ok;
|
|
976
994
|
};
|
|
977
995
|
|
|
996
|
+
/**
|
|
997
|
+
* The last day of this week (ADR-0039): the week begins as the workspace says, or, left to the
|
|
998
|
+
* language, as the person's language has it, else this machine's.
|
|
999
|
+
*/
|
|
1000
|
+
async function endOfWeek(io: CliIo, session: Session, today: string): Promise<string> {
|
|
1001
|
+
const me = await session.client.me();
|
|
1002
|
+
const language = me.language ?? io.locale ?? "en";
|
|
1003
|
+
return weekOf(today, firstDayOfWeek(language, me.workspace.weekStart ?? "auto")).at(-1) ?? today;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
978
1006
|
const tasks: Command = async (args, io) => {
|
|
979
1007
|
const { values } = parse(
|
|
980
1008
|
args,
|
|
@@ -986,6 +1014,7 @@ const tasks: Command = async (args, io) => {
|
|
|
986
1014
|
category: { type: "string" },
|
|
987
1015
|
goal: { type: "string" },
|
|
988
1016
|
due: { type: "string" },
|
|
1017
|
+
search: { type: "string" },
|
|
989
1018
|
archived: { type: "boolean" },
|
|
990
1019
|
...PAGE_OPTIONS,
|
|
991
1020
|
},
|
|
@@ -994,7 +1023,7 @@ const tasks: Command = async (args, io) => {
|
|
|
994
1023
|
const state = stateOf(text(values, "state"));
|
|
995
1024
|
if (on(values, "mine") && on(values, "unassigned")) {
|
|
996
1025
|
throw misuse(
|
|
997
|
-
"Say one: --mine for the tasks
|
|
1026
|
+
"Say one: --mine for the tasks you work on, or --unassigned for those nobody works on",
|
|
998
1027
|
);
|
|
999
1028
|
}
|
|
1000
1029
|
const page = pageOf(values);
|
|
@@ -1009,19 +1038,22 @@ const tasks: Command = async (args, io) => {
|
|
|
1009
1038
|
: due === "overdue"
|
|
1010
1039
|
? addDays(today, -1)
|
|
1011
1040
|
: due === "week"
|
|
1012
|
-
?
|
|
1041
|
+
? await endOfWeek(io, session, today)
|
|
1013
1042
|
: (dayOf(io, due) ?? undefined);
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1043
|
+
const worker = on(values, "mine") ? ((await personIdOf(session, "me")) ?? undefined) : undefined;
|
|
1044
|
+
const search = text(values, "search")?.trim();
|
|
1045
|
+
if (search === "") {
|
|
1046
|
+
throw misuse("Say what to search for: console tasks --search <words>");
|
|
1047
|
+
}
|
|
1017
1048
|
const found = await project.tasks({
|
|
1018
1049
|
...(state === undefined ? {} : { state }),
|
|
1019
1050
|
...(on(values, "open") || due === "overdue" ? { open: true } : {}),
|
|
1020
|
-
...(
|
|
1051
|
+
...(worker === undefined ? {} : { worker }),
|
|
1021
1052
|
...(on(values, "unassigned") ? { unassigned: true } : {}),
|
|
1022
1053
|
...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
|
|
1023
1054
|
...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
|
|
1024
1055
|
...(dueBy === undefined ? {} : { dueBy }),
|
|
1056
|
+
...(search === undefined ? {} : { q: search }),
|
|
1025
1057
|
...(on(values, "archived") ? { archived: true } : {}),
|
|
1026
1058
|
...page,
|
|
1027
1059
|
});
|
|
@@ -1140,8 +1172,37 @@ const skills: Command = async (args, io) => {
|
|
|
1140
1172
|
return EXIT.ok;
|
|
1141
1173
|
};
|
|
1142
1174
|
|
|
1175
|
+
/** The most characters one read of a file asks for: what the console's API allows on one page. */
|
|
1176
|
+
const MAX_FILE_LIMIT = 100_000;
|
|
1177
|
+
|
|
1178
|
+
/** A count given as a flag, within its bounds, or nothing; anything else is not understood. */
|
|
1179
|
+
function countOf(
|
|
1180
|
+
given: string | undefined,
|
|
1181
|
+
name: string,
|
|
1182
|
+
bounds: { readonly least: number; readonly most?: number },
|
|
1183
|
+
): number | undefined {
|
|
1184
|
+
if (given === undefined) {
|
|
1185
|
+
return undefined;
|
|
1186
|
+
}
|
|
1187
|
+
const count = Number(given);
|
|
1188
|
+
if (
|
|
1189
|
+
!Number.isInteger(count) ||
|
|
1190
|
+
count < bounds.least ||
|
|
1191
|
+
(bounds.most !== undefined && count > bounds.most)
|
|
1192
|
+
) {
|
|
1193
|
+
const range =
|
|
1194
|
+
bounds.most === undefined ? `${bounds.least} or more` : `${bounds.least} to ${bounds.most}`;
|
|
1195
|
+
throw misuse(`Not a number for --${name}, ${range}: ${given}`);
|
|
1196
|
+
}
|
|
1197
|
+
return count;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1143
1200
|
const skill: Command = async (args, io) => {
|
|
1144
|
-
const { values, positionals } = parse(args, {
|
|
1201
|
+
const { values, positionals } = parse(args, {
|
|
1202
|
+
file: { type: "string" },
|
|
1203
|
+
offset: { type: "string" },
|
|
1204
|
+
limit: { type: "string" },
|
|
1205
|
+
});
|
|
1145
1206
|
const file = text(values, "file");
|
|
1146
1207
|
// A file is read by its own path from the repository's root; a skill's path beside it would be
|
|
1147
1208
|
// ignored, which reads as if it mattered, so the two together are not understood.
|
|
@@ -1150,6 +1211,17 @@ const skill: Command = async (args, io) => {
|
|
|
1150
1211
|
"Say one: console skill <path of its SKILL.md> for a skill, or console skill --file <path> for a file of the pack.",
|
|
1151
1212
|
);
|
|
1152
1213
|
}
|
|
1214
|
+
// A section is a page of a file read as asked (SkillCDN 0.4): the flags mean nothing without one.
|
|
1215
|
+
if (
|
|
1216
|
+
file === undefined &&
|
|
1217
|
+
(text(values, "offset") !== undefined || text(values, "limit") !== undefined)
|
|
1218
|
+
) {
|
|
1219
|
+
throw misuse(
|
|
1220
|
+
"--offset and --limit go with --file: console skill --file <path> [--offset <n>] [--limit <n>]",
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
const offset = countOf(text(values, "offset"), "offset", { least: 0 });
|
|
1224
|
+
const limit = countOf(text(values, "limit"), "limit", { least: 1, most: MAX_FILE_LIMIT });
|
|
1153
1225
|
const session = await open(io, values);
|
|
1154
1226
|
const project = await session.project();
|
|
1155
1227
|
const { listing, summary } = await packOf(project);
|
|
@@ -1157,6 +1229,16 @@ const skill: Command = async (args, io) => {
|
|
|
1157
1229
|
answer(io, session, listing, () => formatSkills(listing, io.skillsDir));
|
|
1158
1230
|
return EXIT.ok;
|
|
1159
1231
|
}
|
|
1232
|
+
if (file !== undefined && (offset !== undefined || limit !== undefined)) {
|
|
1233
|
+
// One page as asked, the section a search or an outline named; the result says where the
|
|
1234
|
+
// file continues, and asks for nothing.
|
|
1235
|
+
const page = await project.skillFile(file, { offset, limit });
|
|
1236
|
+
const section = page.read === null ? undefined : joinFile([page], summary, limit);
|
|
1237
|
+
answer(io, session, { ...page, read: section === undefined ? null : bodyOfFile(section) }, () =>
|
|
1238
|
+
formatSkillFile(page, section),
|
|
1239
|
+
);
|
|
1240
|
+
return EXIT.ok;
|
|
1241
|
+
}
|
|
1160
1242
|
if (file !== undefined) {
|
|
1161
1243
|
// The file whole, page after page, as the console reads it from the pack.
|
|
1162
1244
|
const pages: RestSkillFile[] = [];
|
|
@@ -1335,10 +1417,42 @@ const TASK_FIELDS: OptionSpec = {
|
|
|
1335
1417
|
goal: { type: "string" },
|
|
1336
1418
|
starts: { type: "string" },
|
|
1337
1419
|
due: { type: "string" },
|
|
1338
|
-
|
|
1420
|
+
worker: { type: "string", multiple: true },
|
|
1339
1421
|
parent: { type: "string" },
|
|
1340
1422
|
};
|
|
1341
1423
|
|
|
1424
|
+
/**
|
|
1425
|
+
* The people `--worker` names, each a login or `me`, given again or with commas between for
|
|
1426
|
+
* several, in place of those there were; `none` alone for nobody; nothing when the flag was
|
|
1427
|
+
* not given.
|
|
1428
|
+
*/
|
|
1429
|
+
async function workerIdsOf(
|
|
1430
|
+
session: Session,
|
|
1431
|
+
given: readonly string[],
|
|
1432
|
+
): Promise<string[] | undefined> {
|
|
1433
|
+
if (given.length === 0) {
|
|
1434
|
+
return undefined;
|
|
1435
|
+
}
|
|
1436
|
+
const names = given
|
|
1437
|
+
.flatMap((value) => value.split(","))
|
|
1438
|
+
.map((name) => name.trim())
|
|
1439
|
+
.filter((name) => name.length > 0);
|
|
1440
|
+
if (names.includes("none")) {
|
|
1441
|
+
if (names.length > 1) {
|
|
1442
|
+
throw misuse("Say --worker none alone, for nobody; or the logins of the workers.");
|
|
1443
|
+
}
|
|
1444
|
+
return [];
|
|
1445
|
+
}
|
|
1446
|
+
const ids: string[] = [];
|
|
1447
|
+
for (const name of names) {
|
|
1448
|
+
const id = await personIdOf(session, name);
|
|
1449
|
+
if (typeof id === "string" && !ids.includes(id)) {
|
|
1450
|
+
ids.push(id);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return ids;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1342
1456
|
/** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
|
|
1343
1457
|
async function taskFieldsOf(
|
|
1344
1458
|
io: CliIo,
|
|
@@ -1352,7 +1466,7 @@ async function taskFieldsOf(
|
|
|
1352
1466
|
const goalId = orNone(text(values, "goal"));
|
|
1353
1467
|
const startsOn = dayOf(io, text(values, "starts"), true);
|
|
1354
1468
|
const dueOn = dayOf(io, text(values, "due"), true);
|
|
1355
|
-
const
|
|
1469
|
+
const workerIds = await workerIdsOf(session, texts(values, "worker"));
|
|
1356
1470
|
const parent = text(values, "parent");
|
|
1357
1471
|
const parentId =
|
|
1358
1472
|
parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
|
|
@@ -1363,7 +1477,7 @@ async function taskFieldsOf(
|
|
|
1363
1477
|
...(goalId === undefined ? {} : { goalId }),
|
|
1364
1478
|
...(startsOn === undefined ? {} : { startsOn }),
|
|
1365
1479
|
...(dueOn === undefined ? {} : { dueOn }),
|
|
1366
|
-
...(
|
|
1480
|
+
...(workerIds === undefined ? {} : { workerIds }),
|
|
1367
1481
|
...(parentId === undefined ? {} : { parentId }),
|
|
1368
1482
|
};
|
|
1369
1483
|
}
|
|
@@ -1415,7 +1529,7 @@ const editTask: Command = async (args, io) => {
|
|
|
1415
1529
|
};
|
|
1416
1530
|
if (Object.keys(patch).length === 0) {
|
|
1417
1531
|
throw misuse(
|
|
1418
|
-
"Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --
|
|
1532
|
+
"Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --worker or --parent; none clears a field.",
|
|
1419
1533
|
);
|
|
1420
1534
|
}
|
|
1421
1535
|
const found = await project.task(ref);
|
|
@@ -1736,11 +1850,16 @@ const ask: Command = async (args, io) => {
|
|
|
1736
1850
|
file: { type: "string" },
|
|
1737
1851
|
run: { type: "string" },
|
|
1738
1852
|
wait: { type: "string" },
|
|
1853
|
+
to: { type: "string" },
|
|
1739
1854
|
});
|
|
1740
1855
|
const question = theOne(
|
|
1741
1856
|
positionals,
|
|
1742
1857
|
'Say the question: console ask "<question>" --option "<label>" --option "<label>"',
|
|
1743
1858
|
);
|
|
1859
|
+
const to = text(values, "to");
|
|
1860
|
+
if (to === "none") {
|
|
1861
|
+
throw misuse("Say whom the decision is for: --to <login|me>.");
|
|
1862
|
+
}
|
|
1744
1863
|
const options = texts(values, "option");
|
|
1745
1864
|
if (options.length < MIN_OPTIONS || options.length > MAX_OPTIONS) {
|
|
1746
1865
|
throw misuse(`Give ${MIN_OPTIONS} to ${MAX_OPTIONS} options, one --option each.`);
|
|
@@ -1750,21 +1869,58 @@ const ask: Command = async (args, io) => {
|
|
|
1750
1869
|
const session = await open(io, values);
|
|
1751
1870
|
const project = await session.project();
|
|
1752
1871
|
const runId = await currentRun(project, values);
|
|
1872
|
+
// Whom the decision is for (ADR-0042): the person the agent works for, unless --to names another.
|
|
1873
|
+
const deciderId = await personIdOf(session, to);
|
|
1753
1874
|
const raised = await project.raiseDecision({
|
|
1754
1875
|
question,
|
|
1755
1876
|
options: options.map((label) => ({ label })),
|
|
1756
1877
|
runId,
|
|
1757
1878
|
...(body === undefined ? {} : { body }),
|
|
1879
|
+
...(typeof deciderId === "string" ? { deciderId } : {}),
|
|
1758
1880
|
});
|
|
1759
1881
|
// The id is printed before any waiting, so that a wait cut short loses nothing.
|
|
1760
1882
|
io.stdout(
|
|
1761
1883
|
session.json
|
|
1762
1884
|
? `${JSON.stringify(raised, null, 2)}\n`
|
|
1763
|
-
: `Decision ${raised.id} raised on run ${runId}; the run waits for
|
|
1885
|
+
: `Decision ${raised.id} raised on run ${runId}; the run waits for ${raised.decider.login}.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`,
|
|
1764
1886
|
);
|
|
1765
1887
|
return settle(io, session, project, raised, seconds, true);
|
|
1766
1888
|
};
|
|
1767
1889
|
|
|
1890
|
+
/**
|
|
1891
|
+
* Writes a person's answer on a decision, in their name through the agent (ADR-0021): the
|
|
1892
|
+
* option they chose, or their own words, and a note about it, so that a choice made in chat
|
|
1893
|
+
* or in passing reaches the record the run waits on.
|
|
1894
|
+
*/
|
|
1895
|
+
const answering: Command = async (args, io) => {
|
|
1896
|
+
const { values, positionals } = parse(args, {
|
|
1897
|
+
option: { type: "string" },
|
|
1898
|
+
own: { type: "string" },
|
|
1899
|
+
note: { type: "string" },
|
|
1900
|
+
file: { type: "string" },
|
|
1901
|
+
});
|
|
1902
|
+
const id = theOne(
|
|
1903
|
+
positionals,
|
|
1904
|
+
'Say which decision: console answer <id> --option <n>|--own "<answer>" [--note <markdown>|--file <path>]',
|
|
1905
|
+
);
|
|
1906
|
+
const option = text(values, "option");
|
|
1907
|
+
const own = text(values, "own");
|
|
1908
|
+
if ((option === undefined) === (own === undefined)) {
|
|
1909
|
+
throw misuse(
|
|
1910
|
+
'Say the option chosen, --option <n>, or the person\'s own answer, --own "<words>": one of the two.',
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
const note = await bodyOf(io, values, text(values, "note"));
|
|
1914
|
+
const session = await open(io, values);
|
|
1915
|
+
const project = await session.project();
|
|
1916
|
+
const answered = await project.answerDecision(id, {
|
|
1917
|
+
...(own === undefined ? { option } : { own }),
|
|
1918
|
+
...(note === undefined ? {} : { note }),
|
|
1919
|
+
});
|
|
1920
|
+
answer(io, session, answered, () => formatDecision(answered));
|
|
1921
|
+
return EXIT.ok;
|
|
1922
|
+
};
|
|
1923
|
+
|
|
1768
1924
|
const decision: Command = async (args, io) => {
|
|
1769
1925
|
const { values, positionals } = parse(args, {
|
|
1770
1926
|
wait: { type: "string" },
|
|
@@ -1956,23 +2112,36 @@ const restore: Command = async (args, io) => {
|
|
|
1956
2112
|
const goals: Command = async (args, io) => {
|
|
1957
2113
|
const { values } = parse(
|
|
1958
2114
|
args,
|
|
1959
|
-
{
|
|
2115
|
+
{
|
|
2116
|
+
category: { type: "string" },
|
|
2117
|
+
state: { type: "string" },
|
|
2118
|
+
search: { type: "string" },
|
|
2119
|
+
...PAGE_OPTIONS,
|
|
2120
|
+
},
|
|
1960
2121
|
false,
|
|
1961
2122
|
);
|
|
1962
2123
|
const state = goalStateOf(text(values, "state"));
|
|
1963
2124
|
const category = text(values, "category");
|
|
2125
|
+
const search = text(values, "search")?.trim();
|
|
2126
|
+
if (search === "") {
|
|
2127
|
+
throw misuse("Say what to search for: console goals --search <words>");
|
|
2128
|
+
}
|
|
2129
|
+
const page = pageOf(values);
|
|
1964
2130
|
const session = await open(io, values);
|
|
1965
2131
|
const project = await session.project();
|
|
1966
2132
|
const found = await project.goals({
|
|
1967
2133
|
...(state === undefined ? {} : { state }),
|
|
1968
2134
|
...(category === undefined ? {} : { category }),
|
|
2135
|
+
...(search === undefined ? {} : { q: search }),
|
|
2136
|
+
...page,
|
|
1969
2137
|
});
|
|
1970
2138
|
answer(io, session, found, () =>
|
|
1971
2139
|
found.items.length === 0
|
|
1972
|
-
? state === undefined && category === undefined
|
|
2140
|
+
? state === undefined && category === undefined && search === undefined
|
|
1973
2141
|
? "No goals. An owner sets one: console goal new <title> --due <day>"
|
|
1974
2142
|
: "No goal matches."
|
|
1975
|
-
: found.items.map(formatGoalLine).join("\n")
|
|
2143
|
+
: found.items.map(formatGoalLine).join("\n") +
|
|
2144
|
+
formatMore(found.more, page.limit, page.offset),
|
|
1976
2145
|
);
|
|
1977
2146
|
return EXIT.ok;
|
|
1978
2147
|
};
|
|
@@ -2062,14 +2231,37 @@ const goal: Command = async (args, io) => {
|
|
|
2062
2231
|
};
|
|
2063
2232
|
|
|
2064
2233
|
const decisions: Command = async (args, io) => {
|
|
2065
|
-
const { values } = parse(
|
|
2234
|
+
const { values } = parse(
|
|
2235
|
+
args,
|
|
2236
|
+
{
|
|
2237
|
+
open: { type: "boolean" },
|
|
2238
|
+
mine: { type: "boolean" },
|
|
2239
|
+
raised: { type: "boolean" },
|
|
2240
|
+
...PAGE_OPTIONS,
|
|
2241
|
+
},
|
|
2242
|
+
false,
|
|
2243
|
+
);
|
|
2066
2244
|
const page = pageOf(values);
|
|
2067
2245
|
const session = await open(io, values);
|
|
2246
|
+
const open_ = on(values, "open") ? { open: true } : {};
|
|
2247
|
+
if (on(values, "mine") || on(values, "raised")) {
|
|
2248
|
+
// One's own across the projects (ADR-0042), addressed to the person or raised by them for
|
|
2249
|
+
// someone else: the waiting first, each with its project's key.
|
|
2250
|
+
const raised = on(values, "raised");
|
|
2251
|
+
const found = await session.client.myDecisions({ ...open_, raised, ...page });
|
|
2252
|
+
answer(io, session, found, () =>
|
|
2253
|
+
found.items.length === 0
|
|
2254
|
+
? raised
|
|
2255
|
+
? "No decisions you asked of others."
|
|
2256
|
+
: "No decisions of yours."
|
|
2257
|
+
: found.items
|
|
2258
|
+
.map(({ project, decision }) => `${project.key} ${formatDecisionLine(decision)}`)
|
|
2259
|
+
.join("\n") + formatMore(found.more, page.limit, page.offset),
|
|
2260
|
+
);
|
|
2261
|
+
return EXIT.ok;
|
|
2262
|
+
}
|
|
2068
2263
|
const project = await session.project();
|
|
2069
|
-
const found = await project.decisions({
|
|
2070
|
-
...(on(values, "open") ? { open: true } : {}),
|
|
2071
|
-
...page,
|
|
2072
|
-
});
|
|
2264
|
+
const found = await project.decisions({ ...open_, ...page });
|
|
2073
2265
|
answer(io, session, found, () =>
|
|
2074
2266
|
found.items.length === 0
|
|
2075
2267
|
? "No decisions."
|
|
@@ -2792,6 +2984,7 @@ const COMMANDS: Readonly<Record<string, Command>> = {
|
|
|
2792
2984
|
comment,
|
|
2793
2985
|
file,
|
|
2794
2986
|
ask,
|
|
2987
|
+
answer: answering,
|
|
2795
2988
|
decision,
|
|
2796
2989
|
decisions,
|
|
2797
2990
|
finish: ending("finished"),
|
|
@@ -2892,11 +3085,39 @@ async function noticeVersions(io: CliIo, seen: ConsoleSeen): Promise<void> {
|
|
|
2892
3085
|
});
|
|
2893
3086
|
}
|
|
2894
3087
|
|
|
3088
|
+
/**
|
|
3089
|
+
* The refusal of a console older than this command works with (ADR-0038), in a console's own
|
|
3090
|
+
* form: a `426` the command reads as it reads the console's, before it acts on anything the
|
|
3091
|
+
* console answered.
|
|
3092
|
+
*/
|
|
3093
|
+
function consoleRefusal(io: CliIo, seen: ConsoleSeen): Response | undefined {
|
|
3094
|
+
const command = parseVersion(io.version);
|
|
3095
|
+
const server = parseVersion(seen.version);
|
|
3096
|
+
if (command === undefined || server === undefined) {
|
|
3097
|
+
return undefined;
|
|
3098
|
+
}
|
|
3099
|
+
const { verdict, range } = judgeConsole(server, command, io.consoleAtLeast ?? CONSOLE_AT_LEAST);
|
|
3100
|
+
if (verdict !== "outdated") {
|
|
3101
|
+
return undefined;
|
|
3102
|
+
}
|
|
3103
|
+
return Response.json(
|
|
3104
|
+
{
|
|
3105
|
+
error: {
|
|
3106
|
+
code: "console.outdated",
|
|
3107
|
+
message: `This console, ${seen.version}, is older than this command (${io.version}) works with (${formatRange(range)}).`,
|
|
3108
|
+
},
|
|
3109
|
+
},
|
|
3110
|
+
{ status: 426 },
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
|
|
2895
3114
|
/** What a refusal of the console is followed by, when there is something to do about it. */
|
|
2896
3115
|
function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
|
|
2897
3116
|
switch (error.code) {
|
|
2898
3117
|
case "client.outdated":
|
|
2899
3118
|
return `Update the command: ${updateWords(io, seen.version)}\n`;
|
|
3119
|
+
case "console.outdated":
|
|
3120
|
+
return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
|
|
2900
3121
|
case "client.too_new":
|
|
2901
3122
|
return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
|
|
2902
3123
|
case "auth.required":
|
|
@@ -2972,7 +3193,13 @@ export async function runCli(argv: readonly string[], io: CliIo): Promise<number
|
|
|
2972
3193
|
const response = await io.fetch(input, init);
|
|
2973
3194
|
seen.version = response.headers.get(VERSION_HEADER) ?? seen.version;
|
|
2974
3195
|
seen.range = response.headers.get(CLIENT_RANGE_HEADER) ?? seen.range;
|
|
2975
|
-
|
|
3196
|
+
// A console older than this command works with is refused here (ADR-0038); `serve`
|
|
3197
|
+
// carries the pages' own requests, which are not the command's to judge.
|
|
3198
|
+
// A refusal of the console's own is passed on as it is.
|
|
3199
|
+
return (
|
|
3200
|
+
(name === "serve" || response.status === 426 ? undefined : consoleRefusal(io, seen)) ??
|
|
3201
|
+
response
|
|
3202
|
+
);
|
|
2976
3203
|
},
|
|
2977
3204
|
};
|
|
2978
3205
|
try {
|
|
@@ -3004,7 +3231,9 @@ export async function runCli(argv: readonly string[], io: CliIo): Promise<number
|
|
|
3004
3231
|
}
|
|
3005
3232
|
if (error instanceof ApiError) {
|
|
3006
3233
|
io.stderr(`${error.code}: ${error.message}\n${hintFor(error, io, seen)}`);
|
|
3007
|
-
return error.code === "client.outdated" ||
|
|
3234
|
+
return error.code === "client.outdated" ||
|
|
3235
|
+
error.code === "client.too_new" ||
|
|
3236
|
+
error.code === "console.outdated"
|
|
3008
3237
|
? EXIT.incompatible
|
|
3009
3238
|
: EXIT.failed;
|
|
3010
3239
|
}
|
package/src/cli/format.ts
CHANGED
|
@@ -147,10 +147,14 @@ export function formatMore(more: boolean, limit: number, offset: number): string
|
|
|
147
147
|
return more ? `\n... more: --offset ${offset + limit}` : "";
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/** The logins of the people at work on a task, in the order they came to it. */
|
|
151
|
+
const logins = (people: readonly RestPerson[]): string =>
|
|
152
|
+
people.map((person) => person.login).join(", ");
|
|
153
|
+
|
|
150
154
|
export function formatTaskLine(task: RestTask): string {
|
|
151
155
|
const notes = [`owner ${task.owner.login}`];
|
|
152
|
-
if (task.
|
|
153
|
-
notes.push(`
|
|
156
|
+
if (task.workers.length > 0) {
|
|
157
|
+
notes.push(`workers ${logins(task.workers)}`);
|
|
154
158
|
}
|
|
155
159
|
if (task.category !== null) {
|
|
156
160
|
notes.push(task.category.key);
|
|
@@ -199,7 +203,7 @@ export function formatTask(
|
|
|
199
203
|
): string {
|
|
200
204
|
const lines = [
|
|
201
205
|
`#${task.number} ${task.title}`,
|
|
202
|
-
`state: ${task.state} priority: ${task.priority} owner: ${task.owner.login}
|
|
206
|
+
`state: ${task.state} priority: ${task.priority} owner: ${task.owner.login} workers: ${task.workers.length === 0 ? "nobody" : logins(task.workers)}`,
|
|
203
207
|
`id: ${task.id}${task.parentId === null ? "" : ` part of: ${task.parentId}`}`,
|
|
204
208
|
...scheduleLines(task),
|
|
205
209
|
];
|
|
@@ -375,14 +379,17 @@ export function formatCategoryLine(category: RestCategory): string {
|
|
|
375
379
|
export function formatWork(work: RestWork): string {
|
|
376
380
|
const lines: string[] = [];
|
|
377
381
|
if (work.tasks.length === 0) {
|
|
378
|
-
lines.push("No open task is
|
|
382
|
+
lines.push("No open task is on your plate.");
|
|
379
383
|
} else {
|
|
380
|
-
lines.push("tasks
|
|
384
|
+
lines.push("tasks you work on:");
|
|
381
385
|
for (const { project, task } of work.tasks) {
|
|
382
386
|
lines.push(` ${project.key} ${formatTaskLine(task)}`);
|
|
383
387
|
}
|
|
384
388
|
}
|
|
385
|
-
lines.push(
|
|
389
|
+
lines.push(
|
|
390
|
+
"",
|
|
391
|
+
work.decisions.length === 0 ? "decisions waiting for you: none" : "decisions waiting for you:",
|
|
392
|
+
);
|
|
386
393
|
for (const { project, decision } of work.decisions) {
|
|
387
394
|
lines.push(` ${project.key} ${formatDecisionLine(decision)}`);
|
|
388
395
|
}
|
|
@@ -392,17 +399,21 @@ export function formatWork(work: RestWork): string {
|
|
|
392
399
|
/** Another person's plate: their open tasks, each with its project; no decisions, which wait for anyone. */
|
|
393
400
|
export function formatPersonWork(person: RestPerson, work: RestPersonWork): string {
|
|
394
401
|
if (work.tasks.length === 0) {
|
|
395
|
-
return `No open task is
|
|
402
|
+
return `No open task is on ${person.login}'s plate.`;
|
|
396
403
|
}
|
|
397
404
|
return [
|
|
398
|
-
`tasks
|
|
405
|
+
`tasks ${person.login} works on:`,
|
|
399
406
|
...work.tasks.map(({ project, task }) => ` ${project.key} ${formatTaskLine(task)}`),
|
|
400
407
|
].join("\n");
|
|
401
408
|
}
|
|
402
409
|
|
|
410
|
+
/** Whom a decision is for, where that is not the one who asked (ADR-0042). */
|
|
411
|
+
const addressed = (decision: RestDecision): string =>
|
|
412
|
+
decision.decider.id === decision.raisedBy.id ? "" : `, for ${decision.decider.login}`;
|
|
413
|
+
|
|
403
414
|
export function formatDecisionLine(decision: RestDecision): string {
|
|
404
415
|
const state = decision.answer === null ? "waiting" : `answered ${decision.answer.option}`;
|
|
405
|
-
return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)})`;
|
|
416
|
+
return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)}${addressed(decision)})`;
|
|
406
417
|
}
|
|
407
418
|
|
|
408
419
|
/** The options, one a line; the person's own answer, when they wrote one, is the last and says so. */
|
|
@@ -419,7 +430,7 @@ export function formatOptions(decision: RestDecision): string {
|
|
|
419
430
|
export function formatAnswer(decision: RestDecision): string {
|
|
420
431
|
const { answer } = decision;
|
|
421
432
|
if (answer === null) {
|
|
422
|
-
return
|
|
433
|
+
return `waits for ${decision.decider.login}.`;
|
|
423
434
|
}
|
|
424
435
|
const chosen = decision.options.find((option) => option.id === answer.option);
|
|
425
436
|
const lines = [
|
|
@@ -434,7 +445,7 @@ export function formatAnswer(decision: RestDecision): string {
|
|
|
434
445
|
export function formatDecision(decision: RestDecision): string {
|
|
435
446
|
const lines = [
|
|
436
447
|
`decision ${decision.id}: ${decision.question}`,
|
|
437
|
-
`asked by ${asker(decision)}${decision.taskNumber === null ? "" : `, about task #${decision.taskNumber}`}`,
|
|
448
|
+
`asked by ${asker(decision)}${addressed(decision)}${decision.taskNumber === null ? "" : `, about task #${decision.taskNumber}`}`,
|
|
438
449
|
];
|
|
439
450
|
if (decision.body.trim().length > 0) {
|
|
440
451
|
lines.push("", decision.body.trimEnd());
|