@skillcdn/console 0.3.0 → 0.3.2
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 +5 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +290 -46
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/credentials.d.ts +5 -0
- package/dist/cli/credentials.d.ts.map +1 -1
- package/dist/cli/credentials.js +2 -0
- package/dist/cli/credentials.js.map +1 -1
- package/dist/cli/directory.d.ts +7 -0
- package/dist/cli/directory.d.ts.map +1 -1
- package/dist/cli/directory.js +7 -0
- package/dist/cli/directory.js.map +1 -1
- package/dist/cli/format.d.ts +10 -1
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +76 -20
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +42 -23
- 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/client.d.ts +39 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +24 -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.map +1 -1
- package/dist/components/board.js +6 -4
- package/dist/components/board.js.map +1 -1
- package/dist/components/calendar.d.ts +18 -8
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +39 -27
- package/dist/components/calendar.js.map +1 -1
- 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/pack-connect.d.ts +23 -0
- package/dist/components/pack-connect.d.ts.map +1 -0
- package/dist/components/pack-connect.js +86 -0
- package/dist/components/pack-connect.js.map +1 -0
- package/dist/components/pack-token.d.ts +27 -0
- package/dist/components/pack-token.d.ts.map +1 -0
- package/dist/components/pack-token.js +11 -0
- package/dist/components/pack-token.js.map +1 -0
- 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 +13 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +37 -5
- package/dist/components/projects.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 +6 -5
- package/dist/components/task-view.js.map +1 -1
- package/dist/components/ui.d.ts +7 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +11 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +3 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +6 -4
- package/dist/components/work.js.map +1 -1
- package/dist/components/workspace.d.ts +46 -0
- package/dist/components/workspace.d.ts.map +1 -0
- package/dist/components/workspace.js +124 -0
- package/dist/components/workspace.js.map +1 -0
- package/dist/console.css +319 -11
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +83 -24
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +26 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +59 -1
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +12 -2
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +20 -2
- package/dist/dates.js.map +1 -1
- package/dist/i18n/en.d.ts +64 -3
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +81 -16
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +77 -16
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- 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 +4 -4
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +11 -2
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +4 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +4 -0
- package/dist/routes.js.map +1 -1
- package/dist/schemas.d.ts +194 -13
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +121 -21
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +21 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +18 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +25 -20
- package/docs/specs/rest.md +42 -19
- package/package.json +2 -2
- package/skills/building-a-console/SKILL.md +6 -6
- package/skills/building-a-console/references/the-look-from-a-website.md +39 -0
- package/skills/deploying-a-console/SKILL.md +9 -8
- package/skills/deploying-a-console/references/resources.md +1 -1
- package/skills/working-the-board/SKILL.md +5 -5
- package/skills/working-the-board/references/setting-up.md +1 -1
- package/skills/working-the-board/references/terms.md +3 -1
- package/skills/working-the-board/references/the-sentence.md +1 -1
- package/src/api.ts +21 -0
- package/src/cli/cli.ts +384 -51
- package/src/cli/credentials.ts +7 -0
- package/src/cli/directory.ts +9 -0
- package/src/cli/format.ts +83 -20
- package/src/cli/help.ts +50 -23
- package/src/cli/main.ts +1 -0
- package/src/client.ts +107 -10
- package/src/compat.ts +63 -14
- package/src/components/board.tsx +10 -4
- package/src/components/calendar.tsx +76 -34
- package/src/components/event-feed.tsx +2 -0
- package/src/components/pack-connect.tsx +179 -0
- package/src/components/pack-token.tsx +74 -0
- package/src/components/picker.tsx +297 -0
- package/src/components/projects.tsx +108 -9
- package/src/components/task-form.tsx +149 -63
- package/src/components/task-list.tsx +2 -2
- package/src/components/task-view.tsx +23 -9
- package/src/components/ui.tsx +25 -0
- package/src/components/work.tsx +10 -5
- package/src/components/workspace.tsx +298 -0
- package/src/console.tsx +202 -32
- package/src/data.ts +101 -1
- package/src/dates.ts +23 -2
- package/src/i18n/en.ts +92 -16
- package/src/i18n/ko.ts +84 -16
- package/src/index.ts +32 -1
- package/src/limits.ts +2 -0
- package/src/router.ts +17 -5
- package/src/routes.ts +4 -0
- package/src/schemas.ts +140 -19
- package/src/styles/console.css +319 -11
- package/src/vocabulary.ts +28 -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 {
|
|
@@ -16,6 +26,7 @@ import {
|
|
|
16
26
|
type RestClaim,
|
|
17
27
|
type RestDecision,
|
|
18
28
|
type RestMember,
|
|
29
|
+
type RestPackConnectStarted,
|
|
19
30
|
type RestPerson,
|
|
20
31
|
type RestSkillBrowse,
|
|
21
32
|
type RestSkillFile,
|
|
@@ -28,7 +39,9 @@ import {
|
|
|
28
39
|
type ColorName,
|
|
29
40
|
GOAL_STATES,
|
|
30
41
|
type GoalState,
|
|
42
|
+
PEOPLE_VISIBILITIES,
|
|
31
43
|
PERSON_ROLES,
|
|
44
|
+
type PeopleVisibility,
|
|
32
45
|
type PersonRole,
|
|
33
46
|
PROJECT_ROLES,
|
|
34
47
|
PROJECT_VISIBILITIES,
|
|
@@ -40,6 +53,7 @@ import {
|
|
|
40
53
|
TASK_STATES,
|
|
41
54
|
type TaskPriority,
|
|
42
55
|
type TaskState,
|
|
56
|
+
WEEK_STARTS,
|
|
43
57
|
} from "../vocabulary.js";
|
|
44
58
|
import { loadWebRoot, type WebRoot, WebRootError } from "../web.js";
|
|
45
59
|
import {
|
|
@@ -53,7 +67,7 @@ import {
|
|
|
53
67
|
withLogin,
|
|
54
68
|
withoutLogin,
|
|
55
69
|
} from "./credentials.js";
|
|
56
|
-
import type
|
|
70
|
+
import { type DirectorySettings, type DirectoryStore, THE_SENTENCE } from "./directory.js";
|
|
57
71
|
import {
|
|
58
72
|
formatAnswer,
|
|
59
73
|
formatCategoryLine,
|
|
@@ -80,6 +94,9 @@ import {
|
|
|
80
94
|
formatVersion,
|
|
81
95
|
formatVersionLine,
|
|
82
96
|
formatWork,
|
|
97
|
+
formatWorkspace,
|
|
98
|
+
moment,
|
|
99
|
+
setMomentZone,
|
|
83
100
|
} from "./format.js";
|
|
84
101
|
import { COMMAND_HELP, helpFor, ownSkillPath, usage } from "./help.js";
|
|
85
102
|
import { describeInstall, type Install, updateCommandFor } from "./install.js";
|
|
@@ -161,6 +178,10 @@ export interface CliIo {
|
|
|
161
178
|
readonly install?: Install | undefined;
|
|
162
179
|
/** What the command last said of the versions; left out, it says it every time. */
|
|
163
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;
|
|
164
185
|
/** Where the console's own skills are on this machine, shipped with the package; nothing when they are not there. */
|
|
165
186
|
readonly skillsDir?: string | undefined;
|
|
166
187
|
/** How `serve` listens on the loopback (ADR-0014); left out, the command cannot serve. */
|
|
@@ -310,6 +331,14 @@ async function open(io: CliIo, values: Values): Promise<Session> {
|
|
|
310
331
|
throw failed(wordsOf(resolved));
|
|
311
332
|
}
|
|
312
333
|
const { url, token } = resolved;
|
|
334
|
+
// Moments are printed in the workspace's time zone as the login kept it (ADR-0035); the
|
|
335
|
+
// environment's pair keeps nothing, and they stay as the API gives them.
|
|
336
|
+
setMomentZone(
|
|
337
|
+
resolved.via === "login"
|
|
338
|
+
? credentials?.logins.find((kept) => kept.url === url && kept.account === resolved.account)
|
|
339
|
+
?.timeZone
|
|
340
|
+
: undefined,
|
|
341
|
+
);
|
|
313
342
|
const client = clientOf(io, url, token);
|
|
314
343
|
const projectKey = async () =>
|
|
315
344
|
text(values, "project") ?? io.env.CONSOLE_PROJECT ?? directory?.project;
|
|
@@ -484,18 +513,25 @@ async function personIdOf(
|
|
|
484
513
|
return (await personOf(session, value)).id;
|
|
485
514
|
}
|
|
486
515
|
|
|
487
|
-
/**
|
|
516
|
+
/**
|
|
517
|
+
* The person a login names, among the workspace's, found by their exact login as people sign in
|
|
518
|
+
* (ADR-0035): whom the console shows the person, and, for an owner of a project, anyone of the
|
|
519
|
+
* workspace, so that someone the People page does not show can still be listed.
|
|
520
|
+
*/
|
|
488
521
|
async function personOf(session: Session, login: string): Promise<RestPerson> {
|
|
489
|
-
const
|
|
490
|
-
const found = items.find((person) => person.login.toLowerCase() === login.toLowerCase());
|
|
522
|
+
const found = await session.client.findPerson(login);
|
|
491
523
|
if (found === undefined) {
|
|
492
524
|
throw misuse(
|
|
493
|
-
`Nobody of the workspace is called ${login}. console people lists
|
|
525
|
+
`Nobody of the workspace is called ${login}, or they are not yours to see. console people lists whom you see, by login.`,
|
|
494
526
|
);
|
|
495
527
|
}
|
|
496
528
|
return found;
|
|
497
529
|
}
|
|
498
530
|
|
|
531
|
+
/** Who sees whom, as the command takes it: with a hyphen or an underscore. */
|
|
532
|
+
const peopleVisibilityOf = (value: string): PeopleVisibility =>
|
|
533
|
+
wordOf(value.replaceAll("-", "_"), PEOPLE_VISIBILITIES, "who sees whom");
|
|
534
|
+
|
|
499
535
|
/** One word of a vocabulary, or the refusal to understand. */
|
|
500
536
|
function wordOf<T extends string>(value: string, words: readonly T[], what: string): T {
|
|
501
537
|
const found = words.find((word) => word === value);
|
|
@@ -652,7 +688,14 @@ async function keep(
|
|
|
652
688
|
}
|
|
653
689
|
const next = withLogin(
|
|
654
690
|
credentials,
|
|
655
|
-
{
|
|
691
|
+
{
|
|
692
|
+
url,
|
|
693
|
+
account: person.login,
|
|
694
|
+
token,
|
|
695
|
+
agent,
|
|
696
|
+
since: new Date(io.now()).toISOString(),
|
|
697
|
+
timeZone: me.workspace.timeZone ?? undefined,
|
|
698
|
+
},
|
|
656
699
|
{ default: asDefault },
|
|
657
700
|
);
|
|
658
701
|
const kept = await io.store.save(next);
|
|
@@ -826,6 +869,24 @@ function agentOf(env: CliIo["env"]): string | undefined {
|
|
|
826
869
|
return undefined;
|
|
827
870
|
}
|
|
828
871
|
|
|
872
|
+
/**
|
|
873
|
+
* Keeps the workspace's time zone on the login the command used (ADR-0035), so that moments are
|
|
874
|
+
* printed in it from now on without asking; the environment's pair keeps nothing.
|
|
875
|
+
*/
|
|
876
|
+
async function rememberZone(io: CliIo, session: Session, zone: string | undefined): Promise<void> {
|
|
877
|
+
setMomentZone(zone);
|
|
878
|
+
if (session.via !== "login" || session.credentials === undefined) {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
const kept = session.credentials.logins.find(
|
|
882
|
+
(login) => login.url === session.url && login.account === session.account,
|
|
883
|
+
);
|
|
884
|
+
if (kept === undefined || kept.timeZone === zone) {
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
await io.store.save(withLogin(session.credentials, { ...kept, timeZone: zone }));
|
|
888
|
+
}
|
|
889
|
+
|
|
829
890
|
const whoami: Command = async (args, io) => {
|
|
830
891
|
const { values } = parse(args, {}, false);
|
|
831
892
|
const session = await open(io, values);
|
|
@@ -834,6 +895,7 @@ const whoami: Command = async (args, io) => {
|
|
|
834
895
|
if (person === null) {
|
|
835
896
|
throw failed("The console does not know this token. Sign in again: console login");
|
|
836
897
|
}
|
|
898
|
+
await rememberZone(io, session, me.workspace.timeZone ?? undefined);
|
|
837
899
|
const key = await session.projectKey();
|
|
838
900
|
const command = parseVersion(io.version);
|
|
839
901
|
const server = parseVersion(me.console?.version);
|
|
@@ -841,11 +903,15 @@ const whoami: Command = async (args, io) => {
|
|
|
841
903
|
command === undefined || server === undefined
|
|
842
904
|
? undefined
|
|
843
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);
|
|
844
910
|
// A console from before the rule says nothing of itself; the command says so instead.
|
|
845
911
|
const consoleWords =
|
|
846
912
|
me.console === undefined
|
|
847
913
|
? "the console does not say its version, so it is older than 0.1.18"
|
|
848
|
-
: `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`;
|
|
849
915
|
answer(
|
|
850
916
|
io,
|
|
851
917
|
session,
|
|
@@ -914,16 +980,29 @@ const use: Command = async (args, io) => {
|
|
|
914
980
|
const bound = pinUrl
|
|
915
981
|
? ` at ${session.url}${pinAccount && session.account !== undefined ? ` as ${session.account}` : ""}`
|
|
916
982
|
: "";
|
|
917
|
-
answer(
|
|
918
|
-
|
|
919
|
-
session,
|
|
920
|
-
project,
|
|
921
|
-
() =>
|
|
983
|
+
answer(io, session, project, () =>
|
|
984
|
+
[
|
|
922
985
|
`Working in ${project.name} (${project.key})${bound} from this directory on: kept in ${kept}, which is meant to be committed in a private repository and left out of a public one. What its people ask of agents: console rules.`,
|
|
986
|
+
// Nothing sends an agent to the console until the checkout says so; the sentence is the
|
|
987
|
+
// skill's (references/the-sentence.md), printed here as `skillcdn use` prints its own.
|
|
988
|
+
"Then put this sentence into the checkout's agreement (AGENTS.md, or CLAUDE.md where that is the file agents read), as it is, so that every agent working here is sent to the console:",
|
|
989
|
+
"",
|
|
990
|
+
THE_SENTENCE,
|
|
991
|
+
].join("\n"),
|
|
923
992
|
);
|
|
924
993
|
return EXIT.ok;
|
|
925
994
|
};
|
|
926
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
|
+
|
|
927
1006
|
const tasks: Command = async (args, io) => {
|
|
928
1007
|
const { values } = parse(
|
|
929
1008
|
args,
|
|
@@ -935,6 +1014,7 @@ const tasks: Command = async (args, io) => {
|
|
|
935
1014
|
category: { type: "string" },
|
|
936
1015
|
goal: { type: "string" },
|
|
937
1016
|
due: { type: "string" },
|
|
1017
|
+
search: { type: "string" },
|
|
938
1018
|
archived: { type: "boolean" },
|
|
939
1019
|
...PAGE_OPTIONS,
|
|
940
1020
|
},
|
|
@@ -943,7 +1023,7 @@ const tasks: Command = async (args, io) => {
|
|
|
943
1023
|
const state = stateOf(text(values, "state"));
|
|
944
1024
|
if (on(values, "mine") && on(values, "unassigned")) {
|
|
945
1025
|
throw misuse(
|
|
946
|
-
"Say one: --mine for the tasks
|
|
1026
|
+
"Say one: --mine for the tasks you work on, or --unassigned for those nobody works on",
|
|
947
1027
|
);
|
|
948
1028
|
}
|
|
949
1029
|
const page = pageOf(values);
|
|
@@ -958,19 +1038,22 @@ const tasks: Command = async (args, io) => {
|
|
|
958
1038
|
: due === "overdue"
|
|
959
1039
|
? addDays(today, -1)
|
|
960
1040
|
: due === "week"
|
|
961
|
-
?
|
|
1041
|
+
? await endOfWeek(io, session, today)
|
|
962
1042
|
: (dayOf(io, due) ?? undefined);
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
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
|
+
}
|
|
966
1048
|
const found = await project.tasks({
|
|
967
1049
|
...(state === undefined ? {} : { state }),
|
|
968
1050
|
...(on(values, "open") || due === "overdue" ? { open: true } : {}),
|
|
969
|
-
...(
|
|
1051
|
+
...(worker === undefined ? {} : { worker }),
|
|
970
1052
|
...(on(values, "unassigned") ? { unassigned: true } : {}),
|
|
971
1053
|
...(text(values, "category") === undefined ? {} : { category: text(values, "category") }),
|
|
972
1054
|
...(text(values, "goal") === undefined ? {} : { goal: text(values, "goal") }),
|
|
973
1055
|
...(dueBy === undefined ? {} : { dueBy }),
|
|
1056
|
+
...(search === undefined ? {} : { q: search }),
|
|
974
1057
|
...(on(values, "archived") ? { archived: true } : {}),
|
|
975
1058
|
...page,
|
|
976
1059
|
});
|
|
@@ -1284,10 +1367,42 @@ const TASK_FIELDS: OptionSpec = {
|
|
|
1284
1367
|
goal: { type: "string" },
|
|
1285
1368
|
starts: { type: "string" },
|
|
1286
1369
|
due: { type: "string" },
|
|
1287
|
-
|
|
1370
|
+
worker: { type: "string", multiple: true },
|
|
1288
1371
|
parent: { type: "string" },
|
|
1289
1372
|
};
|
|
1290
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* The people `--worker` names, each a login or `me`, given again or with commas between for
|
|
1376
|
+
* several, in place of those there were; `none` alone for nobody; nothing when the flag was
|
|
1377
|
+
* not given.
|
|
1378
|
+
*/
|
|
1379
|
+
async function workerIdsOf(
|
|
1380
|
+
session: Session,
|
|
1381
|
+
given: readonly string[],
|
|
1382
|
+
): Promise<string[] | undefined> {
|
|
1383
|
+
if (given.length === 0) {
|
|
1384
|
+
return undefined;
|
|
1385
|
+
}
|
|
1386
|
+
const names = given
|
|
1387
|
+
.flatMap((value) => value.split(","))
|
|
1388
|
+
.map((name) => name.trim())
|
|
1389
|
+
.filter((name) => name.length > 0);
|
|
1390
|
+
if (names.includes("none")) {
|
|
1391
|
+
if (names.length > 1) {
|
|
1392
|
+
throw misuse("Say --worker none alone, for nobody; or the logins of the workers.");
|
|
1393
|
+
}
|
|
1394
|
+
return [];
|
|
1395
|
+
}
|
|
1396
|
+
const ids: string[] = [];
|
|
1397
|
+
for (const name of names) {
|
|
1398
|
+
const id = await personIdOf(session, name);
|
|
1399
|
+
if (typeof id === "string" && !ids.includes(id)) {
|
|
1400
|
+
ids.push(id);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return ids;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1291
1406
|
/** What the flags say of a task's fields, as the API takes them; `none` clears a field. */
|
|
1292
1407
|
async function taskFieldsOf(
|
|
1293
1408
|
io: CliIo,
|
|
@@ -1301,7 +1416,7 @@ async function taskFieldsOf(
|
|
|
1301
1416
|
const goalId = orNone(text(values, "goal"));
|
|
1302
1417
|
const startsOn = dayOf(io, text(values, "starts"), true);
|
|
1303
1418
|
const dueOn = dayOf(io, text(values, "due"), true);
|
|
1304
|
-
const
|
|
1419
|
+
const workerIds = await workerIdsOf(session, texts(values, "worker"));
|
|
1305
1420
|
const parent = text(values, "parent");
|
|
1306
1421
|
const parentId =
|
|
1307
1422
|
parent === undefined ? undefined : parent === "none" ? null : (await project.task(parent)).id;
|
|
@@ -1312,7 +1427,7 @@ async function taskFieldsOf(
|
|
|
1312
1427
|
...(goalId === undefined ? {} : { goalId }),
|
|
1313
1428
|
...(startsOn === undefined ? {} : { startsOn }),
|
|
1314
1429
|
...(dueOn === undefined ? {} : { dueOn }),
|
|
1315
|
-
...(
|
|
1430
|
+
...(workerIds === undefined ? {} : { workerIds }),
|
|
1316
1431
|
...(parentId === undefined ? {} : { parentId }),
|
|
1317
1432
|
};
|
|
1318
1433
|
}
|
|
@@ -1333,7 +1448,12 @@ const newTask: Command = async (args, io) => {
|
|
|
1333
1448
|
...(body === undefined ? {} : { body }),
|
|
1334
1449
|
...fields,
|
|
1335
1450
|
});
|
|
1336
|
-
answer(
|
|
1451
|
+
answer(
|
|
1452
|
+
io,
|
|
1453
|
+
session,
|
|
1454
|
+
made,
|
|
1455
|
+
() => `Task #${made.number} written: ${made.title}\ntask id: ${made.id}`,
|
|
1456
|
+
);
|
|
1337
1457
|
return EXIT.ok;
|
|
1338
1458
|
};
|
|
1339
1459
|
|
|
@@ -1359,7 +1479,7 @@ const editTask: Command = async (args, io) => {
|
|
|
1359
1479
|
};
|
|
1360
1480
|
if (Object.keys(patch).length === 0) {
|
|
1361
1481
|
throw misuse(
|
|
1362
|
-
"Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --
|
|
1482
|
+
"Say what to change: --title, --body, --state, --priority, --category, --goal, --starts, --due, --worker or --parent; none clears a field.",
|
|
1363
1483
|
);
|
|
1364
1484
|
}
|
|
1365
1485
|
const found = await project.task(ref);
|
|
@@ -1556,6 +1676,9 @@ const take: Command = async (args, io) => {
|
|
|
1556
1676
|
answer(io, session, { run, task: taken, comments: comments.items, rules }, () =>
|
|
1557
1677
|
[
|
|
1558
1678
|
`Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs.${tookOver}${elsewhere}`,
|
|
1679
|
+
// The ids on a line of their own, each named: a run's and a task's begin alike, and a
|
|
1680
|
+
// task has a number for people as well.
|
|
1681
|
+
`run id: ${run.id} task id: ${found.id} (task #${found.number})`,
|
|
1559
1682
|
"",
|
|
1560
1683
|
formatTask(taken, undefined, undefined, comments.items),
|
|
1561
1684
|
"",
|
|
@@ -1701,7 +1824,7 @@ const ask: Command = async (args, io) => {
|
|
|
1701
1824
|
io.stdout(
|
|
1702
1825
|
session.json
|
|
1703
1826
|
? `${JSON.stringify(raised, null, 2)}\n`
|
|
1704
|
-
: `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\n${formatOptions(raised)}\n`,
|
|
1827
|
+
: `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`,
|
|
1705
1828
|
);
|
|
1706
1829
|
return settle(io, session, project, raised, seconds, true);
|
|
1707
1830
|
};
|
|
@@ -1897,23 +2020,36 @@ const restore: Command = async (args, io) => {
|
|
|
1897
2020
|
const goals: Command = async (args, io) => {
|
|
1898
2021
|
const { values } = parse(
|
|
1899
2022
|
args,
|
|
1900
|
-
{
|
|
2023
|
+
{
|
|
2024
|
+
category: { type: "string" },
|
|
2025
|
+
state: { type: "string" },
|
|
2026
|
+
search: { type: "string" },
|
|
2027
|
+
...PAGE_OPTIONS,
|
|
2028
|
+
},
|
|
1901
2029
|
false,
|
|
1902
2030
|
);
|
|
1903
2031
|
const state = goalStateOf(text(values, "state"));
|
|
1904
2032
|
const category = text(values, "category");
|
|
2033
|
+
const search = text(values, "search")?.trim();
|
|
2034
|
+
if (search === "") {
|
|
2035
|
+
throw misuse("Say what to search for: console goals --search <words>");
|
|
2036
|
+
}
|
|
2037
|
+
const page = pageOf(values);
|
|
1905
2038
|
const session = await open(io, values);
|
|
1906
2039
|
const project = await session.project();
|
|
1907
2040
|
const found = await project.goals({
|
|
1908
2041
|
...(state === undefined ? {} : { state }),
|
|
1909
2042
|
...(category === undefined ? {} : { category }),
|
|
2043
|
+
...(search === undefined ? {} : { q: search }),
|
|
2044
|
+
...page,
|
|
1910
2045
|
});
|
|
1911
2046
|
answer(io, session, found, () =>
|
|
1912
2047
|
found.items.length === 0
|
|
1913
|
-
? state === undefined && category === undefined
|
|
2048
|
+
? state === undefined && category === undefined && search === undefined
|
|
1914
2049
|
? "No goals. An owner sets one: console goal new <title> --due <day>"
|
|
1915
2050
|
: "No goal matches."
|
|
1916
|
-
: found.items.map(formatGoalLine).join("\n")
|
|
2051
|
+
: found.items.map(formatGoalLine).join("\n") +
|
|
2052
|
+
formatMore(found.more, page.limit, page.offset),
|
|
1917
2053
|
);
|
|
1918
2054
|
return EXIT.ok;
|
|
1919
2055
|
};
|
|
@@ -1950,7 +2086,7 @@ const newGoal: Command = async (args, io) => {
|
|
|
1950
2086
|
...(startsOn === undefined ? {} : { startsOn }),
|
|
1951
2087
|
...(dueOn === undefined ? {} : { dueOn }),
|
|
1952
2088
|
});
|
|
1953
|
-
answer(io, session, made, () => `Goal set: ${made.title}\
|
|
2089
|
+
answer(io, session, made, () => `Goal set: ${made.title}\ngoal id: ${made.id}`);
|
|
1954
2090
|
return EXIT.ok;
|
|
1955
2091
|
};
|
|
1956
2092
|
|
|
@@ -2186,6 +2322,81 @@ const people: Command = async (args, io) => {
|
|
|
2186
2322
|
return EXIT.ok;
|
|
2187
2323
|
};
|
|
2188
2324
|
|
|
2325
|
+
// The workspace's settings (ADR-0035): read by everyone, changed by an administrator, themselves
|
|
2326
|
+
// on the Settings page or through their agent here (ADR-0021).
|
|
2327
|
+
const WORKSPACE_FLAGS =
|
|
2328
|
+
"--people-visibility <everyone|shared-projects>, --week-start <auto|monday|sunday>, --time-zone <zone|none>";
|
|
2329
|
+
const WORKSPACE_USAGE = `Say what to do: console workspace | console workspace edit [${WORKSPACE_FLAGS.replaceAll(", ", "] [")}] | console workspace pack-token --connect|--stdin|--remove`;
|
|
2330
|
+
|
|
2331
|
+
const workspace: Command = async (args, io) => {
|
|
2332
|
+
const [verb, ...rest] = args;
|
|
2333
|
+
if (verb === undefined || verb.startsWith("--")) {
|
|
2334
|
+
const { values } = parse(args, {}, false);
|
|
2335
|
+
const session = await open(io, values);
|
|
2336
|
+
const me = await session.client.me();
|
|
2337
|
+
await rememberZone(io, session, me.workspace.timeZone ?? undefined);
|
|
2338
|
+
answer(io, session, me.workspace, () => formatWorkspace(me.workspace));
|
|
2339
|
+
return EXIT.ok;
|
|
2340
|
+
}
|
|
2341
|
+
if (verb === "edit") {
|
|
2342
|
+
const { values } = parse(
|
|
2343
|
+
rest,
|
|
2344
|
+
{
|
|
2345
|
+
"people-visibility": { type: "string" },
|
|
2346
|
+
"week-start": { type: "string" },
|
|
2347
|
+
"time-zone": { type: "string" },
|
|
2348
|
+
},
|
|
2349
|
+
false,
|
|
2350
|
+
);
|
|
2351
|
+
const visibility = text(values, "people-visibility");
|
|
2352
|
+
const weekStart = text(values, "week-start");
|
|
2353
|
+
const zone = text(values, "time-zone");
|
|
2354
|
+
if (visibility === undefined && weekStart === undefined && zone === undefined) {
|
|
2355
|
+
throw misuse(`Say what to change: ${WORKSPACE_FLAGS}`);
|
|
2356
|
+
}
|
|
2357
|
+
const session = await open(io, values);
|
|
2358
|
+
const changed = await session.client.updateWorkspace({
|
|
2359
|
+
...(visibility === undefined ? {} : { peopleVisibility: peopleVisibilityOf(visibility) }),
|
|
2360
|
+
...(weekStart === undefined
|
|
2361
|
+
? {}
|
|
2362
|
+
: { weekStart: wordOf(weekStart, WEEK_STARTS, "a day the week begins on") }),
|
|
2363
|
+
...(zone === undefined ? {} : { timeZone: zone === "none" ? null : zone }),
|
|
2364
|
+
});
|
|
2365
|
+
await rememberZone(io, session, changed.timeZone ?? undefined);
|
|
2366
|
+
answer(io, session, changed, () => formatWorkspace(changed));
|
|
2367
|
+
return EXIT.ok;
|
|
2368
|
+
}
|
|
2369
|
+
if (verb === "pack-token") {
|
|
2370
|
+
const { values, positionals } = parse(rest, PACK_TOKEN_FLAGS);
|
|
2371
|
+
if (positionals.length > 0) {
|
|
2372
|
+
throw misuse(
|
|
2373
|
+
"The workspace's token names no project: console workspace pack-token --connect, --stdin or --remove",
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
const way = wayOf(values);
|
|
2377
|
+
const session = await open(io, values);
|
|
2378
|
+
const { client } = session;
|
|
2379
|
+
return packToken(
|
|
2380
|
+
io,
|
|
2381
|
+
session,
|
|
2382
|
+
way,
|
|
2383
|
+
{
|
|
2384
|
+
set: (token) => client.setWorkspacePackToken({ token }),
|
|
2385
|
+
remove: () => client.removeWorkspacePackToken(),
|
|
2386
|
+
connect: () => client.connectWorkspacePackToken(),
|
|
2387
|
+
async standing() {
|
|
2388
|
+
const state = await client.workspacePackTokenConnection();
|
|
2389
|
+
return state.status === "connected"
|
|
2390
|
+
? { status: "connected", held: state.workspace }
|
|
2391
|
+
: state;
|
|
2392
|
+
},
|
|
2393
|
+
},
|
|
2394
|
+
formatWorkspace,
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
throw misuse(WORKSPACE_USAGE);
|
|
2398
|
+
};
|
|
2399
|
+
|
|
2189
2400
|
const PERSON_USAGE =
|
|
2190
2401
|
"Say what to do: console person role <login> <admin|member> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>";
|
|
2191
2402
|
|
|
@@ -2309,35 +2520,118 @@ const editProject: Command = async (args, io) => {
|
|
|
2309
2520
|
return EXIT.ok;
|
|
2310
2521
|
};
|
|
2311
2522
|
|
|
2312
|
-
/**
|
|
2313
|
-
const
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2523
|
+
/** How a pack's connection ended short of a token (ADR-0036), in words for the person. */
|
|
2524
|
+
const PACK_CONNECT_ENDED = {
|
|
2525
|
+
expired: "The code expired before anyone approved it. Run the command again for a new one.",
|
|
2526
|
+
declined: "The request was declined at SkillCDN, or the code was spent.",
|
|
2527
|
+
refused:
|
|
2528
|
+
"SkillCDN refused the request: the repository is not one the person may open, the pack is public and needs no token, or they hold as many tokens as one may.",
|
|
2529
|
+
} as const;
|
|
2530
|
+
|
|
2531
|
+
/** How a SkillCDN token is to change, one of the three the flags name. */
|
|
2532
|
+
const wayOf = (values: Values): "connect" | "stdin" | "remove" => {
|
|
2533
|
+
const ways = (["connect", "stdin", "remove"] as const).filter((way) => on(values, way));
|
|
2534
|
+
const [way] = ways;
|
|
2535
|
+
if (way === undefined || ways.length !== 1) {
|
|
2323
2536
|
throw misuse(
|
|
2324
|
-
"Say one of --stdin, which reads
|
|
2537
|
+
"Say one of --connect, which asks SkillCDN for a code a person approves; --stdin, which reads a token made by hand from standard input; and --remove, which takes the token away.",
|
|
2325
2538
|
);
|
|
2326
2539
|
}
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2540
|
+
return way;
|
|
2541
|
+
};
|
|
2542
|
+
|
|
2543
|
+
/** Whose SkillCDN token the command works: a project's, or the workspace's for the organization's pack. */
|
|
2544
|
+
interface TokenHolder<T> {
|
|
2545
|
+
set(token: string): Promise<T>;
|
|
2546
|
+
remove(): Promise<T>;
|
|
2547
|
+
connect(): Promise<RestPackConnectStarted>;
|
|
2548
|
+
standing(): Promise<
|
|
2549
|
+
| { readonly status: "none" }
|
|
2550
|
+
| { readonly status: "pending" }
|
|
2551
|
+
| { readonly status: "ended"; readonly reason: keyof typeof PACK_CONNECT_ENDED }
|
|
2552
|
+
| { readonly status: "connected"; readonly held: T }
|
|
2553
|
+
>;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
/**
|
|
2557
|
+
* The SkillCDN token (ADR-0028): connected with a code a person approves at SkillCDN (ADR-0036),
|
|
2558
|
+
* or read from standard input and never from an argument, so that no shell keeps it; or taken
|
|
2559
|
+
* away. The same for a project's and for the workspace's.
|
|
2560
|
+
*/
|
|
2561
|
+
async function packToken<T>(
|
|
2562
|
+
io: CliIo,
|
|
2563
|
+
session: Session,
|
|
2564
|
+
way: "connect" | "stdin" | "remove",
|
|
2565
|
+
holder: TokenHolder<T>,
|
|
2566
|
+
format: (held: T) => string,
|
|
2567
|
+
): Promise<number> {
|
|
2568
|
+
if (way === "remove") {
|
|
2569
|
+
const changed = await holder.remove();
|
|
2570
|
+
answer(io, session, changed, () => format(changed));
|
|
2332
2571
|
return EXIT.ok;
|
|
2333
2572
|
}
|
|
2573
|
+
if (way === "connect") {
|
|
2574
|
+
const started = await holder.connect();
|
|
2575
|
+
io.stdout(
|
|
2576
|
+
`To connect the pack, a person who may open its repository opens ${started.url} signed in at SkillCDN, checks that the page shows the code ${started.code}, and approves; the console receives the SkillCDN token and keeps it sealed, shown to nobody.\nWaiting for that, until ${moment(started.expiresAt)}.\n`,
|
|
2577
|
+
);
|
|
2578
|
+
for (;;) {
|
|
2579
|
+
await io.sleep(started.intervalSeconds * 1000);
|
|
2580
|
+
const state = await holder.standing();
|
|
2581
|
+
if (state.status === "pending") {
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2584
|
+
if (state.status === "connected") {
|
|
2585
|
+
answer(io, session, state.held, () => `Connected.\n${format(state.held)}`);
|
|
2586
|
+
return EXIT.ok;
|
|
2587
|
+
}
|
|
2588
|
+
if (state.status === "ended") {
|
|
2589
|
+
throw failed(PACK_CONNECT_ENDED[state.reason]);
|
|
2590
|
+
}
|
|
2591
|
+
throw failed(
|
|
2592
|
+
"The console no longer waits for the connection; run the command again for a new code.",
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2334
2596
|
const token = (await io.readStdin()).trim();
|
|
2335
2597
|
if (token.length === 0) {
|
|
2336
2598
|
throw failed("No token was given on standard input.");
|
|
2337
2599
|
}
|
|
2338
|
-
const changed = await
|
|
2339
|
-
answer(io, session, changed, () =>
|
|
2600
|
+
const changed = await holder.set(token);
|
|
2601
|
+
answer(io, session, changed, () => format(changed));
|
|
2340
2602
|
return EXIT.ok;
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
const PACK_TOKEN_FLAGS = {
|
|
2606
|
+
connect: { type: "boolean" },
|
|
2607
|
+
stdin: { type: "boolean" },
|
|
2608
|
+
remove: { type: "boolean" },
|
|
2609
|
+
} as const;
|
|
2610
|
+
|
|
2611
|
+
const projectPackToken: Command = async (args, io) => {
|
|
2612
|
+
const { values, positionals } = parse(args, PACK_TOKEN_FLAGS);
|
|
2613
|
+
const key = theOne(
|
|
2614
|
+
positionals,
|
|
2615
|
+
"Say which project: console project pack-token <key> --connect, --stdin or --remove",
|
|
2616
|
+
);
|
|
2617
|
+
const way = wayOf(values);
|
|
2618
|
+
const session = await open(io, values);
|
|
2619
|
+
const project = session.client.project(key);
|
|
2620
|
+
return packToken(
|
|
2621
|
+
io,
|
|
2622
|
+
session,
|
|
2623
|
+
way,
|
|
2624
|
+
{
|
|
2625
|
+
set: (token) => project.setPackToken({ token }),
|
|
2626
|
+
remove: () => project.removePackToken(),
|
|
2627
|
+
connect: () => project.connectPackToken(),
|
|
2628
|
+
async standing() {
|
|
2629
|
+
const state = await project.packTokenConnection();
|
|
2630
|
+
return state.status === "connected" ? { status: "connected", held: state.project } : state;
|
|
2631
|
+
},
|
|
2632
|
+
},
|
|
2633
|
+
formatProject,
|
|
2634
|
+
);
|
|
2341
2635
|
};
|
|
2342
2636
|
|
|
2343
2637
|
const projectIcon: Command = async (args, io) => {
|
|
@@ -2564,6 +2858,7 @@ const COMMANDS: Readonly<Record<string, Command>> = {
|
|
|
2564
2858
|
rules,
|
|
2565
2859
|
people,
|
|
2566
2860
|
person,
|
|
2861
|
+
workspace,
|
|
2567
2862
|
project,
|
|
2568
2863
|
members,
|
|
2569
2864
|
member,
|
|
@@ -2674,11 +2969,39 @@ async function noticeVersions(io: CliIo, seen: ConsoleSeen): Promise<void> {
|
|
|
2674
2969
|
});
|
|
2675
2970
|
}
|
|
2676
2971
|
|
|
2972
|
+
/**
|
|
2973
|
+
* The refusal of a console older than this command works with (ADR-0038), in a console's own
|
|
2974
|
+
* form: a `426` the command reads as it reads the console's, before it acts on anything the
|
|
2975
|
+
* console answered.
|
|
2976
|
+
*/
|
|
2977
|
+
function consoleRefusal(io: CliIo, seen: ConsoleSeen): Response | undefined {
|
|
2978
|
+
const command = parseVersion(io.version);
|
|
2979
|
+
const server = parseVersion(seen.version);
|
|
2980
|
+
if (command === undefined || server === undefined) {
|
|
2981
|
+
return undefined;
|
|
2982
|
+
}
|
|
2983
|
+
const { verdict, range } = judgeConsole(server, command, io.consoleAtLeast ?? CONSOLE_AT_LEAST);
|
|
2984
|
+
if (verdict !== "outdated") {
|
|
2985
|
+
return undefined;
|
|
2986
|
+
}
|
|
2987
|
+
return Response.json(
|
|
2988
|
+
{
|
|
2989
|
+
error: {
|
|
2990
|
+
code: "console.outdated",
|
|
2991
|
+
message: `This console, ${seen.version}, is older than this command (${io.version}) works with (${formatRange(range)}).`,
|
|
2992
|
+
},
|
|
2993
|
+
},
|
|
2994
|
+
{ status: 426 },
|
|
2995
|
+
);
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2677
2998
|
/** What a refusal of the console is followed by, when there is something to do about it. */
|
|
2678
2999
|
function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
|
|
2679
3000
|
switch (error.code) {
|
|
2680
3001
|
case "client.outdated":
|
|
2681
3002
|
return `Update the command: ${updateWords(io, seen.version)}\n`;
|
|
3003
|
+
case "console.outdated":
|
|
3004
|
+
return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
|
|
2682
3005
|
case "client.too_new":
|
|
2683
3006
|
return `Install the console's version: ${updateWords(io, seen.version)}; or ask the console's operator to update the console.\n`;
|
|
2684
3007
|
case "auth.required":
|
|
@@ -2715,6 +3038,8 @@ function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
|
|
|
2715
3038
|
return "The person is listed already. console member role <login> <owner|member> changes what they are.\n";
|
|
2716
3039
|
case "member.not_found":
|
|
2717
3040
|
return "The person is not listed in the project. console members lists those who are.\n";
|
|
3041
|
+
case "member.open_project":
|
|
3042
|
+
return "The project is open to the workspace, so everyone of it is a member already. console member add <login> --role owner lists an owner; console member role <login> member takes one back to member.\n";
|
|
2718
3043
|
case "category.key_taken":
|
|
2719
3044
|
return "A category has that key already. console categories lists them.\n";
|
|
2720
3045
|
case "category.not_found":
|
|
@@ -2752,7 +3077,13 @@ export async function runCli(argv: readonly string[], io: CliIo): Promise<number
|
|
|
2752
3077
|
const response = await io.fetch(input, init);
|
|
2753
3078
|
seen.version = response.headers.get(VERSION_HEADER) ?? seen.version;
|
|
2754
3079
|
seen.range = response.headers.get(CLIENT_RANGE_HEADER) ?? seen.range;
|
|
2755
|
-
|
|
3080
|
+
// A console older than this command works with is refused here (ADR-0038); `serve`
|
|
3081
|
+
// carries the pages' own requests, which are not the command's to judge.
|
|
3082
|
+
// A refusal of the console's own is passed on as it is.
|
|
3083
|
+
return (
|
|
3084
|
+
(name === "serve" || response.status === 426 ? undefined : consoleRefusal(io, seen)) ??
|
|
3085
|
+
response
|
|
3086
|
+
);
|
|
2756
3087
|
},
|
|
2757
3088
|
};
|
|
2758
3089
|
try {
|
|
@@ -2784,7 +3115,9 @@ export async function runCli(argv: readonly string[], io: CliIo): Promise<number
|
|
|
2784
3115
|
}
|
|
2785
3116
|
if (error instanceof ApiError) {
|
|
2786
3117
|
io.stderr(`${error.code}: ${error.message}\n${hintFor(error, io, seen)}`);
|
|
2787
|
-
return error.code === "client.outdated" ||
|
|
3118
|
+
return error.code === "client.outdated" ||
|
|
3119
|
+
error.code === "client.too_new" ||
|
|
3120
|
+
error.code === "console.outdated"
|
|
2788
3121
|
? EXIT.incompatible
|
|
2789
3122
|
: EXIT.failed;
|
|
2790
3123
|
}
|