@skillcdn/console 0.1.24 → 0.3.1
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 +8 -7
- package/dist/api.d.ts +2 -2
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +2 -2
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +1 -1
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +248 -92
- 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 +13 -28
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +74 -274
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +36 -19
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/pack.d.ts +46 -0
- package/dist/cli/pack.d.ts.map +1 -0
- package/dist/cli/pack.js +250 -0
- package/dist/cli/pack.js.map +1 -0
- package/dist/client.d.ts +26 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -1
- package/dist/client.js.map +1 -1
- package/dist/compat.d.ts +1 -1
- package/dist/compat.js +1 -1
- package/dist/components/calendar.d.ts +5 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/decision-list.d.ts +13 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +74 -29
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +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/paged.d.ts +24 -0
- package/dist/components/paged.d.ts.map +1 -1
- package/dist/components/paged.js +40 -4
- package/dist/components/paged.js.map +1 -1
- 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/skills.d.ts.map +1 -1
- package/dist/components/skills.js +8 -4
- package/dist/components/skills.js.map +1 -1
- package/dist/components/ui.d.ts +2 -0
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +7 -2
- 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 +1 -1
- 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 +268 -5
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +62 -23
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +16 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +64 -2
- 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 +66 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +80 -8
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +75 -7
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/router.d.ts +10 -5
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +24 -9
- 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 +500 -158
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +169 -165
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +19 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +16 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +19 -14
- package/docs/specs/rest.md +34 -19
- package/package.json +2 -1
- package/skills/building-a-console/SKILL.md +5 -5
- 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/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 +2 -0
- package/skills/working-the-board/references/the-sentence.md +1 -1
- package/src/api.ts +18 -7
- package/src/cli/cli.ts +345 -109
- package/src/cli/credentials.ts +7 -0
- package/src/cli/directory.ts +9 -0
- package/src/cli/format.ts +82 -341
- package/src/cli/help.ts +43 -19
- package/src/cli/pack.ts +356 -0
- package/src/client.ts +84 -0
- package/src/compat.ts +1 -1
- package/src/components/calendar.tsx +8 -2
- package/src/components/decision-list.tsx +227 -94
- 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/paged.tsx +72 -3
- package/src/components/projects.tsx +108 -9
- package/src/components/skills.tsx +51 -33
- package/src/components/ui.tsx +16 -3
- package/src/components/work.tsx +4 -1
- package/src/components/workspace.tsx +298 -0
- package/src/console.tsx +176 -29
- package/src/data.ts +89 -2
- package/src/dates.ts +23 -2
- package/src/i18n/en.ts +94 -8
- package/src/i18n/ko.ts +84 -7
- package/src/index.ts +25 -1
- package/src/router.ts +37 -14
- package/src/routes.ts +4 -0
- package/src/schemas.ts +202 -189
- package/src/styles/console.css +268 -5
- package/src/vocabulary.ts +25 -0
package/src/cli/cli.ts
CHANGED
|
@@ -16,11 +16,12 @@ import {
|
|
|
16
16
|
type RestClaim,
|
|
17
17
|
type RestDecision,
|
|
18
18
|
type RestMember,
|
|
19
|
+
type RestPackConnectStarted,
|
|
19
20
|
type RestPerson,
|
|
20
21
|
type RestSkillBrowse,
|
|
21
22
|
type RestSkillFile,
|
|
22
23
|
type RestSkillLoad,
|
|
23
|
-
type
|
|
24
|
+
type RestSkills,
|
|
24
25
|
type RestTaskPatch,
|
|
25
26
|
} from "../schemas.js";
|
|
26
27
|
import {
|
|
@@ -28,7 +29,9 @@ import {
|
|
|
28
29
|
type ColorName,
|
|
29
30
|
GOAL_STATES,
|
|
30
31
|
type GoalState,
|
|
32
|
+
PEOPLE_VISIBILITIES,
|
|
31
33
|
PERSON_ROLES,
|
|
34
|
+
type PeopleVisibility,
|
|
32
35
|
type PersonRole,
|
|
33
36
|
PROJECT_ROLES,
|
|
34
37
|
PROJECT_VISIBILITIES,
|
|
@@ -40,6 +43,7 @@ import {
|
|
|
40
43
|
TASK_STATES,
|
|
41
44
|
type TaskPriority,
|
|
42
45
|
type TaskState,
|
|
46
|
+
WEEK_STARTS,
|
|
43
47
|
} from "../vocabulary.js";
|
|
44
48
|
import { loadWebRoot, type WebRoot, WebRootError } from "../web.js";
|
|
45
49
|
import {
|
|
@@ -53,7 +57,7 @@ import {
|
|
|
53
57
|
withLogin,
|
|
54
58
|
withoutLogin,
|
|
55
59
|
} from "./credentials.js";
|
|
56
|
-
import type
|
|
60
|
+
import { type DirectorySettings, type DirectoryStore, THE_SENTENCE } from "./directory.js";
|
|
57
61
|
import {
|
|
58
62
|
formatAnswer,
|
|
59
63
|
formatCategoryLine,
|
|
@@ -75,20 +79,34 @@ import {
|
|
|
75
79
|
formatRules,
|
|
76
80
|
formatRun,
|
|
77
81
|
formatRunLine,
|
|
78
|
-
formatSkillBrowse,
|
|
79
|
-
formatSkillFile,
|
|
80
|
-
formatSkillFind,
|
|
81
|
-
formatSkillLoad,
|
|
82
|
-
formatSkills,
|
|
83
82
|
formatTask,
|
|
84
83
|
formatTaskLine,
|
|
85
84
|
formatVersion,
|
|
86
85
|
formatVersionLine,
|
|
87
86
|
formatWork,
|
|
87
|
+
formatWorkspace,
|
|
88
|
+
moment,
|
|
89
|
+
setMomentZone,
|
|
88
90
|
} from "./format.js";
|
|
89
91
|
import { COMMAND_HELP, helpFor, ownSkillPath, usage } from "./help.js";
|
|
90
92
|
import { describeInstall, type Install, updateCommandFor } from "./install.js";
|
|
91
93
|
import type { NoticeStore } from "./notices.js";
|
|
94
|
+
import {
|
|
95
|
+
bodyOfFile,
|
|
96
|
+
bodyOfFolder,
|
|
97
|
+
bodyOfSkill,
|
|
98
|
+
formatSkillBrowse,
|
|
99
|
+
formatSkillFile,
|
|
100
|
+
formatSkillFind,
|
|
101
|
+
formatSkillLoad,
|
|
102
|
+
formatSkills,
|
|
103
|
+
joinFile,
|
|
104
|
+
joinFolder,
|
|
105
|
+
joinSkill,
|
|
106
|
+
type PackSummary,
|
|
107
|
+
skillPathIn,
|
|
108
|
+
summaryOf,
|
|
109
|
+
} from "./pack.js";
|
|
92
110
|
import { createServeHandler, DEFAULT_SERVE_PORT, type Listen } from "./serve.js";
|
|
93
111
|
|
|
94
112
|
// The `console` command (docs/specs/cli.md): the agent's side of the console. A thin client of
|
|
@@ -299,6 +317,14 @@ async function open(io: CliIo, values: Values): Promise<Session> {
|
|
|
299
317
|
throw failed(wordsOf(resolved));
|
|
300
318
|
}
|
|
301
319
|
const { url, token } = resolved;
|
|
320
|
+
// Moments are printed in the workspace's time zone as the login kept it (ADR-0035); the
|
|
321
|
+
// environment's pair keeps nothing, and they stay as the API gives them.
|
|
322
|
+
setMomentZone(
|
|
323
|
+
resolved.via === "login"
|
|
324
|
+
? credentials?.logins.find((kept) => kept.url === url && kept.account === resolved.account)
|
|
325
|
+
?.timeZone
|
|
326
|
+
: undefined,
|
|
327
|
+
);
|
|
302
328
|
const client = clientOf(io, url, token);
|
|
303
329
|
const projectKey = async () =>
|
|
304
330
|
text(values, "project") ?? io.env.CONSOLE_PROJECT ?? directory?.project;
|
|
@@ -473,18 +499,25 @@ async function personIdOf(
|
|
|
473
499
|
return (await personOf(session, value)).id;
|
|
474
500
|
}
|
|
475
501
|
|
|
476
|
-
/**
|
|
502
|
+
/**
|
|
503
|
+
* The person a login names, among the workspace's, found by their exact login as people sign in
|
|
504
|
+
* (ADR-0035): whom the console shows the person, and, for an owner of a project, anyone of the
|
|
505
|
+
* workspace, so that someone the People page does not show can still be listed.
|
|
506
|
+
*/
|
|
477
507
|
async function personOf(session: Session, login: string): Promise<RestPerson> {
|
|
478
|
-
const
|
|
479
|
-
const found = items.find((person) => person.login.toLowerCase() === login.toLowerCase());
|
|
508
|
+
const found = await session.client.findPerson(login);
|
|
480
509
|
if (found === undefined) {
|
|
481
510
|
throw misuse(
|
|
482
|
-
`Nobody of the workspace is called ${login}. console people lists
|
|
511
|
+
`Nobody of the workspace is called ${login}, or they are not yours to see. console people lists whom you see, by login.`,
|
|
483
512
|
);
|
|
484
513
|
}
|
|
485
514
|
return found;
|
|
486
515
|
}
|
|
487
516
|
|
|
517
|
+
/** Who sees whom, as the command takes it: with a hyphen or an underscore. */
|
|
518
|
+
const peopleVisibilityOf = (value: string): PeopleVisibility =>
|
|
519
|
+
wordOf(value.replaceAll("-", "_"), PEOPLE_VISIBILITIES, "who sees whom");
|
|
520
|
+
|
|
488
521
|
/** One word of a vocabulary, or the refusal to understand. */
|
|
489
522
|
function wordOf<T extends string>(value: string, words: readonly T[], what: string): T {
|
|
490
523
|
const found = words.find((word) => word === value);
|
|
@@ -641,7 +674,14 @@ async function keep(
|
|
|
641
674
|
}
|
|
642
675
|
const next = withLogin(
|
|
643
676
|
credentials,
|
|
644
|
-
{
|
|
677
|
+
{
|
|
678
|
+
url,
|
|
679
|
+
account: person.login,
|
|
680
|
+
token,
|
|
681
|
+
agent,
|
|
682
|
+
since: new Date(io.now()).toISOString(),
|
|
683
|
+
timeZone: me.workspace.timeZone ?? undefined,
|
|
684
|
+
},
|
|
645
685
|
{ default: asDefault },
|
|
646
686
|
);
|
|
647
687
|
const kept = await io.store.save(next);
|
|
@@ -815,6 +855,24 @@ function agentOf(env: CliIo["env"]): string | undefined {
|
|
|
815
855
|
return undefined;
|
|
816
856
|
}
|
|
817
857
|
|
|
858
|
+
/**
|
|
859
|
+
* Keeps the workspace's time zone on the login the command used (ADR-0035), so that moments are
|
|
860
|
+
* printed in it from now on without asking; the environment's pair keeps nothing.
|
|
861
|
+
*/
|
|
862
|
+
async function rememberZone(io: CliIo, session: Session, zone: string | undefined): Promise<void> {
|
|
863
|
+
setMomentZone(zone);
|
|
864
|
+
if (session.via !== "login" || session.credentials === undefined) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const kept = session.credentials.logins.find(
|
|
868
|
+
(login) => login.url === session.url && login.account === session.account,
|
|
869
|
+
);
|
|
870
|
+
if (kept === undefined || kept.timeZone === zone) {
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
await io.store.save(withLogin(session.credentials, { ...kept, timeZone: zone }));
|
|
874
|
+
}
|
|
875
|
+
|
|
818
876
|
const whoami: Command = async (args, io) => {
|
|
819
877
|
const { values } = parse(args, {}, false);
|
|
820
878
|
const session = await open(io, values);
|
|
@@ -823,6 +881,7 @@ const whoami: Command = async (args, io) => {
|
|
|
823
881
|
if (person === null) {
|
|
824
882
|
throw failed("The console does not know this token. Sign in again: console login");
|
|
825
883
|
}
|
|
884
|
+
await rememberZone(io, session, me.workspace.timeZone ?? undefined);
|
|
826
885
|
const key = await session.projectKey();
|
|
827
886
|
const command = parseVersion(io.version);
|
|
828
887
|
const server = parseVersion(me.console?.version);
|
|
@@ -903,12 +962,15 @@ const use: Command = async (args, io) => {
|
|
|
903
962
|
const bound = pinUrl
|
|
904
963
|
? ` at ${session.url}${pinAccount && session.account !== undefined ? ` as ${session.account}` : ""}`
|
|
905
964
|
: "";
|
|
906
|
-
answer(
|
|
907
|
-
|
|
908
|
-
session,
|
|
909
|
-
project,
|
|
910
|
-
() =>
|
|
965
|
+
answer(io, session, project, () =>
|
|
966
|
+
[
|
|
911
967
|
`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.`,
|
|
968
|
+
// Nothing sends an agent to the console until the checkout says so; the sentence is the
|
|
969
|
+
// skill's (references/the-sentence.md), printed here as `skillcdn use` prints its own.
|
|
970
|
+
"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:",
|
|
971
|
+
"",
|
|
972
|
+
THE_SENTENCE,
|
|
973
|
+
].join("\n"),
|
|
912
974
|
);
|
|
913
975
|
return EXIT.ok;
|
|
914
976
|
};
|
|
@@ -1003,10 +1065,19 @@ const MAX_SKILL_PAGES = 64;
|
|
|
1003
1065
|
/** How many results a search answers at most: what the deployment allows on one page. */
|
|
1004
1066
|
const MAX_FIND_LIMIT = 25;
|
|
1005
1067
|
|
|
1068
|
+
/** The pack of the project as the console lists it, and what its reads are rendered at; none when the pack did not answer. */
|
|
1069
|
+
async function packOf(
|
|
1070
|
+
project: ProjectClient,
|
|
1071
|
+
): Promise<{ readonly listing: RestSkills; readonly summary: PackSummary | undefined }> {
|
|
1072
|
+
const listing = await project.skills();
|
|
1073
|
+
return { listing, summary: summaryOf(listing) };
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1006
1076
|
const skills: Command = async (args, io) => {
|
|
1007
1077
|
const { values, positionals } = parse(args, {
|
|
1008
1078
|
search: { type: "string" },
|
|
1009
1079
|
limit: { type: "string" },
|
|
1080
|
+
cursor: { type: "string" },
|
|
1010
1081
|
});
|
|
1011
1082
|
if (positionals.length > 1) {
|
|
1012
1083
|
throw misuse("Say one folder: console skills [<folder>] [--search <words>]");
|
|
@@ -1016,8 +1087,11 @@ const skills: Command = async (args, io) => {
|
|
|
1016
1087
|
const folder = given === undefined || given === "/" || given === "." ? undefined : given;
|
|
1017
1088
|
const words = text(values, "search");
|
|
1018
1089
|
const limit = text(values, "limit");
|
|
1019
|
-
|
|
1020
|
-
|
|
1090
|
+
const cursor = text(values, "cursor");
|
|
1091
|
+
if (words === undefined && (limit !== undefined || cursor !== undefined)) {
|
|
1092
|
+
throw misuse(
|
|
1093
|
+
"--limit and --cursor go with --search: console skills --search <words> [--limit <n>] [--cursor <cursor>]",
|
|
1094
|
+
);
|
|
1021
1095
|
}
|
|
1022
1096
|
const atMost = limit === undefined ? undefined : Number(limit);
|
|
1023
1097
|
if (
|
|
@@ -1028,37 +1102,41 @@ const skills: Command = async (args, io) => {
|
|
|
1028
1102
|
}
|
|
1029
1103
|
const session = await open(io, values);
|
|
1030
1104
|
const project = await session.project();
|
|
1105
|
+
const { listing, summary } = await packOf(project);
|
|
1106
|
+
// The pack as it stands; or why it does not answer, which a folder or a search would meet too.
|
|
1107
|
+
if (summary === undefined || (words === undefined && given === undefined)) {
|
|
1108
|
+
answer(io, session, listing, () => formatSkills(listing, io.skillsDir));
|
|
1109
|
+
return EXIT.ok;
|
|
1110
|
+
}
|
|
1031
1111
|
if (words !== undefined) {
|
|
1032
|
-
// One page of what the words find; more is said, and
|
|
1033
|
-
const found = await project.skillFind(words, { path: folder, limit: atMost });
|
|
1034
|
-
answer(io, session, found, () => formatSkillFind(found));
|
|
1112
|
+
// One page of what the words find; more is said, and asked for with --cursor.
|
|
1113
|
+
const found = await project.skillFind(words, { path: folder, limit: atMost, cursor });
|
|
1114
|
+
answer(io, session, found, () => formatSkillFind(found, summary));
|
|
1035
1115
|
return EXIT.ok;
|
|
1036
1116
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
break;
|
|
1046
|
-
}
|
|
1047
|
-
folders.push(last.folder);
|
|
1048
|
-
if (last.folder.nextCursor === null) {
|
|
1049
|
-
break;
|
|
1050
|
-
}
|
|
1051
|
-
cursor = last.folder.nextCursor;
|
|
1117
|
+
// The folder whole: its pages until the end, as the pack lists them.
|
|
1118
|
+
const pages: RestSkillBrowse[] = [];
|
|
1119
|
+
let next: string | undefined;
|
|
1120
|
+
let last: RestSkillBrowse | undefined;
|
|
1121
|
+
for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
|
|
1122
|
+
last = await project.skillBrowse({ path: folder, cursor: next });
|
|
1123
|
+
if (last.read?.status !== "ready") {
|
|
1124
|
+
break;
|
|
1052
1125
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1126
|
+
pages.push(last);
|
|
1127
|
+
next = last.read.nextCursor ?? undefined;
|
|
1128
|
+
if (next === undefined) {
|
|
1129
|
+
break;
|
|
1055
1130
|
}
|
|
1056
|
-
const browsed = last;
|
|
1057
|
-
answer(io, session, { ...browsed, folders }, () => formatSkillBrowse(browsed, folders));
|
|
1058
|
-
return EXIT.ok;
|
|
1059
1131
|
}
|
|
1060
|
-
|
|
1061
|
-
|
|
1132
|
+
if (last === undefined) {
|
|
1133
|
+
throw failed("The folder could not be read.");
|
|
1134
|
+
}
|
|
1135
|
+
const browsed = last;
|
|
1136
|
+
const whole = joinFolder(pages, summary);
|
|
1137
|
+
answer(io, session, { ...browsed, read: whole === undefined ? null : bodyOfFolder(whole) }, () =>
|
|
1138
|
+
formatSkillBrowse(browsed, whole),
|
|
1139
|
+
);
|
|
1062
1140
|
return EXIT.ok;
|
|
1063
1141
|
};
|
|
1064
1142
|
|
|
@@ -1074,84 +1152,73 @@ const skill: Command = async (args, io) => {
|
|
|
1074
1152
|
}
|
|
1075
1153
|
const session = await open(io, values);
|
|
1076
1154
|
const project = await session.project();
|
|
1155
|
+
const { listing, summary } = await packOf(project);
|
|
1156
|
+
if (summary === undefined) {
|
|
1157
|
+
answer(io, session, listing, () => formatSkills(listing, io.skillsDir));
|
|
1158
|
+
return EXIT.ok;
|
|
1159
|
+
}
|
|
1077
1160
|
if (file !== undefined) {
|
|
1078
|
-
// The file whole, page after page, as the console reads it from the
|
|
1079
|
-
const
|
|
1161
|
+
// The file whole, page after page, as the console reads it from the pack.
|
|
1162
|
+
const pages: RestSkillFile[] = [];
|
|
1080
1163
|
let offset: number | undefined;
|
|
1081
1164
|
let last: RestSkillFile | undefined;
|
|
1082
|
-
for (let
|
|
1165
|
+
for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
|
|
1083
1166
|
last = await project.skillFile(file, offset === undefined ? {} : { offset });
|
|
1084
|
-
if (last.
|
|
1167
|
+
if (last.read === null) {
|
|
1085
1168
|
break;
|
|
1086
1169
|
}
|
|
1087
|
-
|
|
1088
|
-
if (last.file.nextOffset === null) {
|
|
1170
|
+
pages.push(last);
|
|
1171
|
+
if (last.read.kind !== "file" || last.read.nextOffset === null) {
|
|
1089
1172
|
break;
|
|
1090
1173
|
}
|
|
1091
|
-
offset = last.
|
|
1174
|
+
offset = last.read.nextOffset;
|
|
1092
1175
|
}
|
|
1093
1176
|
if (last === undefined) {
|
|
1094
1177
|
throw failed("The file could not be read.");
|
|
1095
1178
|
}
|
|
1096
|
-
const
|
|
1097
|
-
const
|
|
1098
|
-
answer(io, session, { ...
|
|
1179
|
+
const fileRead = last;
|
|
1180
|
+
const whole = joinFile(pages, summary);
|
|
1181
|
+
answer(io, session, { ...fileRead, read: whole === undefined ? null : bodyOfFile(whole) }, () =>
|
|
1182
|
+
formatSkillFile(fileRead, whole),
|
|
1183
|
+
);
|
|
1099
1184
|
return EXIT.ok;
|
|
1100
1185
|
}
|
|
1101
|
-
const
|
|
1186
|
+
const wanted = skillPathIn(
|
|
1102
1187
|
theOne(
|
|
1103
1188
|
positionals,
|
|
1104
1189
|
"Say which skill: console skill <path of its SKILL.md>, or console skill --file <path> for a file of the pack",
|
|
1105
1190
|
),
|
|
1191
|
+
listing.address,
|
|
1106
1192
|
);
|
|
1193
|
+
if ("error" in wanted) {
|
|
1194
|
+
throw misuse(wanted.error);
|
|
1195
|
+
}
|
|
1107
1196
|
// The skill whole: its pages until complete, the rules first, as load_skill lays them out.
|
|
1108
|
-
const pages:
|
|
1197
|
+
const pages: RestSkillLoad[] = [];
|
|
1109
1198
|
let cursor: string | undefined;
|
|
1110
1199
|
let last: RestSkillLoad | undefined;
|
|
1111
1200
|
for (let n = 0; n < MAX_SKILL_PAGES; n += 1) {
|
|
1112
|
-
last = await project.skill(path, cursor);
|
|
1113
|
-
if (last.status !== "ready"
|
|
1201
|
+
last = await project.skill(wanted.path, cursor);
|
|
1202
|
+
if (last.read?.status !== "ready") {
|
|
1114
1203
|
break;
|
|
1115
1204
|
}
|
|
1116
|
-
pages.push(last
|
|
1117
|
-
|
|
1205
|
+
pages.push(last);
|
|
1206
|
+
cursor = last.read.skill.nextCursor ?? undefined;
|
|
1207
|
+
if (last.read.skill.complete === true || cursor === undefined) {
|
|
1118
1208
|
break;
|
|
1119
1209
|
}
|
|
1120
|
-
cursor = last.skill.nextCursor;
|
|
1121
1210
|
}
|
|
1122
1211
|
if (last === undefined) {
|
|
1123
1212
|
throw failed("The skill could not be read.");
|
|
1124
1213
|
}
|
|
1125
|
-
// A URI of another pack would read this pack's skill at the same path, or nothing: said instead.
|
|
1126
|
-
if (pack !== undefined && last.address !== null && last.address !== pack) {
|
|
1127
|
-
throw misuse(
|
|
1128
|
-
`That URI is of another pack, ${pack}; this project's pack is at ${last.address}. Say the path of the SKILL.md, or a URI console skills lists.`,
|
|
1129
|
-
);
|
|
1130
|
-
}
|
|
1131
1214
|
const loaded = last;
|
|
1132
|
-
|
|
1215
|
+
const whole = joinSkill(pages, summary);
|
|
1216
|
+
answer(io, session, { ...loaded, read: whole === undefined ? null : bodyOfSkill(whole) }, () =>
|
|
1217
|
+
formatSkillLoad(loaded, whole),
|
|
1218
|
+
);
|
|
1133
1219
|
return EXIT.ok;
|
|
1134
1220
|
};
|
|
1135
1221
|
|
|
1136
|
-
/**
|
|
1137
|
-
* A skill's path as given, or taken out of the URI `console skills` lists beside it
|
|
1138
|
-
* (`skill://gh/<owner>/<repo>/<path>`), with the pack the URI names, for the read to check.
|
|
1139
|
-
*/
|
|
1140
|
-
function skillPathOf(given: string): { readonly path: string; readonly pack?: string } {
|
|
1141
|
-
const uri = /^skill:\/\/(gh\/[^/]+\/[^/]+)\/(.+)$/.exec(given);
|
|
1142
|
-
const named = uri?.[1];
|
|
1143
|
-
const within = uri?.[2];
|
|
1144
|
-
if (named !== undefined && within !== undefined) {
|
|
1145
|
-
return { path: within, pack: `/${named}` };
|
|
1146
|
-
}
|
|
1147
|
-
if (given.startsWith("skill://")) {
|
|
1148
|
-
throw misuse(
|
|
1149
|
-
`Not a skill's URI the console knows: ${given}. Say the path of its SKILL.md from the repository's root, or the URI console skills lists.`,
|
|
1150
|
-
);
|
|
1151
|
-
}
|
|
1152
|
-
return { path: given };
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
1222
|
/** A version number given as a flag, or nothing; anything else is not understood. */
|
|
1156
1223
|
function versionOf(given: string | undefined): number | undefined {
|
|
1157
1224
|
if (given === undefined) {
|
|
@@ -1317,7 +1384,12 @@ const newTask: Command = async (args, io) => {
|
|
|
1317
1384
|
...(body === undefined ? {} : { body }),
|
|
1318
1385
|
...fields,
|
|
1319
1386
|
});
|
|
1320
|
-
answer(
|
|
1387
|
+
answer(
|
|
1388
|
+
io,
|
|
1389
|
+
session,
|
|
1390
|
+
made,
|
|
1391
|
+
() => `Task #${made.number} written: ${made.title}\ntask id: ${made.id}`,
|
|
1392
|
+
);
|
|
1321
1393
|
return EXIT.ok;
|
|
1322
1394
|
};
|
|
1323
1395
|
|
|
@@ -1540,6 +1612,9 @@ const take: Command = async (args, io) => {
|
|
|
1540
1612
|
answer(io, session, { run, task: taken, comments: comments.items, rules }, () =>
|
|
1541
1613
|
[
|
|
1542
1614
|
`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}`,
|
|
1615
|
+
// The ids on a line of their own, each named: a run's and a task's begin alike, and a
|
|
1616
|
+
// task has a number for people as well.
|
|
1617
|
+
`run id: ${run.id} task id: ${found.id} (task #${found.number})`,
|
|
1543
1618
|
"",
|
|
1544
1619
|
formatTask(taken, undefined, undefined, comments.items),
|
|
1545
1620
|
"",
|
|
@@ -1685,7 +1760,7 @@ const ask: Command = async (args, io) => {
|
|
|
1685
1760
|
io.stdout(
|
|
1686
1761
|
session.json
|
|
1687
1762
|
? `${JSON.stringify(raised, null, 2)}\n`
|
|
1688
|
-
: `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\n${formatOptions(raised)}\n`,
|
|
1763
|
+
: `Decision ${raised.id} raised on run ${runId}; the run waits for a person.\ndecision id: ${raised.id}\n${formatOptions(raised)}\n`,
|
|
1689
1764
|
);
|
|
1690
1765
|
return settle(io, session, project, raised, seconds, true);
|
|
1691
1766
|
};
|
|
@@ -1934,7 +2009,7 @@ const newGoal: Command = async (args, io) => {
|
|
|
1934
2009
|
...(startsOn === undefined ? {} : { startsOn }),
|
|
1935
2010
|
...(dueOn === undefined ? {} : { dueOn }),
|
|
1936
2011
|
});
|
|
1937
|
-
answer(io, session, made, () => `Goal set: ${made.title}\
|
|
2012
|
+
answer(io, session, made, () => `Goal set: ${made.title}\ngoal id: ${made.id}`);
|
|
1938
2013
|
return EXIT.ok;
|
|
1939
2014
|
};
|
|
1940
2015
|
|
|
@@ -2170,6 +2245,81 @@ const people: Command = async (args, io) => {
|
|
|
2170
2245
|
return EXIT.ok;
|
|
2171
2246
|
};
|
|
2172
2247
|
|
|
2248
|
+
// The workspace's settings (ADR-0035): read by everyone, changed by an administrator, themselves
|
|
2249
|
+
// on the Settings page or through their agent here (ADR-0021).
|
|
2250
|
+
const WORKSPACE_FLAGS =
|
|
2251
|
+
"--people-visibility <everyone|shared-projects>, --week-start <auto|monday|sunday>, --time-zone <zone|none>";
|
|
2252
|
+
const WORKSPACE_USAGE = `Say what to do: console workspace | console workspace edit [${WORKSPACE_FLAGS.replaceAll(", ", "] [")}] | console workspace pack-token --connect|--stdin|--remove`;
|
|
2253
|
+
|
|
2254
|
+
const workspace: Command = async (args, io) => {
|
|
2255
|
+
const [verb, ...rest] = args;
|
|
2256
|
+
if (verb === undefined || verb.startsWith("--")) {
|
|
2257
|
+
const { values } = parse(args, {}, false);
|
|
2258
|
+
const session = await open(io, values);
|
|
2259
|
+
const me = await session.client.me();
|
|
2260
|
+
await rememberZone(io, session, me.workspace.timeZone ?? undefined);
|
|
2261
|
+
answer(io, session, me.workspace, () => formatWorkspace(me.workspace));
|
|
2262
|
+
return EXIT.ok;
|
|
2263
|
+
}
|
|
2264
|
+
if (verb === "edit") {
|
|
2265
|
+
const { values } = parse(
|
|
2266
|
+
rest,
|
|
2267
|
+
{
|
|
2268
|
+
"people-visibility": { type: "string" },
|
|
2269
|
+
"week-start": { type: "string" },
|
|
2270
|
+
"time-zone": { type: "string" },
|
|
2271
|
+
},
|
|
2272
|
+
false,
|
|
2273
|
+
);
|
|
2274
|
+
const visibility = text(values, "people-visibility");
|
|
2275
|
+
const weekStart = text(values, "week-start");
|
|
2276
|
+
const zone = text(values, "time-zone");
|
|
2277
|
+
if (visibility === undefined && weekStart === undefined && zone === undefined) {
|
|
2278
|
+
throw misuse(`Say what to change: ${WORKSPACE_FLAGS}`);
|
|
2279
|
+
}
|
|
2280
|
+
const session = await open(io, values);
|
|
2281
|
+
const changed = await session.client.updateWorkspace({
|
|
2282
|
+
...(visibility === undefined ? {} : { peopleVisibility: peopleVisibilityOf(visibility) }),
|
|
2283
|
+
...(weekStart === undefined
|
|
2284
|
+
? {}
|
|
2285
|
+
: { weekStart: wordOf(weekStart, WEEK_STARTS, "a day the week begins on") }),
|
|
2286
|
+
...(zone === undefined ? {} : { timeZone: zone === "none" ? null : zone }),
|
|
2287
|
+
});
|
|
2288
|
+
await rememberZone(io, session, changed.timeZone ?? undefined);
|
|
2289
|
+
answer(io, session, changed, () => formatWorkspace(changed));
|
|
2290
|
+
return EXIT.ok;
|
|
2291
|
+
}
|
|
2292
|
+
if (verb === "pack-token") {
|
|
2293
|
+
const { values, positionals } = parse(rest, PACK_TOKEN_FLAGS);
|
|
2294
|
+
if (positionals.length > 0) {
|
|
2295
|
+
throw misuse(
|
|
2296
|
+
"The workspace's token names no project: console workspace pack-token --connect, --stdin or --remove",
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
const way = wayOf(values);
|
|
2300
|
+
const session = await open(io, values);
|
|
2301
|
+
const { client } = session;
|
|
2302
|
+
return packToken(
|
|
2303
|
+
io,
|
|
2304
|
+
session,
|
|
2305
|
+
way,
|
|
2306
|
+
{
|
|
2307
|
+
set: (token) => client.setWorkspacePackToken({ token }),
|
|
2308
|
+
remove: () => client.removeWorkspacePackToken(),
|
|
2309
|
+
connect: () => client.connectWorkspacePackToken(),
|
|
2310
|
+
async standing() {
|
|
2311
|
+
const state = await client.workspacePackTokenConnection();
|
|
2312
|
+
return state.status === "connected"
|
|
2313
|
+
? { status: "connected", held: state.workspace }
|
|
2314
|
+
: state;
|
|
2315
|
+
},
|
|
2316
|
+
},
|
|
2317
|
+
formatWorkspace,
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
throw misuse(WORKSPACE_USAGE);
|
|
2321
|
+
};
|
|
2322
|
+
|
|
2173
2323
|
const PERSON_USAGE =
|
|
2174
2324
|
"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>";
|
|
2175
2325
|
|
|
@@ -2293,35 +2443,118 @@ const editProject: Command = async (args, io) => {
|
|
|
2293
2443
|
return EXIT.ok;
|
|
2294
2444
|
};
|
|
2295
2445
|
|
|
2296
|
-
/**
|
|
2297
|
-
const
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2446
|
+
/** How a pack's connection ended short of a token (ADR-0036), in words for the person. */
|
|
2447
|
+
const PACK_CONNECT_ENDED = {
|
|
2448
|
+
expired: "The code expired before anyone approved it. Run the command again for a new one.",
|
|
2449
|
+
declined: "The request was declined at SkillCDN, or the code was spent.",
|
|
2450
|
+
refused:
|
|
2451
|
+
"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.",
|
|
2452
|
+
} as const;
|
|
2453
|
+
|
|
2454
|
+
/** How a SkillCDN token is to change, one of the three the flags name. */
|
|
2455
|
+
const wayOf = (values: Values): "connect" | "stdin" | "remove" => {
|
|
2456
|
+
const ways = (["connect", "stdin", "remove"] as const).filter((way) => on(values, way));
|
|
2457
|
+
const [way] = ways;
|
|
2458
|
+
if (way === undefined || ways.length !== 1) {
|
|
2307
2459
|
throw misuse(
|
|
2308
|
-
"Say one of --stdin, which reads
|
|
2460
|
+
"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.",
|
|
2309
2461
|
);
|
|
2310
2462
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2463
|
+
return way;
|
|
2464
|
+
};
|
|
2465
|
+
|
|
2466
|
+
/** Whose SkillCDN token the command works: a project's, or the workspace's for the organization's pack. */
|
|
2467
|
+
interface TokenHolder<T> {
|
|
2468
|
+
set(token: string): Promise<T>;
|
|
2469
|
+
remove(): Promise<T>;
|
|
2470
|
+
connect(): Promise<RestPackConnectStarted>;
|
|
2471
|
+
standing(): Promise<
|
|
2472
|
+
| { readonly status: "none" }
|
|
2473
|
+
| { readonly status: "pending" }
|
|
2474
|
+
| { readonly status: "ended"; readonly reason: keyof typeof PACK_CONNECT_ENDED }
|
|
2475
|
+
| { readonly status: "connected"; readonly held: T }
|
|
2476
|
+
>;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
/**
|
|
2480
|
+
* The SkillCDN token (ADR-0028): connected with a code a person approves at SkillCDN (ADR-0036),
|
|
2481
|
+
* or read from standard input and never from an argument, so that no shell keeps it; or taken
|
|
2482
|
+
* away. The same for a project's and for the workspace's.
|
|
2483
|
+
*/
|
|
2484
|
+
async function packToken<T>(
|
|
2485
|
+
io: CliIo,
|
|
2486
|
+
session: Session,
|
|
2487
|
+
way: "connect" | "stdin" | "remove",
|
|
2488
|
+
holder: TokenHolder<T>,
|
|
2489
|
+
format: (held: T) => string,
|
|
2490
|
+
): Promise<number> {
|
|
2491
|
+
if (way === "remove") {
|
|
2492
|
+
const changed = await holder.remove();
|
|
2493
|
+
answer(io, session, changed, () => format(changed));
|
|
2316
2494
|
return EXIT.ok;
|
|
2317
2495
|
}
|
|
2496
|
+
if (way === "connect") {
|
|
2497
|
+
const started = await holder.connect();
|
|
2498
|
+
io.stdout(
|
|
2499
|
+
`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`,
|
|
2500
|
+
);
|
|
2501
|
+
for (;;) {
|
|
2502
|
+
await io.sleep(started.intervalSeconds * 1000);
|
|
2503
|
+
const state = await holder.standing();
|
|
2504
|
+
if (state.status === "pending") {
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
if (state.status === "connected") {
|
|
2508
|
+
answer(io, session, state.held, () => `Connected.\n${format(state.held)}`);
|
|
2509
|
+
return EXIT.ok;
|
|
2510
|
+
}
|
|
2511
|
+
if (state.status === "ended") {
|
|
2512
|
+
throw failed(PACK_CONNECT_ENDED[state.reason]);
|
|
2513
|
+
}
|
|
2514
|
+
throw failed(
|
|
2515
|
+
"The console no longer waits for the connection; run the command again for a new code.",
|
|
2516
|
+
);
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2318
2519
|
const token = (await io.readStdin()).trim();
|
|
2319
2520
|
if (token.length === 0) {
|
|
2320
2521
|
throw failed("No token was given on standard input.");
|
|
2321
2522
|
}
|
|
2322
|
-
const changed = await
|
|
2323
|
-
answer(io, session, changed, () =>
|
|
2523
|
+
const changed = await holder.set(token);
|
|
2524
|
+
answer(io, session, changed, () => format(changed));
|
|
2324
2525
|
return EXIT.ok;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
const PACK_TOKEN_FLAGS = {
|
|
2529
|
+
connect: { type: "boolean" },
|
|
2530
|
+
stdin: { type: "boolean" },
|
|
2531
|
+
remove: { type: "boolean" },
|
|
2532
|
+
} as const;
|
|
2533
|
+
|
|
2534
|
+
const projectPackToken: Command = async (args, io) => {
|
|
2535
|
+
const { values, positionals } = parse(args, PACK_TOKEN_FLAGS);
|
|
2536
|
+
const key = theOne(
|
|
2537
|
+
positionals,
|
|
2538
|
+
"Say which project: console project pack-token <key> --connect, --stdin or --remove",
|
|
2539
|
+
);
|
|
2540
|
+
const way = wayOf(values);
|
|
2541
|
+
const session = await open(io, values);
|
|
2542
|
+
const project = session.client.project(key);
|
|
2543
|
+
return packToken(
|
|
2544
|
+
io,
|
|
2545
|
+
session,
|
|
2546
|
+
way,
|
|
2547
|
+
{
|
|
2548
|
+
set: (token) => project.setPackToken({ token }),
|
|
2549
|
+
remove: () => project.removePackToken(),
|
|
2550
|
+
connect: () => project.connectPackToken(),
|
|
2551
|
+
async standing() {
|
|
2552
|
+
const state = await project.packTokenConnection();
|
|
2553
|
+
return state.status === "connected" ? { status: "connected", held: state.project } : state;
|
|
2554
|
+
},
|
|
2555
|
+
},
|
|
2556
|
+
formatProject,
|
|
2557
|
+
);
|
|
2325
2558
|
};
|
|
2326
2559
|
|
|
2327
2560
|
const projectIcon: Command = async (args, io) => {
|
|
@@ -2548,6 +2781,7 @@ const COMMANDS: Readonly<Record<string, Command>> = {
|
|
|
2548
2781
|
rules,
|
|
2549
2782
|
people,
|
|
2550
2783
|
person,
|
|
2784
|
+
workspace,
|
|
2551
2785
|
project,
|
|
2552
2786
|
members,
|
|
2553
2787
|
member,
|
|
@@ -2699,6 +2933,8 @@ function hintFor(error: ApiError, io: CliIo, seen: ConsoleSeen): string {
|
|
|
2699
2933
|
return "The person is listed already. console member role <login> <owner|member> changes what they are.\n";
|
|
2700
2934
|
case "member.not_found":
|
|
2701
2935
|
return "The person is not listed in the project. console members lists those who are.\n";
|
|
2936
|
+
case "member.open_project":
|
|
2937
|
+
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";
|
|
2702
2938
|
case "category.key_taken":
|
|
2703
2939
|
return "A category has that key already. console categories lists them.\n";
|
|
2704
2940
|
case "category.not_found":
|