@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/dist/cli/cli.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { basename, extname } from "node:path";
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
import { ApiError, createClient, } from "../client.js";
|
|
4
|
+
import { CLIENT_RANGE_HEADER, judgeClient, parseVersion, VERSION_HEADER } from "../compat.js";
|
|
4
5
|
import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
|
|
5
6
|
import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
6
7
|
import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
|
|
7
8
|
import { hostOfLogin, } from "../schemas.js";
|
|
8
9
|
import { COLOR_NAMES, GOAL_STATES, PERSON_ROLES, PROJECT_ROLES, PROJECT_VISIBILITIES, PROVIDER_KEYS, TASK_PRIORITIES, TASK_STATES, } from "../vocabulary.js";
|
|
9
10
|
import { loadWebRoot, WebRootError } from "../web.js";
|
|
10
|
-
import { formatAnswer, formatCategoryLine, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatSkills, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, } from "./format.js";
|
|
11
|
+
import { formatAnswer, formatCategoryLine, formatComment, formatDecision, formatDecisionLine, formatDocument, formatDocuments, formatGoal, formatGoalLine, formatListingLine, formatMemberLine, formatMore, formatOptions, formatPersonLine, formatPersonWork, formatProject, formatProjectLine, formatRules, formatRun, formatRunLine, formatSkills, formatTask, formatTaskLine, formatVersion, formatVersionLine, formatWork, } from "./format.js";
|
|
11
12
|
import { COMMAND_HELP, helpFor, usage } from "./help.js";
|
|
13
|
+
import { describeInstall, updateCommandFor } from "./install.js";
|
|
12
14
|
import { createServeHandler, DEFAULT_SERVE_PORT } from "./serve.js";
|
|
13
15
|
// The `console` command (docs/specs/cli.md): the agent's side of the console. A thin client of
|
|
14
16
|
// the REST API with no logic of its own: every command is one or two requests and the words to
|
|
@@ -24,7 +26,11 @@ export const EXIT = {
|
|
|
24
26
|
usage: 2,
|
|
25
27
|
/** The decision still waits for a person. */
|
|
26
28
|
waiting: 3,
|
|
29
|
+
/** The command and the console do not go together (ADR-0025): stderr says which to update, and how. */
|
|
30
|
+
incompatible: 4,
|
|
27
31
|
};
|
|
32
|
+
/** A day, for how often the command repeats what it noticed of the versions. */
|
|
33
|
+
const DAY_MS = 86_400_000;
|
|
28
34
|
/** How long one request for a decision's answer asks the console to hold it: what the server allows. */
|
|
29
35
|
const WAIT_STEP_SECONDS = 50;
|
|
30
36
|
/** A breath between two such requests, should the console answer at once. */
|
|
@@ -83,7 +89,7 @@ async function open(io, values) {
|
|
|
83
89
|
if (url === undefined || token === undefined) {
|
|
84
90
|
throw failed("Not signed in. A person signs the command in: console login --url <the console's origin>");
|
|
85
91
|
}
|
|
86
|
-
const client =
|
|
92
|
+
const client = clientOf(io, url, token);
|
|
87
93
|
const projectKey = async () => text(values, "project") ?? io.env.CONSOLE_PROJECT ?? (await io.directory.load())?.project;
|
|
88
94
|
return {
|
|
89
95
|
client,
|
|
@@ -323,7 +329,7 @@ async function settle(io, session, project, decision, seconds, shown = false) {
|
|
|
323
329
|
}
|
|
324
330
|
/** Checks a token against the console, keeps it with the address, and says who it is. */
|
|
325
331
|
async function keep(io, url, token, agent) {
|
|
326
|
-
const me = await
|
|
332
|
+
const me = await clientOf(io, url, token).me();
|
|
327
333
|
const person = me.person;
|
|
328
334
|
if (person === null) {
|
|
329
335
|
throw failed("The console does not know this token. Make one on your Agents page and try again.");
|
|
@@ -349,7 +355,7 @@ const login = async (args, io) => {
|
|
|
349
355
|
}
|
|
350
356
|
// A connection (ADR-0011): a code a person approves on the console's own pages, and the token
|
|
351
357
|
// handed over here once, never shown.
|
|
352
|
-
const client =
|
|
358
|
+
const client = clientOf(io, url);
|
|
353
359
|
// "<agent> on <machine>": the pages say the agent and keep the machine for a hover.
|
|
354
360
|
const agent = text(values, "agent") ?? agentOf(io.env) ?? "An agent";
|
|
355
361
|
const begun = await client.connect({
|
|
@@ -387,7 +393,7 @@ const logout = async (args, io) => {
|
|
|
387
393
|
// Agents page, and the person is told so.
|
|
388
394
|
let gone = true;
|
|
389
395
|
try {
|
|
390
|
-
await
|
|
396
|
+
await clientOf(io, url, token).giveUpToken();
|
|
391
397
|
}
|
|
392
398
|
catch (error) {
|
|
393
399
|
if (!(error instanceof ApiError && error.code === "auth.required")) {
|
|
@@ -422,7 +428,31 @@ const whoami = async (args, io) => {
|
|
|
422
428
|
throw failed("The console does not know this token. Sign in again: console login");
|
|
423
429
|
}
|
|
424
430
|
const key = await session.projectKey();
|
|
425
|
-
|
|
431
|
+
const command = parseVersion(io.version);
|
|
432
|
+
const server = parseVersion(me.console?.version);
|
|
433
|
+
const verdict = command === undefined || server === undefined
|
|
434
|
+
? undefined
|
|
435
|
+
: judgeClient(command, server).verdict;
|
|
436
|
+
// A console from before the rule says nothing of itself; the command says so instead.
|
|
437
|
+
const consoleWords = me.console === undefined
|
|
438
|
+
? "the console does not say its version, so it is older than 0.1.18"
|
|
439
|
+
: `the console is ${me.console.version} and takes ${me.console.clientRange}`;
|
|
440
|
+
answer(io, session, {
|
|
441
|
+
...me,
|
|
442
|
+
console: me.console ?? null,
|
|
443
|
+
project: key ?? null,
|
|
444
|
+
command: { version: io.version ?? null, install: io.install ?? null },
|
|
445
|
+
}, () => [
|
|
446
|
+
`${person.login} (${person.role}) at ${me.workspace.name}, ${session.url}${key === undefined ? "; no project named here" : `; project ${key}`}`,
|
|
447
|
+
`This command is ${io.version ?? "of a version it does not know"}${io.install === undefined ? "" : `, ${describeInstall(io.install)}`}; ${consoleWords}.`,
|
|
448
|
+
...(verdict === "behind"
|
|
449
|
+
? [`A newer command exists. Update it: ${updateWords(io, me.console?.version)}`]
|
|
450
|
+
: verdict === "ahead"
|
|
451
|
+
? [
|
|
452
|
+
`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)}`,
|
|
453
|
+
]
|
|
454
|
+
: []),
|
|
455
|
+
].join("\n"));
|
|
426
456
|
return EXIT.ok;
|
|
427
457
|
};
|
|
428
458
|
const projects = async (args, io) => {
|
|
@@ -697,11 +727,82 @@ const task = async (args, io) => {
|
|
|
697
727
|
const session = await open(io, values);
|
|
698
728
|
const project = await session.project();
|
|
699
729
|
const found = await project.task(ref);
|
|
700
|
-
const [decisions, runs] = await Promise.all([
|
|
730
|
+
const [decisions, runs, comments] = await Promise.all([
|
|
701
731
|
project.decisions({ task: found.id }),
|
|
702
732
|
project.runs({ task: found.id }),
|
|
733
|
+
project.comments(found.id),
|
|
703
734
|
]);
|
|
704
|
-
answer(io, session, { task: found, decisions: decisions.items, runs: runs.items }, () => formatTask(found, decisions.items, runs.items));
|
|
735
|
+
answer(io, session, { task: found, decisions: decisions.items, runs: runs.items, comments: comments.items }, () => formatTask(found, decisions.items, runs.items, comments.items));
|
|
736
|
+
return EXIT.ok;
|
|
737
|
+
};
|
|
738
|
+
/**
|
|
739
|
+
* A word in the discussion on a task (ADR-0024): what is said, as given, from a file or from
|
|
740
|
+
* standard input, with the files named by --attach put in it; one request, one comment.
|
|
741
|
+
*/
|
|
742
|
+
const comment = async (args, io) => {
|
|
743
|
+
const { values, positionals } = parse(args, {
|
|
744
|
+
file: { type: "string" },
|
|
745
|
+
attach: { type: "string", multiple: true },
|
|
746
|
+
});
|
|
747
|
+
const ask = 'Say which task, and what: console comment <number|id> "<markdown>" [--file <path> | -] [--attach <path>]...';
|
|
748
|
+
if (positionals.length === 0 || positionals.length > 2) {
|
|
749
|
+
throw misuse(ask);
|
|
750
|
+
}
|
|
751
|
+
const ref = positionals[0] ?? "";
|
|
752
|
+
const body = (await bodyOf(io, values, positionals[1])) ?? "";
|
|
753
|
+
const paths = texts(values, "attach");
|
|
754
|
+
if (body.trim().length === 0 && paths.length === 0) {
|
|
755
|
+
throw misuse(`Say something, or attach a file. ${ask}`);
|
|
756
|
+
}
|
|
757
|
+
const files = [];
|
|
758
|
+
for (const path of paths) {
|
|
759
|
+
let bytes;
|
|
760
|
+
try {
|
|
761
|
+
bytes = await io.readBytes(path);
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
throw failed(`No file could be read at ${path}. Name the path of a file to attach.`);
|
|
765
|
+
}
|
|
766
|
+
const name = basename(path);
|
|
767
|
+
const contentType = CONTENT_TYPES[extname(name).toLowerCase()];
|
|
768
|
+
files.push({ name, bytes, ...(contentType === undefined ? {} : { contentType }) });
|
|
769
|
+
}
|
|
770
|
+
const session = await open(io, values);
|
|
771
|
+
const project = await session.project();
|
|
772
|
+
const task = await project.task(ref);
|
|
773
|
+
const written = await project.comment(task.id, { body, files });
|
|
774
|
+
answer(io, session, written, () => [
|
|
775
|
+
`Commented on #${task.number} ${task.title}${written.files.length === 0 ? "" : `, with ${written.files.length} file${written.files.length === 1 ? "" : "s"}`}.`,
|
|
776
|
+
formatComment(written),
|
|
777
|
+
].join("\n"));
|
|
778
|
+
return EXIT.ok;
|
|
779
|
+
};
|
|
780
|
+
/**
|
|
781
|
+
* A file of the project, read from the console and written on this machine: one a run handed
|
|
782
|
+
* in, one put in a comment, or one attached to a page, by its id, so that an agent can look at
|
|
783
|
+
* a picture a person put on the board.
|
|
784
|
+
*/
|
|
785
|
+
const file = async (args, io) => {
|
|
786
|
+
const { values, positionals } = parse(args, { out: { type: "string" } });
|
|
787
|
+
const id = theOne(positionals, "Say which file: console file <id> [--out <path>]");
|
|
788
|
+
const session = await open(io, values);
|
|
789
|
+
const project = await session.project();
|
|
790
|
+
const read = await project.readFile(id);
|
|
791
|
+
const target = text(values, "out") ?? read.name;
|
|
792
|
+
let saved;
|
|
793
|
+
try {
|
|
794
|
+
saved = await io.writeBytes(target, read.bytes, read.name);
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
throw failed(`The file could not be written at ${target}. Name a path with --out <path>.`);
|
|
798
|
+
}
|
|
799
|
+
answer(io, session, {
|
|
800
|
+
id,
|
|
801
|
+
name: read.name,
|
|
802
|
+
contentType: read.contentType,
|
|
803
|
+
size: read.bytes.byteLength,
|
|
804
|
+
path: saved,
|
|
805
|
+
}, () => `Saved ${read.name} (${read.bytes.byteLength} bytes, ${read.contentType}) at ${saved}`);
|
|
705
806
|
return EXIT.ok;
|
|
706
807
|
};
|
|
707
808
|
const take = async (args, io) => {
|
|
@@ -717,10 +818,11 @@ const take = async (args, io) => {
|
|
|
717
818
|
});
|
|
718
819
|
// The rules come with the run, as they read for the person: everything from here acts
|
|
719
820
|
// within them (ADR-0023); the task is read again, as it stands now that it is taken.
|
|
720
|
-
const [ruling, me, taken] = await Promise.all([
|
|
821
|
+
const [ruling, me, taken, comments] = await Promise.all([
|
|
721
822
|
project.rules(),
|
|
722
823
|
session.client.me(),
|
|
723
824
|
project.task(ref),
|
|
825
|
+
project.comments(found.id),
|
|
724
826
|
]);
|
|
725
827
|
// Taking a task with a run open on it ends that run (ADR-0020): said when it happened.
|
|
726
828
|
const tookOver = found.openRuns > 0
|
|
@@ -730,15 +832,15 @@ const take = async (args, io) => {
|
|
|
730
832
|
workspace: forPerson(ruling.workspace, me.language),
|
|
731
833
|
project: forPerson(ruling.project, me.language),
|
|
732
834
|
};
|
|
733
|
-
answer(io, session, { run, task: taken, rules }, () => [
|
|
835
|
+
answer(io, session, { run, task: taken, comments: comments.items, rules }, () => [
|
|
734
836
|
`Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.${tookOver}`,
|
|
735
837
|
"",
|
|
736
|
-
formatTask(taken),
|
|
838
|
+
formatTask(taken, undefined, undefined, comments.items),
|
|
737
839
|
"",
|
|
738
840
|
formatRules(rules),
|
|
739
841
|
"",
|
|
740
842
|
'Say how it goes: console report "<what you found or did>". Hand in what you make: console hand-in <https url | file path>.',
|
|
741
|
-
'Ask when a person must decide: console ask "<question>" --option "..." --option "...". End with: console finish --summary "<what was done, what is left>".',
|
|
843
|
+
'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>".',
|
|
742
844
|
].join("\n"));
|
|
743
845
|
return EXIT.ok;
|
|
744
846
|
};
|
|
@@ -1005,25 +1107,40 @@ const restore = async (args, io) => {
|
|
|
1005
1107
|
return EXIT.ok;
|
|
1006
1108
|
};
|
|
1007
1109
|
const goals = async (args, io) => {
|
|
1008
|
-
const { values } = parse(args, {}, false);
|
|
1110
|
+
const { values } = parse(args, { category: { type: "string" }, state: { type: "string" } }, false);
|
|
1111
|
+
const state = goalStateOf(text(values, "state"));
|
|
1112
|
+
const category = text(values, "category");
|
|
1009
1113
|
const session = await open(io, values);
|
|
1010
1114
|
const project = await session.project();
|
|
1011
|
-
const found = await project.goals(
|
|
1115
|
+
const found = await project.goals({
|
|
1116
|
+
...(state === undefined ? {} : { state }),
|
|
1117
|
+
...(category === undefined ? {} : { category }),
|
|
1118
|
+
});
|
|
1012
1119
|
answer(io, session, found, () => found.items.length === 0
|
|
1013
|
-
?
|
|
1120
|
+
? state === undefined && category === undefined
|
|
1121
|
+
? "No goals. An owner sets one: console goal new <title> --due <day>"
|
|
1122
|
+
: "No goal matches."
|
|
1014
1123
|
: found.items.map(formatGoalLine).join("\n"));
|
|
1015
1124
|
return EXIT.ok;
|
|
1016
1125
|
};
|
|
1017
1126
|
const GOAL_FIELDS = {
|
|
1018
1127
|
state: { type: "string" },
|
|
1128
|
+
category: { type: "string" },
|
|
1129
|
+
starts: { type: "string" },
|
|
1019
1130
|
due: { type: "string" },
|
|
1020
1131
|
body: { type: "string" },
|
|
1021
1132
|
file: { type: "string" },
|
|
1022
1133
|
};
|
|
1134
|
+
/** A category's key as a flag names one, or `null` for `none` where the field may be cleared. */
|
|
1135
|
+
function categoryOf(value, clearable = false) {
|
|
1136
|
+
return clearable && value === "none" ? null : value;
|
|
1137
|
+
}
|
|
1023
1138
|
const newGoal = async (args, io) => {
|
|
1024
1139
|
const { values, positionals } = parse(args, GOAL_FIELDS);
|
|
1025
1140
|
const title = theOne(positionals, 'Say the title: console goal new "<title>" [--due <day>]');
|
|
1026
1141
|
const state = goalStateOf(text(values, "state"));
|
|
1142
|
+
const category = categoryOf(text(values, "category"));
|
|
1143
|
+
const startsOn = dayOf(io, text(values, "starts"));
|
|
1027
1144
|
const dueOn = dayOf(io, text(values, "due"));
|
|
1028
1145
|
const body = await bodyOf(io, values, text(values, "body"));
|
|
1029
1146
|
const session = await open(io, values);
|
|
@@ -1032,6 +1149,8 @@ const newGoal = async (args, io) => {
|
|
|
1032
1149
|
title,
|
|
1033
1150
|
...(body === undefined ? {} : { body }),
|
|
1034
1151
|
...(state === undefined ? {} : { state }),
|
|
1152
|
+
...(category === undefined ? {} : { category }),
|
|
1153
|
+
...(startsOn === undefined ? {} : { startsOn }),
|
|
1035
1154
|
...(dueOn === undefined ? {} : { dueOn }),
|
|
1036
1155
|
});
|
|
1037
1156
|
answer(io, session, made, () => `Goal set: ${made.title}\nid: ${made.id}`);
|
|
@@ -1042,16 +1161,20 @@ const editGoal = async (args, io) => {
|
|
|
1042
1161
|
const id = theOne(positionals, "Say which goal: console goal edit <id> --state <state> ...");
|
|
1043
1162
|
const title = text(values, "title");
|
|
1044
1163
|
const state = goalStateOf(text(values, "state"));
|
|
1164
|
+
const category = categoryOf(text(values, "category"), true);
|
|
1165
|
+
const startsOn = dayOf(io, text(values, "starts"), true);
|
|
1045
1166
|
const dueOn = dayOf(io, text(values, "due"), true);
|
|
1046
1167
|
const body = await bodyOf(io, values, text(values, "body"));
|
|
1047
1168
|
const patch = {
|
|
1048
1169
|
...(title === undefined ? {} : { title }),
|
|
1049
1170
|
...(body === undefined ? {} : { body }),
|
|
1050
1171
|
...(state === undefined ? {} : { state }),
|
|
1172
|
+
...(category === undefined ? {} : { category }),
|
|
1173
|
+
...(startsOn === undefined ? {} : { startsOn }),
|
|
1051
1174
|
...(dueOn === undefined ? {} : { dueOn }),
|
|
1052
1175
|
};
|
|
1053
1176
|
if (Object.keys(patch).length === 0) {
|
|
1054
|
-
throw misuse("Say what to change: --title, --body, --state or --due;
|
|
1177
|
+
throw misuse("Say what to change: --title, --body, --state, --category, --starts or --due; none clears a category or a day.");
|
|
1055
1178
|
}
|
|
1056
1179
|
const session = await open(io, values);
|
|
1057
1180
|
const project = await session.project();
|
|
@@ -1476,6 +1599,8 @@ const COMMANDS = {
|
|
|
1476
1599
|
take,
|
|
1477
1600
|
report,
|
|
1478
1601
|
"hand-in": handIn,
|
|
1602
|
+
comment,
|
|
1603
|
+
file,
|
|
1479
1604
|
ask,
|
|
1480
1605
|
decision,
|
|
1481
1606
|
decisions,
|
|
@@ -1503,9 +1628,67 @@ function help(io, name) {
|
|
|
1503
1628
|
io.stdout(found);
|
|
1504
1629
|
return EXIT.ok;
|
|
1505
1630
|
}
|
|
1631
|
+
/** A client of the console at `url`, naming the command on every request (ADR-0025). */
|
|
1632
|
+
function clientOf(io, url, token) {
|
|
1633
|
+
return createClient({
|
|
1634
|
+
baseUrl: url,
|
|
1635
|
+
fetch: io.fetch,
|
|
1636
|
+
...(token === undefined ? {} : { token }),
|
|
1637
|
+
...(io.userAgent === undefined ? {} : { userAgent: io.userAgent }),
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
/** The command that puts the console's version where this command is installed. */
|
|
1641
|
+
function updateWords(io, version) {
|
|
1642
|
+
const wanted = version ?? "latest";
|
|
1643
|
+
if (io.install === undefined) {
|
|
1644
|
+
return `npm install -g @skillcdn/console@${wanted}`;
|
|
1645
|
+
}
|
|
1646
|
+
const command = updateCommandFor(io.install, wanted);
|
|
1647
|
+
return io.install.kind === "project" ? `${command} (in ${io.install.root})` : command;
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* What the command says of the versions once the console answered: that a newer command exists,
|
|
1651
|
+
* or that this one is newer than the console; once a day for one pair of versions, on stderr,
|
|
1652
|
+
* so that the console's JSON stays what it is. What is out of the range never gets here: the
|
|
1653
|
+
* console refused it, and the refusal says what to do.
|
|
1654
|
+
*/
|
|
1655
|
+
async function noticeVersions(io, seen) {
|
|
1656
|
+
const command = parseVersion(io.version);
|
|
1657
|
+
const server = parseVersion(seen.version);
|
|
1658
|
+
if (io.version === undefined ||
|
|
1659
|
+
seen.version === undefined ||
|
|
1660
|
+
command === undefined ||
|
|
1661
|
+
server === undefined) {
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
const { verdict } = judgeClient(command, server);
|
|
1665
|
+
if (verdict !== "behind" && verdict !== "ahead") {
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const previous = await io.notices?.load();
|
|
1669
|
+
const now = io.now();
|
|
1670
|
+
if (previous !== undefined &&
|
|
1671
|
+
previous.command === io.version &&
|
|
1672
|
+
previous.console === seen.version &&
|
|
1673
|
+
now - Date.parse(previous.shownAt) < DAY_MS) {
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
io.stderr(verdict === "behind"
|
|
1677
|
+
? `A newer command exists: the console runs ${seen.version}, this command is ${io.version}. Update it: ${updateWords(io, seen.version)}\n`
|
|
1678
|
+
: `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`);
|
|
1679
|
+
await io.notices?.save({
|
|
1680
|
+
command: io.version,
|
|
1681
|
+
console: seen.version,
|
|
1682
|
+
shownAt: new Date(now).toISOString(),
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1506
1685
|
/** What a refusal of the console is followed by, when there is something to do about it. */
|
|
1507
|
-
function hintFor(error) {
|
|
1686
|
+
function hintFor(error, io, seen) {
|
|
1508
1687
|
switch (error.code) {
|
|
1688
|
+
case "client.outdated":
|
|
1689
|
+
return `Update the command: ${updateWords(io, seen.version)}\n`;
|
|
1690
|
+
case "client.too_new":
|
|
1691
|
+
return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
|
|
1509
1692
|
case "auth.required":
|
|
1510
1693
|
return "The token is not one the console knows, any more or at all. A person signs the command in again: console login\n";
|
|
1511
1694
|
case "project.not_found":
|
|
@@ -1524,6 +1707,10 @@ function hintFor(error) {
|
|
|
1524
1707
|
return "The due day is before the start. Say a due day on or after it.\n";
|
|
1525
1708
|
case "goal.not_found":
|
|
1526
1709
|
return "No goal of this project has that id. console goals lists them.\n";
|
|
1710
|
+
case "goal.invalid_category":
|
|
1711
|
+
return "No category has that key. console categories lists them.\n";
|
|
1712
|
+
case "goal.invalid_dates":
|
|
1713
|
+
return "The target day is before the start. Say a target day on or after it.\n";
|
|
1527
1714
|
case "auth.forbidden":
|
|
1528
1715
|
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";
|
|
1529
1716
|
case "project.key_taken":
|
|
@@ -1563,9 +1750,21 @@ function hintFor(error) {
|
|
|
1563
1750
|
/** Runs the command and answers with its exit code. What it prints goes through `io`. */
|
|
1564
1751
|
export async function runCli(argv, io) {
|
|
1565
1752
|
const [name, ...rest] = argv;
|
|
1753
|
+
// Every answer of the console says its version and the range it takes; the command reads
|
|
1754
|
+
// them off the answers it gets anyway, and asks nothing more (ADR-0025).
|
|
1755
|
+
const seen = {};
|
|
1756
|
+
const watched = {
|
|
1757
|
+
...io,
|
|
1758
|
+
fetch: async (input, init) => {
|
|
1759
|
+
const response = await io.fetch(input, init);
|
|
1760
|
+
seen.version = response.headers.get(VERSION_HEADER) ?? seen.version;
|
|
1761
|
+
seen.range = response.headers.get(CLIENT_RANGE_HEADER) ?? seen.range;
|
|
1762
|
+
return response;
|
|
1763
|
+
},
|
|
1764
|
+
};
|
|
1566
1765
|
try {
|
|
1567
1766
|
if (name === "--version" || name === "-v" || name === "version") {
|
|
1568
|
-
io.stdout(`console ${io.version ?? "(version unknown)"} (@skillcdn/console)\n`);
|
|
1767
|
+
io.stdout(`console ${io.version ?? "(version unknown)"} (@skillcdn/console)${io.install === undefined ? "" : `, ${describeInstall(io.install)}`}\n`);
|
|
1569
1768
|
return EXIT.ok;
|
|
1570
1769
|
}
|
|
1571
1770
|
if (name === undefined || name === "help" || name === "--help" || name === "-h") {
|
|
@@ -1578,7 +1777,11 @@ export async function runCli(argv, io) {
|
|
|
1578
1777
|
if (command === undefined) {
|
|
1579
1778
|
throw misuse(`Not a command: ${name}. Run: console help`);
|
|
1580
1779
|
}
|
|
1581
|
-
|
|
1780
|
+
const exit = await command(rest, watched);
|
|
1781
|
+
if (name !== "whoami") {
|
|
1782
|
+
await noticeVersions(io, seen);
|
|
1783
|
+
}
|
|
1784
|
+
return exit;
|
|
1582
1785
|
}
|
|
1583
1786
|
catch (error) {
|
|
1584
1787
|
if (error instanceof CliExit) {
|
|
@@ -1586,8 +1789,10 @@ export async function runCli(argv, io) {
|
|
|
1586
1789
|
return error.exit;
|
|
1587
1790
|
}
|
|
1588
1791
|
if (error instanceof ApiError) {
|
|
1589
|
-
io.stderr(`${error.code}: ${error.message}\n${hintFor(error)}`);
|
|
1590
|
-
return
|
|
1792
|
+
io.stderr(`${error.code}: ${error.message}\n${hintFor(error, io, seen)}`);
|
|
1793
|
+
return error.code === "client.outdated" || error.code === "client.too_new"
|
|
1794
|
+
? EXIT.incompatible
|
|
1795
|
+
: EXIT.failed;
|
|
1591
1796
|
}
|
|
1592
1797
|
throw error;
|
|
1593
1798
|
}
|